Merge pull request #16612 from supabase/feat/project-exhaustion-warnings

Resource exhaustion notifications and badges
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-09-13 15:19:25 +08:00
commit 62f3e4059f
26 files changed
+927 -435

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
export const get = jest.fn()
export const post = jest.fn()
export const patch = jest.fn()
export const del = jest.fn()
export const put = jest.fn()
export const head = jest.fn()
export const trace = jest.fn()
export const options = jest.fn()
@@ -22,9 +22,7 @@ import {
} from 'components/layouts/ProjectLayout/ProjectContext'
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
export interface AddOnsProps {}
const AddOns = ({}: AddOnsProps) => {
const AddOns = () => {
const { ref: projectRef } = useParams()
const snap = useSubscriptionPageStateSnapshot()
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
@@ -1,18 +1,20 @@
import dayjs from 'dayjs'
import { IconBarChart2 } from 'ui'
import Panel from 'components/ui/Panel'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { DataPoint } from 'data/analytics/constants'
import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
import dayjs from 'dayjs'
import Link from 'next/link'
import { Alert, Button, IconBarChart2 } from 'ui'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { getAddons } from '../Subscription/Subscription.utils'
import SectionContent from './SectionContent'
import SectionHeader from './SectionHeader'
import { USAGE_CATEGORIES } from './Usage.constants'
import { getUpgradeUrl } from './Usage.utils'
import UsageBarChart from './UsageBarChart'
import Panel from 'components/ui/Panel'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { getAddons } from '../Subscription/Subscription.utils'
import { CPUWarnings, DiskIOBandwidthWarnings, RAMWarnings } from './UsageWarningAlerts'
import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
export interface InfrastructureProps {
projectRef: string
@@ -28,11 +30,12 @@ const Infrastructure = ({
currentBillingCycleSelected,
}: InfrastructureProps) => {
const { data: subscription } = useProjectSubscriptionV2Query({ projectRef })
const { data: resourceWarnings } = useResourceWarningsQuery()
const projectResourceWarnings = resourceWarnings?.find((x) => x.project === projectRef)
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'infra')
const upgradeUrl = getUpgradeUrl(projectRef, subscription)
const isFreeTier = subscription?.plan?.id === 'free'
const isFreePlan = subscription?.plan?.id === 'free'
const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef })
const selectedAddons = addons?.selected_addons ?? []
@@ -125,81 +128,14 @@ const Infrastructure = ({
<SectionContent section={attribute}>
{attribute.key === 'disk_io_consumption' && (
<>
{hasLatest && latestIoBudgetConsumption >= 100 ? (
<Alert withIcon variant="danger" title="Your Disk IO Budget has been used up">
<p className="mb-4">
Your workload has used up all your Disk IO Budget and is now running at the
baseline performance. If you need consistent disk performance, consider
upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreeTier ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : hasLatest && latestIoBudgetConsumption >= 80 ? (
<Alert
withIcon
variant="danger"
title="You are close to running out of Disk IO Budget"
>
<p className="mb-4">
Your workload has consumed {latestIoBudgetConsumption}% of your Disk IO
Budget. If you use up all your Disk IO Budget, your instance will reverted
to baseline performance. If you need consistent disk performance, consider
upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreeTier ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : currentBillingCycleSelected && highestIoBudgetConsumption >= 100 ? (
<Alert
withIcon
variant="warning"
title="You ran out of IO Budget at least once"
>
<p className="mb-4">
Your workload has used up all your Disk IO Budget and reverted to baseline
performance at least once during this billing cycle. If you need consistent
disk performance, consider upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreeTier ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : currentBillingCycleSelected && highestIoBudgetConsumption >= 80 ? (
<Alert
withIcon
variant="warning"
title="You were close to using all your IO Budget at least once"
>
<p className="mb-4">
Your workload has consumed {highestIoBudgetConsumption}% of your Disk IO
budget during this billing cycle. If you use up all your Disk IO Budget,
your instance will reverted to baseline performance. If you need consistent
disk performance, consider upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreeTier ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : null}
<DiskIOBandwidthWarnings
upgradeUrl={upgradeUrl}
isFreePlan={isFreePlan}
hasLatest={hasLatest}
currentBillingCycleSelected={currentBillingCycleSelected}
latestIoBudgetConsumption={latestIoBudgetConsumption}
highestIoBudgetConsumption={highestIoBudgetConsumption}
/>
<div className="space-y-1">
<p>Disk IO Bandwidth</p>
@@ -246,6 +182,21 @@ const Infrastructure = ({
</div>
</>
)}
{attribute.key === 'max_cpu_usage' && (
<CPUWarnings
isFreePlan={isFreePlan}
upgradeUrl={upgradeUrl}
severity={projectResourceWarnings?.cpu_exhaustion}
/>
)}
{attribute.key === 'ram_usage' && (
<RAMWarnings
isFreePlan={isFreePlan}
upgradeUrl={upgradeUrl}
severity={projectResourceWarnings?.memory_and_swap_exhaustion}
/>
)}
<div className="space-y-1">
<div className="flex flex-row justify-between">
{attribute.key === 'disk_io_consumption' ? (
@@ -1,15 +1,16 @@
import clsx from 'clsx'
import { useParams } from 'common'
import dayjs from 'dayjs'
import { useMemo, useRef, useState } from 'react'
import { InView } from 'react-intersection-observer'
import { Button, IconAlertCircle, IconExternalLink, IconInfo, IconLoader, cn } from 'ui'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
import dayjs from 'dayjs'
import { useSelectedOrganization } from 'hooks'
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants'
import { useMemo, useRef, useState } from 'react'
import { InView } from 'react-intersection-observer'
import { Button, IconAlertCircle, IconExternalLink, IconInfo, IconLoader, cn } from 'ui'
import Activity from './Activity'
import Bandwidth from './Bandwidth'
import Infrastructure from './Infrastructure'
@@ -0,0 +1,71 @@
import { AlertTitle } from '@ui/components/shadcn/ui/alert'
import Link from 'next/link'
import { AlertDescription_Shadcn_, Alert_Shadcn_, Button, IconAlertCircle } from 'ui'
interface CPUWarningsProps {
isFreePlan: boolean
upgradeUrl: string
severity?: 'warning' | 'critical' | null
}
const CPUWarnings = ({ isFreePlan, upgradeUrl, severity }: CPUWarningsProps) => {
if (severity === 'warning') {
return (
<Alert_Shadcn_ variant="warning">
<IconAlertCircle />
<AlertTitle>Your max CPU usage has exceeded 80%</AlertTitle>
<AlertDescription_Shadcn_>
High CPU usage could result in slower queries, disruption of daily back up routines, and
in rare cases, your instance may become unresponsive. If you need more resources, consider
upgrading to a larger compute add-on.
</AlertDescription_Shadcn_>
<div className="mt-3 flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/platform/exhaust-cpu">
<a>
<Button type="default">Learn more</Button>
</a>
</Link>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</div>
</Alert_Shadcn_>
)
}
if (severity === 'critical') {
return (
<Alert_Shadcn_ variant="destructive">
<IconAlertCircle />
<AlertTitle>Your max CPU usage has reached 100%</AlertTitle>
<AlertDescription_Shadcn_>
High CPU usage could result in slower queries, disruption of daily back up routines, and
in rare cases, your instance may become unresponsive. If you need more resources, consider
upgrading to a larger compute add-on.
</AlertDescription_Shadcn_>
<div className="mt-3 flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/platform/exhaust-cpu">
<a>
<Button type="default">Learn more</Button>
</a>
</Link>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</div>
</Alert_Shadcn_>
)
}
return null
}
export default CPUWarnings
@@ -0,0 +1,106 @@
import Link from 'next/link'
import { Alert, Button } from 'ui'
// [Joshen] In the future, conditionals should be from resource exhaustion endpoint as single source of truth
interface DiskIOBandwidthWarningsProps {
isFreePlan: boolean
hasLatest: boolean
upgradeUrl: string
currentBillingCycleSelected: boolean
latestIoBudgetConsumption: number
highestIoBudgetConsumption: number
}
const DiskIOBandwidthWarnings = ({
isFreePlan,
hasLatest,
currentBillingCycleSelected,
upgradeUrl,
latestIoBudgetConsumption,
highestIoBudgetConsumption,
}: DiskIOBandwidthWarningsProps) => {
if (hasLatest && latestIoBudgetConsumption >= 100) {
return (
<Alert withIcon variant="danger" title="Your Disk IO Budget has been used up">
<p className="mb-4">
Your workload has used up all your Disk IO Budget and is now running at the baseline
performance. If you need consistent disk performance, consider upgrading to a larger
compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
)
}
if (hasLatest && latestIoBudgetConsumption >= 80) {
return (
<Alert withIcon variant="danger" title="You are close to running out of Disk IO Budget">
<p className="mb-4">
Your workload has consumed {latestIoBudgetConsumption}% of your Disk IO Budget. If you use
up all your Disk IO Budget, your instance will reverted to baseline performance. If you
need consistent disk performance, consider upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
)
}
if (currentBillingCycleSelected && highestIoBudgetConsumption >= 100) {
return (
<Alert withIcon variant="warning" title="You ran out of IO Budget at least once">
<p className="mb-4">
Your workload has used up all your Disk IO Budget and reverted to baseline performance at
least once during this billing cycle. If you need consistent disk performance, consider
upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
)
}
if (currentBillingCycleSelected && highestIoBudgetConsumption >= 80) {
return (
<Alert
withIcon
variant="warning"
title="You were close to using all your IO Budget at least once"
>
<p className="mb-4">
Your workload has consumed {highestIoBudgetConsumption}% of your Disk IO budget during
this billing cycle. If you use up all your Disk IO Budget, your instance will reverted to
baseline performance. If you need consistent disk performance, consider upgrading to a
larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
)
}
return null
}
export default DiskIOBandwidthWarnings
@@ -0,0 +1,71 @@
import { AlertTitle } from '@ui/components/shadcn/ui/alert'
import Link from 'next/link'
import { AlertDescription_Shadcn_, Alert_Shadcn_, Button, IconAlertCircle } from 'ui'
interface RAMWarningsProps {
isFreePlan: boolean
upgradeUrl: string
severity?: 'warning' | 'critical' | null
}
const RAMWarnings = ({ isFreePlan, upgradeUrl, severity }: RAMWarningsProps) => {
if (severity === 'warning') {
return (
<Alert_Shadcn_ variant="warning">
<IconAlertCircle />
<AlertTitle>Your memory usage has exceeded 80%</AlertTitle>
<AlertDescription_Shadcn_>
High memory usage could result in overall degraded performance, and in rare cases, your
instance may become unresponsive. If you need more resources, consider upgrading to a
larger compute add-on.
</AlertDescription_Shadcn_>
<div className="mt-3 flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/platform/exhaust-ram">
<a>
<Button type="default">Learn more</Button>
</a>
</Link>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</div>
</Alert_Shadcn_>
)
}
if (severity === 'critical') {
return (
<Alert_Shadcn_ variant="destructive">
<IconAlertCircle />
<AlertTitle>Your memory usage has reached 100%</AlertTitle>
<AlertDescription_Shadcn_>
High memory usage could result in overall degraded performance, and in rare cases, your
instance may become unresponsive. If you need more resources, consider upgrading to a
larger compute add-on.
</AlertDescription_Shadcn_>
<div className="mt-3 flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/platform/exhaust-cpu">
<a>
<Button type="default">Learn more</Button>
</a>
</Link>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</div>
</Alert_Shadcn_>
)
}
return null
}
export default RAMWarnings
@@ -0,0 +1,5 @@
import CPUWarnings from './CPUWarnings'
import DiskIOBandwidthWarnings from './DiskIOBandwidthWarnings'
import RAMWarnings from './RAMWarnings'
export { CPUWarnings, DiskIOBandwidthWarnings, RAMWarnings }
@@ -1,24 +1,19 @@
import {
Badge,
IconAlertTriangle,
IconGitBranch,
IconGitHub,
IconLoader,
IconPauseCircle,
IconTriangle,
} from 'ui'
import { IconGitBranch, IconGitHub } from 'ui'
import CardButton from 'components/ui/CardButton'
import { useProjectReadOnlyStatus } from 'hooks/misc/useProjectReadOnlyStatus'
import { BASE_PATH, PROJECT_STATUS } from 'lib/constants'
import { BASE_PATH } from 'lib/constants'
import { Project } from 'types'
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
import { ResourceWarning } from 'data/usage/resource-warnings-query'
import { ProjectCardStatus } from './ProjectCardStatus'
import { inferProjectStatus } from './ProjectCard.utils'
export interface ProjectCardProps {
project: Project
rewriteHref?: string
githubIntegration?: IntegrationProjectConnection
vercelIntegration?: IntegrationProjectConnection
resourceWarnings?: ResourceWarning
}
const ProjectCard = ({
@@ -26,30 +21,26 @@ const ProjectCard = ({
rewriteHref,
githubIntegration,
vercelIntegration,
resourceWarnings,
}: ProjectCardProps) => {
const { name, ref: projectRef } = project
const desc = `${project.cloud_provider} | ${project.region}`
const isReadonly = useProjectReadOnlyStatus(projectRef)
const isBranchingEnabled = project.preview_branch_refs?.length > 0
const isBranchingEnabled = project.preview_branch_refs.length > 0
const isGithubIntegrated = githubIntegration !== undefined
const isVercelIntegrated = vercelIntegration !== undefined
const githubRepository = githubIntegration?.metadata.name ?? undefined
// Project status should supersede is read only status
const isHealthy = project.status === PROJECT_STATUS.ACTIVE_HEALTHY
const isPausing =
project.status === PROJECT_STATUS.GOING_DOWN || project.status === PROJECT_STATUS.PAUSING
const isPaused = project.status === PROJECT_STATUS.INACTIVE
const isRestoring = project.status === PROJECT_STATUS.RESTORING
const projectStatus = inferProjectStatus(project)
return (
<li className="col-span-1 list-none">
<CardButton
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
containerHeightClassName="h-44"
title={
<div className="w-full justify-between space-y-1">
<p className="flex-shrink truncate">{name}</p>
<div className="w-full justify-between space-y-1.5 px-6">
<p className="flex-shrink truncate text-base">{name}</p>
<span className="text-sm lowercase text-scale-1000">{desc}</span>
<div className="flex items-center space-x-1.5">
{isVercelIntegrated && (
<div className="w-fit p-1 border rounded-md flex items-center border-scale-600">
@@ -77,55 +68,16 @@ const ProjectCard = ({
</div>
}
footer={
<div className="flex items-end justify-between">
<span className="text-sm lowercase text-scale-1000">{desc}</span>
{isHealthy && isReadonly && (
<div className="grow text-right">
<Badge color="yellow">
<div className="flex items-center gap-2">
<IconAlertTriangle size={14} strokeWidth={2} />
<span className="truncate">Read-only mode</span>
</div>
</Badge>
</div>
)}
{isRestoring && (
<div className="grow text-right">
<Badge color="brand">
<div className="flex items-center gap-2">
<IconLoader className="animate-spin" size={14} strokeWidth={2} />
<span className="truncate">Restoring</span>
</div>
</Badge>
</div>
)}
{isPausing && (
<div className="grow text-right">
<Badge color="scale">
<div className="flex items-center gap-2">
<IconLoader className="animate-spin" size={14} strokeWidth={2} />
<span className="truncate">Pausing</span>
</div>
</Badge>
</div>
)}
{isPaused && (
<div className="grow text-right">
<Badge color="scale">
<div className="flex items-center gap-2">
<IconPauseCircle size={14} strokeWidth={2} />
<span className="truncate">Paused</span>
</div>
</Badge>
</div>
<div className="mb-[-26px]">
{resourceWarnings && (
<ProjectCardStatus
projectStatus={projectStatus}
resourceWarnings={resourceWarnings}
/>
)}
</div>
}
/>
></CardButton>
</li>
)
}
@@ -0,0 +1,34 @@
import { PROJECT_STATUS } from 'lib/constants'
import { Project } from 'types'
export const inferProjectStatus = (project: Project) => {
let status = undefined
switch (project.status) {
case PROJECT_STATUS.ACTIVE_HEALTHY:
status = 'isHealthy'
break
case PROJECT_STATUS.GOING_DOWN:
case PROJECT_STATUS.PAUSING:
status = 'isPausing'
break
case PROJECT_STATUS.INACTIVE:
status = 'isPaused'
break
case PROJECT_STATUS.RESTORING:
status = 'isRestoring'
break
case PROJECT_STATUS.UNKNOWN:
case PROJECT_STATUS.COMING_UP:
status = 'isComingUp'
break
}
return status as InferredProjectStatus
}
export type InferredProjectStatus =
| 'isHealthy'
| 'isPausing'
| 'isPaused'
| 'isRestoring'
| 'isComingUp'
| undefined
@@ -0,0 +1,98 @@
import { RESOURCE_WARNING_MESSAGES } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants'
import { ResourceWarning } from 'data/usage/resource-warnings-query'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
IconAlertTriangle,
IconLoader,
IconPauseCircle,
IconRefreshCw,
} from 'ui'
import { InferredProjectStatus } from './ProjectCard.utils'
import { getWarningContent } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils'
export interface ProjectCardWarningsProps {
resourceWarnings: ResourceWarning
projectStatus: InferredProjectStatus
}
export const ProjectCardStatus = ({
resourceWarnings,
projectStatus,
}: ProjectCardWarningsProps) => {
//const showResourceExhaustionWarnings = useFlag('resourceExhaustionWarnings')
const showResourceExhaustionWarnings = false
// [Joshen] Read only takes higher precedence over multiple resource warnings
const activeWarnings = resourceWarnings.is_readonly_mode_enabled
? ['is_readonly_mode_enabled']
: Object.keys(resourceWarnings).filter(
(property) =>
property !== 'project' &&
property !== 'is_readonly_mode_enabled' &&
resourceWarnings[property as keyof typeof resourceWarnings] !== null
)
const hasCriticalWarning = activeWarnings.some(
(x) => resourceWarnings[x as keyof typeof resourceWarnings] === 'critical'
)
const isCritical = activeWarnings.includes('is_readonly_mode_enabled') || hasCriticalWarning
const getTitle = () => {
if (projectStatus === 'isPaused') return 'Project is paused'
if (projectStatus === 'isPausing') return 'Project is pausing'
if (projectStatus === 'isComingUp') return 'Project is coming up'
if (projectStatus === 'isRestoring') return 'Project is restoring'
// If none of the paused/restoring states match, proceed with the default logic
return activeWarnings.length > 1
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.cardContent[
hasCriticalWarning ? 'critical' : 'warning'
].title
: getWarningContent(resourceWarnings, activeWarnings[0], 'cardContent')?.title
}
const getDescription = () => {
if (projectStatus === 'isPaused') return 'This project will not accept requests until resumed'
if (projectStatus === 'isPausing') return 'The pause process will complete in a few minutes'
if (projectStatus === 'isComingUp') return 'Your project will be ready in a few minutes'
if (projectStatus === 'isRestoring') return 'Your project will be ready in a few minutes'
// If none of the paused/restoring states match, proceed with the default logic
return activeWarnings.length > 1 && showResourceExhaustionWarnings
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.cardContent[
hasCriticalWarning ? 'critical' : 'warning'
].description
: getWarningContent(resourceWarnings, activeWarnings[0], 'cardContent')?.description
}
const alertTitle = getTitle()
const alertDescription = getDescription()
const alertType = isCritical
? 'destructive'
: projectStatus === 'isPaused'
? 'default'
: 'warning'
if (activeWarnings.length === 0 && projectStatus === 'isHealthy') return <div className="py-2" />
return (
<div>
<Alert_Shadcn_
variant={alertType}
className="border-0 rounded-none rounded-b-md my-2 mb-2.5 [&>svg]:w-[28px] [&>svg]:h-[28px] [&>svg]:mt-1 [&>svg]:p-1.5 [&>svg]:left-6 pl-6"
>
{projectStatus === 'isPaused' || projectStatus === 'isPausing' ? (
<IconPauseCircle strokeWidth={2} />
) : projectStatus === 'isRestoring' || projectStatus === 'isComingUp' ? (
<IconRefreshCw size={14} strokeWidth={2} />
) : (
<IconAlertTriangle strokeWidth={2} />
)}
<AlertTitle_Shadcn_ className="text-sm mb-0.5">{alertTitle}</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="text-xs">{alertDescription}</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</div>
)
}
@@ -11,6 +11,7 @@ import {
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { usePermissionsQuery } from 'data/permissions/permissions-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { ResourceWarning, useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
import { IS_PLATFORM } from 'lib/constants'
import { makeRandomString } from 'lib/helpers'
import { Organization, Project, ResponseError } from 'types'
@@ -35,6 +36,7 @@ const ProjectList = ({ rewriteHref }: ProjectListProps) => {
isError: isErrorPermissions,
error: permissionsError,
} = usePermissionsQuery()
const { data: resourceWarnings } = useResourceWarningsQuery()
const { data: allOverdueInvoices } = useOverdueInvoicesQuery({ enabled: IS_PLATFORM })
const projectsByOrg = groupBy(allProjects, 'organization_id')
const isLoadingPermissions = IS_PLATFORM ? _isLoadingPermissions : false
@@ -50,6 +52,7 @@ const ProjectList = ({ rewriteHref }: ProjectListProps) => {
overdueInvoices={(allOverdueInvoices ?? []).filter(
(it) => it.organization_id === organization.id
)}
resourceWarnings={resourceWarnings ?? []}
rewriteHref={rewriteHref}
isLoadingPermissions={isLoadingPermissions}
isErrorPermissions={isErrorPermissions}
@@ -72,6 +75,7 @@ type OrganizationProjectsProps = {
organization: Organization
projects: Project[]
overdueInvoices: OverdueInvoicesResponse[]
resourceWarnings: ResourceWarning[]
isLoadingPermissions: boolean
isErrorPermissions: boolean
permissionsError: ResponseError | null
@@ -82,9 +86,10 @@ type OrganizationProjectsProps = {
}
const OrganizationProjects = ({
organization: { id, name, slug, subscription_id },
organization: { name, slug, subscription_id },
projects,
overdueInvoices,
resourceWarnings,
isLoadingPermissions,
isErrorPermissions,
permissionsError,
@@ -150,6 +155,9 @@ const OrganizationProjects = ({
key={makeRandomString(5)}
project={project}
rewriteHref={rewriteHref ? rewriteHref(project.ref) : undefined}
resourceWarnings={resourceWarnings.find(
(resourceWarning) => resourceWarning.project === project.ref
)}
/>
))
)}
@@ -134,14 +134,14 @@ const Addons = () => {
<ScaffoldSection>
<ScaffoldSectionDetail>
<div className="space-y-6">
<p>Optimized compute</p>
<p className="m-0">Optimized compute</p>
<div className="space-y-2">
<p className="text-sm text-scale-1100">More information</p>
<p className="text-sm text-scale-1100 m-0">More information</p>
<div>
<Link href="https://supabase.com/docs/guides/platform/compute-add-ons">
<a target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm">About compute add-ons</p>
<p className="text-sm m-0">About compute add-ons</p>
<IconExternalLink size={16} strokeWidth={1.5} />
</div>
</a>
@@ -151,7 +151,7 @@ const Addons = () => {
<Link href="https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pooler">
<a target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm">Connection Pooler</p>
<p className="text-sm m-0">Connection Pooler</p>
<IconExternalLink size={16} strokeWidth={1.5} />
</div>
</a>
@@ -339,14 +339,14 @@ const Addons = () => {
<ScaffoldSection>
<ScaffoldSectionDetail>
<div className="space-y-6">
<p>Point in time recovery</p>
<p className="m-0">Point in time recovery</p>
<div className="space-y-2">
<p className="text-sm text-scale-1100">More information</p>
<p className="text-sm text-scale-1100 m-0">More information</p>
<div>
<Link href="https://supabase.com/docs/guides/platform/backups#point-in-time-recovery">
<a target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm">About PITR backups</p>
<p className="text-sm m-0">About PITR backups</p>
<IconExternalLink size={16} strokeWidth={1.5} />
</div>
</a>
@@ -403,14 +403,14 @@ const Addons = () => {
<ScaffoldSection>
<ScaffoldSectionDetail>
<div className="space-y-6">
<p>Custom domain</p>
<p className="m-0">Custom domain</p>
<div className="space-y-2">
<p className="text-sm text-scale-1100">More information</p>
<p className="text-sm text-scale-1100 m-0">More information</p>
<div>
<Link href="https://supabase.com/docs/guides/platform/custom-domains">
<a target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm">About custom domains</p>
<p className="text-sm m-0">About custom domains</p>
<IconExternalLink size={16} strokeWidth={1.5} />
</div>
</a>
@@ -1,8 +1,16 @@
import { useParams } from 'common'
import dayjs from 'dayjs'
import Link from 'next/link'
import { Fragment, useMemo, useState } from 'react'
import { IconBarChart2, IconExternalLink } from 'ui'
import { useParams } from 'common'
import { getAddons } from 'components/interfaces/BillingV2/Subscription/Subscription.utils'
import {
CPUWarnings,
DiskIOBandwidthWarnings,
RAMWarnings,
} from 'components/interfaces/BillingV2/Usage/UsageWarningAlerts'
import UsageBarChart from 'components/interfaces/Organization/Usage/UsageBarChart'
import {
ScaffoldContainer,
ScaffoldDivider,
@@ -19,10 +27,8 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSelectedOrganization } from 'hooks'
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants'
import { Alert, Button, IconBarChart2, IconExternalLink } from 'ui'
import { getAddons } from '../../BillingV2/Subscription/Subscription.utils'
import UsageBarChart from '../../Organization/Usage/UsageBarChart'
import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants'
import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
const InfrastructureActivity = () => {
const { ref: projectRef } = useParams()
@@ -30,17 +36,17 @@ const InfrastructureActivity = () => {
const isOrgBilling = !!organization?.subscription_id
const [dateRange, setDateRange] = useState<any>()
const currentBillingCycleSelected = true
const upgradeUrl = '/'
const categoryMeta = INFRA_ACTIVITY_METRICS.find((category) => category.key === 'infra')
const { data: subscription, isLoading: isLoadingSubscription } = useOrgSubscriptionQuery({
orgSlug: organization?.slug,
})
const isFreePlan = subscription?.plan?.id === 'free'
const { data: resourceWarnings } = useResourceWarningsQuery()
const projectResourceWarnings = resourceWarnings?.find((x) => x.project === projectRef)
const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef })
const selectedAddons = addons?.selected_addons ?? []
const { computeInstance } = getAddons(selectedAddons)
const currentComputeInstanceSpecs = computeInstance?.variant?.meta ?? {
baseline_disk_io_mbs: 87,
@@ -50,6 +56,27 @@ const InfrastructureActivity = () => {
memory_gb: 1,
}
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 =
subscription === undefined
? `/`
: subscription.plan.id === 'free'
? `/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
@@ -116,6 +143,13 @@ const InfrastructureActivity = () => {
dateFormat,
})
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 = Math.max(
...(ioBudgetData?.data || []).map((x) => Number(x.disk_io_consumption) ?? 0),
0
@@ -180,11 +214,11 @@ const InfrastructureActivity = () => {
<div className="sticky top-32 space-y-6">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<p className="text-base capitalize">{attribute.name}</p>
<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-scale-1000 pr-8">
<p key={`desc-${idx}`} className="text-sm text-scale-1000 pr-8 m-0">
{value}
</p>
))}
@@ -199,7 +233,7 @@ const InfrastructureActivity = () => {
<Link href={link.url}>
<a target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm">{link.name}</p>
<p className="text-sm m-0">{link.name}</p>
<IconExternalLink size={16} strokeWidth={1.5} />
</div>
</a>
@@ -212,46 +246,14 @@ const InfrastructureActivity = () => {
<ScaffoldSectionContent>
{attribute.key === 'disk_io_consumption' && (
<>
{currentBillingCycleSelected && highestIoBudgetConsumption >= 100 ? (
<Alert
withIcon
variant="danger"
title="IO Budget for today has been used up"
>
<p className="mb-4">
Your workload has used up all the burst IO throughput minutes and ran at
the baseline performance. If you need consistent disk performance,
consider upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : currentBillingCycleSelected && highestIoBudgetConsumption >= 80 ? (
<Alert
withIcon
variant="warning"
title="IO Budget for today is running out"
>
<p className="mb-4">
Your workload is about to use up all the burst IO throughput minutes
during the day. Once this is completely used up, your workload will run
at the baseline performance. If you need consistent disk performance,
consider upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : null}
<DiskIOBandwidthWarnings
upgradeUrl={upgradeUrl}
isFreePlan={isFreePlan}
hasLatest={hasLatest}
currentBillingCycleSelected={currentBillingCycleSelected}
latestIoBudgetConsumption={latestIoBudgetConsumption}
highestIoBudgetConsumption={highestIoBudgetConsumption}
/>
<div className="space-y-1">
<p>Disk IO Bandwidth</p>
@@ -302,6 +304,21 @@ const InfrastructureActivity = () => {
</div>
</>
)}
{attribute.key === 'max_cpu_usage' && (
<CPUWarnings
isFreePlan={isFreePlan}
upgradeUrl={upgradeUrl}
severity={projectResourceWarnings?.cpu_exhaustion}
/>
)}
{attribute.key === 'ram_usage' && (
<RAMWarnings
isFreePlan={isFreePlan}
upgradeUrl={upgradeUrl}
severity={projectResourceWarnings?.memory_and_swap_exhaustion}
/>
)}
<div className="space-y-1">
<div className="flex flex-row justify-between">
{attribute.key === 'disk_io_consumption' ? (
@@ -6,20 +6,17 @@ import Link from 'next/link'
import { useEffect, useRef } from 'react'
import { Badge, Button, IconArrowRight, IconLoader } from 'ui'
import { useParams } from 'common'
import { DisplayApiSettings, DisplayConfigSettings } from 'components/ui/ProjectSettings'
import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query'
import { invalidateProjectsQuery } from 'data/projects/projects-query'
import { useSelectedProject } from 'hooks'
import { getWithTimeout } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { Project } from 'types'
import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query'
import { useParams } from 'common'
export interface BuildingStateProps {
project: Project
}
const BuildingState = ({ project }: BuildingStateProps) => {
const BuildingState = () => {
const { ref } = useParams()
const project = useSelectedProject()
const queryClient = useQueryClient()
const checkServerInterval = useRef<number>()
@@ -48,6 +45,8 @@ const BuildingState = ({ project }: BuildingStateProps) => {
}
}, [])
if (project === undefined) return null
return (
<div className="mx-auto my-16 w-full max-w-6xl items-center justify-center">
<div className="mx-6 flex flex-col space-y-16">
@@ -1,12 +1,14 @@
import { useParams } from 'common'
import Link from 'next/link'
import { useMemo } from 'react'
import { Badge } from 'ui'
import {
getResourcesExceededLimits,
getResourcesExceededLimitsOrg,
} from 'components/ui/OveragesBanner/OveragesBanner.utils'
import { useProjectReadOnlyQuery } from 'data/config/project-read-only-query'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
import { useFlag, useSelectedOrganization, useSelectedProject } from 'hooks'
import { IS_PLATFORM } from 'lib/constants'
@@ -16,19 +18,11 @@ import HelpPopover from './HelpPopover'
import NotificationsPopover from './NotificationsPopover'
import OrgDropdown from './OrgDropdown'
import ProjectDropdown from './ProjectDropdown'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
import { useMemo } from 'react'
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
const selectedOrganization = useSelectedOrganization()
const selectedProject = useSelectedProject()
const { ref: projectRef } = useParams()
const { data: isReadOnlyMode } = useProjectReadOnlyQuery({
projectRef: selectedProject?.ref,
connectionString: selectedProject?.connectionString,
})
const selectedProject = useSelectedProject()
const selectedOrganization = useSelectedOrganization()
// Skip with org-based-billing, as quota is for the entire org
const { data: projectUsage } = useProjectUsageQuery(
@@ -108,18 +102,6 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
{/* Project Dropdown */}
<ProjectDropdown />
{/* [Terry] Temporary until we figure out how we want to display this permanently */}
{/* context: https://www.notion.so/supabase/DB-Disk-Size-Free-tier-Read-only-Critical-f2b8937c13a149e3ac769fe5888f6db0*/}
{isReadOnlyMode && (
<div className="ml-2">
<Link href={`/project/${projectRef}/settings/billing/usage`}>
<a>
<Badge color="red">Project is in read-only mode</Badge>
</a>
</Link>
</div>
)}
{showOverUsageBadge && (
<div className="ml-2">
<Link
@@ -4,8 +4,10 @@ import { Fragment, PropsWithChildren, ReactNode } from 'react'
import { useParams } from 'common/hooks'
import Connecting from 'components/ui/Loading'
import ResourceExhaustionWarningBanner from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner'
import { useFlag, useSelectedOrganization, useSelectedProject, withAuth } from 'hooks'
import { PROJECT_STATUS, IS_PLATFORM } from 'lib/constants'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import AppLayout from '../AppLayout/AppLayout'
import BuildingState from './BuildingState'
import ConnectingState from './ConnectingState'
import LayoutHeader from './LayoutHeader'
@@ -16,7 +18,6 @@ import { ProjectContextProvider } from './ProjectContext'
import ProjectPausedState from './ProjectPausedState'
import RestoringState from './RestoringState'
import UpgradingState from './UpgradingState'
import AppLayout from '../AppLayout/AppLayout'
// [Joshen] This is temporary while we unblock users from managing their project
// if their project is not responding well for any reason. Eventually needs a bit of an overhaul
@@ -70,6 +71,7 @@ const ProjectLayout = ({
const organizationName = selectedOrganization?.name
const navLayoutV2 = useFlag('navigationLayoutV2')
const showResourceExhaustionWarnings = useFlag('resourceExhaustionWarnings')
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
const ignorePausedState =
@@ -96,14 +98,12 @@ const ProjectLayout = ({
<div className="flex h-full">
{/* Left-most navigation side bar to access products */}
{!hideIconBar && <NavigationBar />}
{/* Product menu bar */}
{!showPausedState && (
<MenuBarWrapper isLoading={isLoading} productMenu={productMenu}>
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
</MenuBarWrapper>
)}
<main className="flex flex-col flex-1 w-full overflow-x-hidden">
{!navLayoutV2 && !hideHeader && IS_PLATFORM && <LayoutHeader />}
{showPausedState ? (
@@ -113,7 +113,10 @@ const ProjectLayout = ({
</div>
</div>
) : (
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
<ContentWrapper isLoading={isLoading}>
{showResourceExhaustionWarnings && <ResourceExhaustionWarningBanner />}
{children}
</ContentWrapper>
)}
</main>
</div>
@@ -194,7 +197,7 @@ const ContentWrapper = ({ isLoading, children }: ContentWrapperProps) => {
) : requiresDbConnection && isProjectRestoring ? (
<RestoringState />
) : requiresDbConnection && isProjectBuilding ? (
<BuildingState project={selectedProject} />
<BuildingState />
) : (
<Fragment key={selectedProject?.ref}>{children}</Fragment>
)}
@@ -224,6 +227,7 @@ export const ProjectLayoutNonBlocking = ({
const showPausedState = isPaused && !ignorePausedState
const navLayoutV2 = useFlag('navigationLayoutV2')
const showResourceExhaustionWarnings = useFlag('resourceExhaustionWarnings')
return (
<AppLayout>
@@ -251,7 +255,10 @@ export const ProjectLayoutNonBlocking = ({
</div>
</div>
) : (
children
<>
{showResourceExhaustionWarnings && <ResourceExhaustionWarningBanner />}
{children}
</>
)}
</main>
</div>
+2 -2
View File
@@ -27,7 +27,7 @@ const CardButton = ({
imgAlt,
onClick,
icon,
containerHeightClassName = 'h-32',
containerHeightClassName = '',
loading = false,
}: PropsWithChildren<CardButtonProps>) => {
const LinkContainer = ({ children }: { children: React.ReactNode }) => (
@@ -47,7 +47,7 @@ const CardButton = ({
'group relative text-left',
'bg-panel-header-light dark:bg-panel-header-dark',
'border border-panel-border-light dark:border-panel-border-dark',
'rounded-md py-4 px-6 flex flex-row',
'rounded-md py-4 flex flex-row',
'transition ease-in-out duration-150',
containerHeightClassName,
]
@@ -0,0 +1,170 @@
export const RESOURCE_WARNING_MESSAGES = {
is_readonly_mode_enabled: {
bannerContent: {
warning: {
title:
'Your project is currently in read-only mode and is no longer accepting write requests',
description:
'You will need to manually override read-only mode and reduce the disk size to below 95%',
},
critical: {
title:
'Your project is currently in read-only mode and is no longer accepting write requests',
description:
'You will need to manually override read-only mode and reduce the disk size to below 95%',
},
},
cardContent: {
warning: {
title: 'Project is in read-only mode',
description: 'Database is no longer accepting write requests.',
},
critical: {
title: 'Project is in read-only mode',
description: 'Database is no longer accepting write requests.',
},
},
docsUrl: 'https://supabase.com/docs/guides/platform/database-size#disabling-read-only-mode',
buttonText: undefined,
metric: undefined,
},
disk_io_exhaustion: {
bannerContent: {
warning: {
title:
'Your project is about to deplete its Disk IO Budget, and may become unresponsive once fully exhausted',
description:
'You will need to optimize your performance or upgrade your compute. Check the usage page for more recent and detailed statistics.',
},
critical: {
title: 'Your project has depleted its Disk IO Budget, and may become unresponsive',
description:
'You will need to optimize your performance or upgrade your compute. Check the usage page for more recent and detailed statistics.',
},
},
cardContent: {
warning: {
title: 'Project is depleting its Disk IO Budget',
description: 'It may become unresponsive if fully exhausted',
},
critical: {
title: 'Project has depleted its Disk IO Budget',
description: 'It may become unresponsive',
},
},
docsUrl: 'https://supabase.com/docs/guides/platform/exhaust-disk-io',
buttonText: 'Check usage',
metric: 'disk_io',
},
disk_space_exhaustion: {
bannerContent: {
warning: {
title:
'Your project is about to exhaust its disk space budget, and may become unresponsive once fully exhausted',
description:
'You can opt to increase your disk size up to 200GB on the database settings page.',
},
critical: {
title: 'Your project has exhausted its disk space budget, and may become unresponsive',
description:
'You can opt to increase your disk size up to 200GB on the database settings page.',
},
},
cardContent: {
warning: {
title: 'Project is exhausting disk space budget',
description: 'It may become unresponsive if fully exhausted',
},
critical: {
title: 'Project has exhausted disk space budget',
description: 'It may become unresponsive',
},
},
docsUrl: 'https://supabase.com/docs/guides/platform/database-size#disk-management',
buttonText: undefined,
metric: 'disk_space',
},
cpu_exhaustion: {
bannerContent: {
warning: {
title: 'Your project is currently facing high CPU usage, and its performance is affected',
description:
'You will need to optimize your performance or upgrade your compute. Check the usage page for more recent and detailed statistics.',
},
critical: {
title: "Your project's CPU usage is at 100% and its performance is affected",
description:
'You will need to optimize your performance or upgrade your compute. Check the usage page for more recent and detailed statistics.',
},
},
cardContent: {
warning: {
title: 'Project has high CPU usage',
description: `Performance is affected`,
},
critical: {
title: 'Project CPU usage is at 100%',
description: `Performance is affected`,
},
},
docsUrl: 'https://supabase.com/docs/guides/platform/exhaust-cpu',
buttonText: 'Check usage',
metric: 'cpu',
},
memory_and_swap_exhaustion: {
bannerContent: {
warning: {
title:
'Your project is currently facing high memory usage, and its performance is affected',
description:
'You will need to optimize your performance or upgrade your compute. Check the usage page for more recent and detailed statistics.',
},
critical: {
title: "Your project's memory usage is at 100%, and its performance is affected",
description:
'You will need to optimize your performance or upgrade your compute. Check the usage page for more recent and detailed statistics.',
},
},
cardContent: {
warning: {
title: 'Project has high memory usage',
description: `Performance is affected`,
},
critical: {
title: 'Project memory usage is at 100%',
description: `Performance is affected`,
},
},
docsUrl: 'https://supabase.com/docs/guides/platform/exhaust-ram',
buttonText: 'Check usage',
metric: 'ram',
},
multiple_resource_warnings: {
bannerContent: {
warning: {
title:
'Your project is currently exhausting multiple resources, and its performance is affected',
description:
"Check which resources are reaching their threshold on your project's usage page.",
},
critical: {
title: 'Your project has exhausted multiple resources, and its performance is affected',
description:
"Check which resources have reached their threshold on your project's usage page.",
},
},
cardContent: {
warning: {
title: 'Project is exhausting multiple resources',
description: `Performance is affected.`,
},
critical: {
title: 'Project has exhausted multiple resources',
description: `Performance is affected.`,
},
},
docsUrl: undefined,
buttonText: 'Check usage',
metric: null,
},
}
@@ -0,0 +1,137 @@
import { useParams } from 'common'
import Link from 'next/link'
import { useRouter } from 'next/router'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
IconAlertTriangle,
IconExternalLink,
} from 'ui'
import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
import { useSelectedOrganization } from 'hooks'
import { RESOURCE_WARNING_MESSAGES } from './ResourceExhaustionWarningBanner.constants'
import { getWarningContent } from './ResourceExhaustionWarningBanner.utils'
const ResourceExhaustionWarningBanner = () => {
const { ref } = useParams()
const router = useRouter()
const organization = useSelectedOrganization()
const { data: resourceWarnings } = useResourceWarningsQuery()
const projectResourceWarnings = (resourceWarnings ?? [])?.find(
(warning) => warning.project === ref
)
const isOrgBilling = !!organization?.subscription_id
// [Joshen] Read only takes higher precedence over multiple resource warnings
const activeWarnings =
projectResourceWarnings !== undefined
? projectResourceWarnings.is_readonly_mode_enabled
? ['is_readonly_mode_enabled']
: Object.keys(projectResourceWarnings).filter(
(property) =>
property !== 'project' &&
property !== 'is_readonly_mode_enabled' &&
projectResourceWarnings[property as keyof typeof projectResourceWarnings] !== null
)
: []
const hasCriticalWarning =
projectResourceWarnings !== undefined
? activeWarnings.some(
(x) => projectResourceWarnings[x as keyof typeof projectResourceWarnings] === 'critical'
)
: false
const isCritical = activeWarnings.includes('is_readonly_mode_enabled') || hasCriticalWarning
const title =
activeWarnings.length > 1
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.bannerContent[
hasCriticalWarning ? 'critical' : 'warning'
].title
: projectResourceWarnings !== undefined
? getWarningContent(projectResourceWarnings, activeWarnings[0], 'bannerContent')?.title
: null
const description =
activeWarnings.length > 1
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.bannerContent[
hasCriticalWarning ? 'critical' : 'warning'
].description
: projectResourceWarnings !== undefined
? getWarningContent(projectResourceWarnings, activeWarnings[0], 'bannerContent')?.description
: null
const learnMoreUrl =
activeWarnings.length > 1
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.docsUrl
: RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES]
?.docsUrl
const metric =
activeWarnings.length > 1
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.metric
: RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES]
?.metric
const correctionUrl = (
metric === undefined
? undefined
: metric === null
? '/project/[ref]/settings/[infra-path]'
: metric === 'disk_space'
? '/project/[ref]/settings/database'
: `/project/[ref]/settings/[infra-path]#${metric}`
)
?.replace('[ref]', ref ?? 'default')
?.replace('[infra-path]', isOrgBilling ? 'infrastructure' : 'billing/usage')
const buttonText =
activeWarnings.length > 1
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.buttonText
: RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES]
?.buttonText
// Don't show banner if no warnings, or on usage/infra page
if (
activeWarnings.length === 0 ||
((router.pathname.endsWith('/usage') || router.pathname.endsWith('/infrastructure')) &&
!activeWarnings.includes('is_readonly_mode_enabled'))
)
return null
return (
<Alert_Shadcn_
variant={isCritical ? 'destructive' : 'warning'}
className="border-0 border-r-0 rounded-none [&>svg]:left-6 px-6 [&>svg]:w-[26px]
[&>svg]:h-[26px]"
>
<IconAlertTriangle />
<AlertTitle_Shadcn_>{title}</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>{description}</AlertDescription_Shadcn_>
<div className="absolute top-5 right-5 flex items-center space-x-2">
{learnMoreUrl !== undefined && (
<Link passHref href={learnMoreUrl}>
<a>
<Button type="default" icon={<IconExternalLink />}>
Learn more
</Button>
</a>
</Link>
)}
{correctionUrl !== undefined && (
<Link passHref href={correctionUrl}>
<a>
<Button type="default">{buttonText ?? 'Check'}</Button>
</a>
</Link>
)}
</div>
</Alert_Shadcn_>
)
}
export default ResourceExhaustionWarningBanner
@@ -0,0 +1,19 @@
import { ResourceWarning } from 'data/usage/resource-warnings-query'
import { RESOURCE_WARNING_MESSAGES } from './ResourceExhaustionWarningBanner.constants'
export const getWarningContent = (
resourceWarnings: ResourceWarning,
metric: string,
contentType: 'cardContent' | 'bannerContent'
) => {
if (metric === 'is_readonly_mode_enabled') {
return RESOURCE_WARNING_MESSAGES.is_readonly_mode_enabled.cardContent.warning
}
const severity = resourceWarnings[metric as keyof typeof resourceWarnings]
if (typeof severity !== 'string') return undefined
return RESOURCE_WARNING_MESSAGES[metric as keyof typeof RESOURCE_WARNING_MESSAGES][contentType][
severity as 'warning' | 'critical'
]
}
@@ -1,55 +0,0 @@
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_ADMIN_URL } from 'lib/constants'
import { useCallback } from 'react'
import { projectKeys } from './keys'
export type ProjectReadOnlyStatusVariables = { projectRef?: string }
export type ProjectReadOnlyStatus = {
enabled: boolean
override_enabled: boolean
override_active_until: string
}
export async function getProjectReadOnlyStatus(
{ projectRef }: ProjectReadOnlyStatusVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('Project ref is required')
const data = await get(`${API_ADMIN_URL}/projects/${projectRef}/readonly`, { signal })
if (data.error) throw data.error
return data as ProjectReadOnlyStatus
}
export type ProjectReadOnlyStatusData = Awaited<ReturnType<typeof getProjectReadOnlyStatus>>
export type ProjectReadOnlyStatusError = unknown
export const useProjectReadOnlyStatusQuery = <TData = ProjectReadOnlyStatusData>(
{ projectRef }: ProjectReadOnlyStatusVariables,
{
enabled = true,
...options
}: UseQueryOptions<ProjectReadOnlyStatusData, ProjectReadOnlyStatusError, TData> = {}
) =>
useQuery<ProjectReadOnlyStatusData, ProjectReadOnlyStatusError, TData>(
projectKeys.readonlyStatus(projectRef),
({ signal }) => getProjectReadOnlyStatus({ projectRef }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined',
...options,
}
)
export const useProjectReadOnlyStatusPrefetch = ({
projectRef,
}: ProjectReadOnlyStatusVariables) => {
const client = useQueryClient()
return useCallback(() => {
if (projectRef) {
client.prefetchQuery(projectKeys.readonlyStatus(projectRef), ({ signal }) =>
getProjectReadOnlyStatus({ projectRef }, signal)
)
}
}, [projectRef])
}
@@ -1,110 +0,0 @@
import { QueryClient, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { useCallback, useRef } from 'react'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { Project, ResponseError } from 'types'
import { projectKeys } from './keys'
export type ProjectsReadonlyStatusesVariables = {
ref?: string
}
export async function getProjectsReadonlyStatuses(signal?: AbortSignal) {
const data = await get(`${API_URL}/projects?include_readonly_mode_status=true`, { signal })
if (data.error) throw data.error
return Object.fromEntries(
data.map((project: any) => [project.ref, project.is_readonly_mode_enabled])
) as {
[k: string]: boolean
}
}
export type ProjectsReadonlyStatusesData = Awaited<ReturnType<typeof getProjectsReadonlyStatuses>>
export type ProjectsReadonlyStatusesError = ResponseError
export const useProjectsReadonlyStatusesQuery = <TData = ProjectsReadonlyStatusesData>({
enabled = true,
...options
}: UseQueryOptions<ProjectsReadonlyStatusesData, ProjectsReadonlyStatusesError, TData> = {}) =>
useQuery<ProjectsReadonlyStatusesData, ProjectsReadonlyStatusesError, TData>(
projectKeys.readonlyStatusList(),
({ signal }) => getProjectsReadonlyStatuses(signal),
{ enabled: enabled, ...options }
)
export function prefetchProjectsReadonlyStatuses(client: QueryClient) {
return client.prefetchQuery(projectKeys.readonlyStatusList(), ({ signal }) =>
getProjectsReadonlyStatuses(signal)
)
}
export function useProjectsReadonlyStatusesPrefetch() {
const client = useQueryClient()
return useCallback(() => {
prefetchProjectsReadonlyStatuses(client)
}, [client])
}
export function useAutoProjectsReadonlyStatusesPrefetch() {
const prefetch = useProjectsReadonlyStatusesPrefetch()
const called = useRef<boolean>(false)
if (called.current === false) {
called.current = true
prefetch()
}
}
export function invalidateProjectsReadonlyStatusesQuery(client: QueryClient) {
return client.invalidateQueries(projectKeys.readonlyStatusList())
}
export function setProjectStatus(
client: QueryClient,
projectRef: Project['ref'],
status: Project['status']
) {
client.setQueriesData<Project[] | undefined>(
projectKeys.readonlyStatusList(),
(old) => {
if (!old) return old
return old.map((project) => {
if (project.ref === projectRef) {
return { ...project, status }
}
return project
})
},
{ updatedAt: Date.now() }
)
client.setQueriesData<Project>(
projectKeys.detail(projectRef),
(old) => {
if (!old) return old
return { ...old, status }
},
{ updatedAt: Date.now() }
)
}
export function setProjectPostgrestStatus(
client: QueryClient,
projectRef: Project['ref'],
status: Project['postgrestStatus']
) {
client.setQueriesData<Project>(
projectKeys.detail(projectRef),
(old) => {
if (!old) return old
return { ...old, postgrestStatus: status }
},
{ updatedAt: Date.now() }
)
}
+1
View File
@@ -1,4 +1,5 @@
export const usageKeys = {
usage: (projectRef: string | undefined) => ['projects', projectRef, 'usage'] as const,
orgUsage: (orgSlug: string | undefined) => ['organizations', orgSlug, 'usage'] as const,
resourceWarnings: () => ['projects', 'resource-warnings'] as const,
}
@@ -0,0 +1,31 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'data/fetchers'
import { usageKeys } from './keys'
import { ResponseError } from 'types'
import { components } from 'data/api'
export async function getResourceWarnings(signal?: AbortSignal) {
const { data, error } = await get(`/platform/projects-resource-warnings`, { signal })
if (error) throw error
return data
}
export type ResourceWarning = components['schemas']['ProjectResourceWarningsResponse']
export type ResourceWarningsData = Awaited<ReturnType<typeof getResourceWarnings>>
export type ResourceWarningsError = ResponseError
export const useResourceWarningsQuery = <TData = ResourceWarningsData>({
enabled = true,
...options
}: UseQueryOptions<ResourceWarningsData, ResourceWarningsError, TData> = {}) =>
useQuery<ResourceWarningsData, ResourceWarningsError, TData>(
usageKeys.resourceWarnings(),
({ signal }) => getResourceWarnings(signal),
{
enabled,
staleTime: 1000 * 60 * 30, // default 30 minutes
...options,
}
)
@@ -1,9 +0,0 @@
import { useProjectsReadonlyStatusesQuery } from 'data/projects/projects-readonly-status-query'
export function useProjectReadOnlyStatus(projectRef?: string) {
const { data } = useProjectsReadonlyStatusesQuery()
if (!projectRef) return false
return data?.[projectRef] ?? false
}