diff --git a/studio/__mocks__/data/fetchers.js b/studio/__mocks__/data/fetchers.js new file mode 100644 index 00000000000..2331502c3d9 --- /dev/null +++ b/studio/__mocks__/data/fetchers.js @@ -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() diff --git a/studio/components/interfaces/BillingV2/Subscription/AddOns/AddOns.tsx b/studio/components/interfaces/BillingV2/Subscription/AddOns/AddOns.tsx index 3d7c6d34e0d..444317ed056 100644 --- a/studio/components/interfaces/BillingV2/Subscription/AddOns/AddOns.tsx +++ b/studio/components/interfaces/BillingV2/Subscription/AddOns/AddOns.tsx @@ -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') diff --git a/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx b/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx index fdb0948c26c..39c3c24459a 100644 --- a/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx +++ b/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx @@ -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 = ({ {attribute.key === 'disk_io_consumption' && ( <> - {hasLatest && latestIoBudgetConsumption >= 100 ? ( - -

- 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. -

- - - - - -
- ) : hasLatest && latestIoBudgetConsumption >= 80 ? ( - -

- 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. -

- - - - - -
- ) : currentBillingCycleSelected && highestIoBudgetConsumption >= 100 ? ( - -

- 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. -

- - - - - -
- ) : currentBillingCycleSelected && highestIoBudgetConsumption >= 80 ? ( - -

- 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. -

- - - - - -
- ) : null} +

Disk IO Bandwidth

@@ -246,6 +182,21 @@ const Infrastructure = ({
)} + {attribute.key === 'max_cpu_usage' && ( + + )} + {attribute.key === 'ram_usage' && ( + + )} +
{attribute.key === 'disk_io_consumption' ? ( diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.tsx b/studio/components/interfaces/BillingV2/Usage/Usage.tsx index d768880161b..3f1d04a8f8d 100644 --- a/studio/components/interfaces/BillingV2/Usage/Usage.tsx +++ b/studio/components/interfaces/BillingV2/Usage/Usage.tsx @@ -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' diff --git a/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/CPUWarnings.tsx b/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/CPUWarnings.tsx new file mode 100644 index 00000000000..8a207e12518 --- /dev/null +++ b/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/CPUWarnings.tsx @@ -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 ( + + + Your max CPU usage has exceeded 80% + + 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. + + + + ) + } + + if (severity === 'critical') { + return ( + + + Your max CPU usage has reached 100% + + 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. + + + + ) + } + + return null +} + +export default CPUWarnings diff --git a/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx b/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx new file mode 100644 index 00000000000..6d257ac3f10 --- /dev/null +++ b/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings.tsx @@ -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 ( + +

+ 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. +

+ + + + + +
+ ) + } + + if (hasLatest && latestIoBudgetConsumption >= 80) { + return ( + +

+ 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. +

+ + + + + +
+ ) + } + + if (currentBillingCycleSelected && highestIoBudgetConsumption >= 100) { + return ( + +

+ 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. +

+ + + + + +
+ ) + } + + if (currentBillingCycleSelected && highestIoBudgetConsumption >= 80) { + return ( + +

+ 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. +

+ + + + + +
+ ) + } + + return null +} + +export default DiskIOBandwidthWarnings diff --git a/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/RAMWarnings.tsx b/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/RAMWarnings.tsx new file mode 100644 index 00000000000..fe233f08695 --- /dev/null +++ b/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/RAMWarnings.tsx @@ -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 ( + + + Your memory usage has exceeded 80% + + 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. + + + + ) + } + + if (severity === 'critical') { + return ( + + + Your memory usage has reached 100% + + 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. + + + + ) + } + + return null +} + +export default RAMWarnings diff --git a/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/index.ts b/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/index.ts new file mode 100644 index 00000000000..3b3afe3f30e --- /dev/null +++ b/studio/components/interfaces/BillingV2/Usage/UsageWarningAlerts/index.ts @@ -0,0 +1,5 @@ +import CPUWarnings from './CPUWarnings' +import DiskIOBandwidthWarnings from './DiskIOBandwidthWarnings' +import RAMWarnings from './RAMWarnings' + +export { CPUWarnings, DiskIOBandwidthWarnings, RAMWarnings } diff --git a/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx b/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx index 28719add668..a97bc84152e 100644 --- a/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx +++ b/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx @@ -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 (
  • -

    {name}

    +
    +

    {name}

    + {desc}
    {isVercelIntegrated && (
    @@ -77,55 +68,16 @@ const ProjectCard = ({
    } footer={ -
    - {desc} - - {isHealthy && isReadonly && ( -
    - -
    - - Read-only mode -
    -
    -
    - )} - - {isRestoring && ( -
    - -
    - - Restoring -
    -
    -
    - )} - - {isPausing && ( -
    - -
    - - Pausing -
    -
    -
    - )} - - {isPaused && ( -
    - -
    - - Paused -
    -
    -
    +
    + {resourceWarnings && ( + )}
    } - /> + >
  • ) } diff --git a/studio/components/interfaces/Home/ProjectList/ProjectCard.utils.ts b/studio/components/interfaces/Home/ProjectList/ProjectCard.utils.ts new file mode 100644 index 00000000000..ea1ab6b0872 --- /dev/null +++ b/studio/components/interfaces/Home/ProjectList/ProjectCard.utils.ts @@ -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 diff --git a/studio/components/interfaces/Home/ProjectList/ProjectCardStatus.tsx b/studio/components/interfaces/Home/ProjectList/ProjectCardStatus.tsx new file mode 100644 index 00000000000..bc889b3c721 --- /dev/null +++ b/studio/components/interfaces/Home/ProjectList/ProjectCardStatus.tsx @@ -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
    + + return ( +
    + + {projectStatus === 'isPaused' || projectStatus === 'isPausing' ? ( + + ) : projectStatus === 'isRestoring' || projectStatus === 'isComingUp' ? ( + + ) : ( + + )} + {alertTitle} + {alertDescription} + +
    + ) +} diff --git a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx index 2ba1bdf6de7..ab938e7de38 100644 --- a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx +++ b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx @@ -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 + )} /> )) )} diff --git a/studio/components/interfaces/Settings/Addons/Addons.tsx b/studio/components/interfaces/Settings/Addons/Addons.tsx index 88035a87b70..edff68aac31 100644 --- a/studio/components/interfaces/Settings/Addons/Addons.tsx +++ b/studio/components/interfaces/Settings/Addons/Addons.tsx @@ -134,14 +134,14 @@ const Addons = () => {
    -

    Optimized compute

    +

    Optimized compute

    -

    More information

    +

    More information

    -

    About compute add-ons

    +

    About compute add-ons

    @@ -151,7 +151,7 @@ const Addons = () => {
    -

    Connection Pooler

    +

    Connection Pooler

    @@ -339,14 +339,14 @@ const Addons = () => {
    -

    Point in time recovery

    +

    Point in time recovery

    -

    More information

    +

    More information

    -

    About PITR backups

    +

    About PITR backups

    @@ -403,14 +403,14 @@ const Addons = () => {
    -

    Custom domain

    +

    Custom domain

    -

    More information

    +

    More information

    -

    About custom domains

    +

    About custom domains

    diff --git a/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx b/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx index caacdeee269..9a077356b21 100644 --- a/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx +++ b/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx @@ -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() - 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 = () => {
    -

    {attribute.name}

    +

    {attribute.name}

    {attribute.description.split('\n').map((value, idx) => ( -

    +

    {value}

    ))} @@ -199,7 +233,7 @@ const InfrastructureActivity = () => {
    -

    {link.name}

    +

    {link.name}

    @@ -212,46 +246,14 @@ const InfrastructureActivity = () => { {attribute.key === 'disk_io_consumption' && ( <> - {currentBillingCycleSelected && highestIoBudgetConsumption >= 100 ? ( - -

    - 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. -

    - - - - - -
    - ) : currentBillingCycleSelected && highestIoBudgetConsumption >= 80 ? ( - -

    - 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. -

    - - - - - -
    - ) : null} +

    Disk IO Bandwidth

    @@ -302,6 +304,21 @@ const InfrastructureActivity = () => {
    )} + {attribute.key === 'max_cpu_usage' && ( + + )} + {attribute.key === 'ram_usage' && ( + + )} +
    {attribute.key === 'disk_io_consumption' ? ( diff --git a/studio/components/layouts/ProjectLayout/BuildingState.tsx b/studio/components/layouts/ProjectLayout/BuildingState.tsx index 42418ba43ab..733d04fbe93 100644 --- a/studio/components/layouts/ProjectLayout/BuildingState.tsx +++ b/studio/components/layouts/ProjectLayout/BuildingState.tsx @@ -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() @@ -48,6 +45,8 @@ const BuildingState = ({ project }: BuildingStateProps) => { } }, []) + if (project === undefined) return null + return (
    diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index 3d9ae98cdea..065d6f80e93 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -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 */} - {/* [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 && ( - - )} - {showOverUsageBadge && (
    {/* Left-most navigation side bar to access products */} {!hideIconBar && } - {/* Product menu bar */} {!showPausedState && ( {productMenu} )} -
    {!navLayoutV2 && !hideHeader && IS_PLATFORM && } {showPausedState ? ( @@ -113,7 +113,10 @@ const ProjectLayout = ({
    ) : ( - {children} + + {showResourceExhaustionWarnings && } + {children} + )}
    @@ -194,7 +197,7 @@ const ContentWrapper = ({ isLoading, children }: ContentWrapperProps) => { ) : requiresDbConnection && isProjectRestoring ? ( ) : requiresDbConnection && isProjectBuilding ? ( - + ) : ( {children} )} @@ -224,6 +227,7 @@ export const ProjectLayoutNonBlocking = ({ const showPausedState = isPaused && !ignorePausedState const navLayoutV2 = useFlag('navigationLayoutV2') + const showResourceExhaustionWarnings = useFlag('resourceExhaustionWarnings') return ( @@ -251,7 +255,10 @@ export const ProjectLayoutNonBlocking = ({
    ) : ( - children + <> + {showResourceExhaustionWarnings && } + {children} + )}
    diff --git a/studio/components/ui/CardButton.tsx b/studio/components/ui/CardButton.tsx index f0696bafe7b..9c0690af6ac 100644 --- a/studio/components/ui/CardButton.tsx +++ b/studio/components/ui/CardButton.tsx @@ -27,7 +27,7 @@ const CardButton = ({ imgAlt, onClick, icon, - containerHeightClassName = 'h-32', + containerHeightClassName = '', loading = false, }: PropsWithChildren) => { 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, ] diff --git a/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts b/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts new file mode 100644 index 00000000000..5ed9d61b5f9 --- /dev/null +++ b/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts @@ -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, + }, +} diff --git a/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx b/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx new file mode 100644 index 00000000000..37df8e4bb7a --- /dev/null +++ b/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx @@ -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 ( + + + {title} + {description} +
    + {learnMoreUrl !== undefined && ( + + + + + + )} + {correctionUrl !== undefined && ( + + + + + + )} +
    +
    + ) +} + +export default ResourceExhaustionWarningBanner diff --git a/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils.ts b/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils.ts new file mode 100644 index 00000000000..0b3dd8b77e7 --- /dev/null +++ b/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils.ts @@ -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' + ] +} diff --git a/studio/data/projects/project-readonly-status-query.ts b/studio/data/projects/project-readonly-status-query.ts deleted file mode 100644 index bcd6274f02c..00000000000 --- a/studio/data/projects/project-readonly-status-query.ts +++ /dev/null @@ -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> -export type ProjectReadOnlyStatusError = unknown - -export const useProjectReadOnlyStatusQuery = ( - { projectRef }: ProjectReadOnlyStatusVariables, - { - enabled = true, - ...options - }: UseQueryOptions = {} -) => - useQuery( - 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]) -} diff --git a/studio/data/projects/projects-readonly-status-query.ts b/studio/data/projects/projects-readonly-status-query.ts deleted file mode 100644 index 0cae487cb70..00000000000 --- a/studio/data/projects/projects-readonly-status-query.ts +++ /dev/null @@ -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> -export type ProjectsReadonlyStatusesError = ResponseError - -export const useProjectsReadonlyStatusesQuery = ({ - enabled = true, - ...options -}: UseQueryOptions = {}) => - useQuery( - 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(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( - 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( - 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( - projectKeys.detail(projectRef), - (old) => { - if (!old) return old - - return { ...old, postgrestStatus: status } - }, - { updatedAt: Date.now() } - ) -} diff --git a/studio/data/usage/keys.ts b/studio/data/usage/keys.ts index aac6590a78e..54be56a1858 100644 --- a/studio/data/usage/keys.ts +++ b/studio/data/usage/keys.ts @@ -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, } diff --git a/studio/data/usage/resource-warnings-query.ts b/studio/data/usage/resource-warnings-query.ts new file mode 100644 index 00000000000..b1d56c12cfe --- /dev/null +++ b/studio/data/usage/resource-warnings-query.ts @@ -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> +export type ResourceWarningsError = ResponseError + +export const useResourceWarningsQuery = ({ + enabled = true, + ...options +}: UseQueryOptions = {}) => + useQuery( + usageKeys.resourceWarnings(), + ({ signal }) => getResourceWarnings(signal), + { + enabled, + staleTime: 1000 * 60 * 30, // default 30 minutes + ...options, + } + ) diff --git a/studio/hooks/misc/useProjectReadOnlyStatus.ts b/studio/hooks/misc/useProjectReadOnlyStatus.ts deleted file mode 100644 index 4c889b54528..00000000000 --- a/studio/hooks/misc/useProjectReadOnlyStatus.ts +++ /dev/null @@ -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 -}