mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Merge pull request #16612 from supabase/feat/project-exhaustion-warnings
Resource exhaustion notifications and badges
This commit is contained in:
26 files changed
+927
-435
No files matched your search
@@ -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
|
||||
+106
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
+170
@@ -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,
|
||||
},
|
||||
}
|
||||
+137
@@ -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
|
||||
+19
@@ -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,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
|
||||
}
|
||||
Reference in new issue
Block a user