mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Merge branch 'master' into docs/updates_on_a_plane
This commit is contained in:
55 files changed
+1613
-648
No files matched your search
@@ -1,3 +1,6 @@
|
||||
/packages/shared-data/pricing.ts @roryw10 @kevcodez
|
||||
/packages/shared-data/plans.ts @roryw10 @kevcodez
|
||||
|
||||
/studio/ @supabase/Dashboard
|
||||
/apps/www/ @supabase/Website
|
||||
/apps/docs/ @supabase/docs
|
||||
|
||||
@@ -8,7 +8,6 @@ import Head from 'next/head'
|
||||
import { PropsWithChildren, memo } from 'react'
|
||||
import Footer from '~/components/Navigation/Footer'
|
||||
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
|
||||
import { Announcement, LW8CountdownBanner } from 'ui'
|
||||
|
||||
const levelsData = {
|
||||
home: {
|
||||
@@ -327,9 +326,6 @@ const SiteLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
<title>Supabase Docs</title>
|
||||
</Head>
|
||||
<main>
|
||||
<Announcement>
|
||||
<LW8CountdownBanner />
|
||||
</Announcement>
|
||||
<div className="flex flex-row h-screen">
|
||||
<NavContainer />
|
||||
<Container>
|
||||
|
||||
@@ -3,10 +3,17 @@ import Link from 'next/link'
|
||||
import { Badge } from 'ui'
|
||||
import { ArrowNarrowRightIcon } from '@heroicons/react/outline'
|
||||
|
||||
const AnnouncementBadge = () => (
|
||||
interface Props {
|
||||
url: string
|
||||
announcement: string
|
||||
badge?: string
|
||||
target?: '_self' | '_blank'
|
||||
}
|
||||
const AnnouncementBadge = ({ url, announcement, badge, target = '_self' }: Props) => (
|
||||
<div className="w-full max-w-xl flex justify-center opacity-0 !animate-[fadeIn_0.5s_cubic-bezier(0.25,0.25,0,1)_0.5s_both]">
|
||||
<Link href="/launch-week">
|
||||
<Link href={url}>
|
||||
<a
|
||||
target={target}
|
||||
className="
|
||||
group
|
||||
relative
|
||||
@@ -26,10 +33,12 @@ const AnnouncementBadge = () => (
|
||||
focus:outline-none focus:ring-brand-600 focus:ring-2 focus:rounded-full
|
||||
"
|
||||
>
|
||||
<Badge color="brand" size="large" className="py-1">
|
||||
Explore
|
||||
</Badge>
|
||||
<span className="text-foreground">Launch Week 8 announcements</span>
|
||||
{badge && (
|
||||
<Badge color="brand" size="large" className="py-1">
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-foreground">{announcement}</span>
|
||||
<ArrowNarrowRightIcon className="h-4 ml-2 -translate-x-1 transition-transform group-hover:translate-x-0" />
|
||||
<div
|
||||
className="absolute inset-0 -z-10 bg-gradient-to-br
|
||||
|
||||
@@ -8,7 +8,7 @@ import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import HeroFrameworks from './HeroFrameworks'
|
||||
import styles from './hero.module.css'
|
||||
import Image from 'next/image'
|
||||
import AnnouncementBadge from '../Announcement/Badge'
|
||||
// import AnnouncementBadge from '../Announcement/Badge'
|
||||
|
||||
const Hero = () => {
|
||||
const router = useRouter()
|
||||
@@ -19,7 +19,7 @@ const Hero = () => {
|
||||
|
||||
return (
|
||||
<div className="relative -mt-[65px]">
|
||||
<SectionContainer className="pt-8 md:pt-16 lg:pt-24 overflow-hidden">
|
||||
<SectionContainer className="pt-8 md:pt-16 overflow-hidden">
|
||||
<div className="relative">
|
||||
<div className="mx-auto">
|
||||
<div className="mx-auto max-w-2xl lg:col-span-6 lg:flex lg:items-center justify-center text-center">
|
||||
@@ -30,9 +30,9 @@ const Hero = () => {
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="z-40 w-full flex justify-center mb-8 lg:mb-8">
|
||||
{/* <div className="z-40 w-full flex justify-center mb-8 lg:mb-8">
|
||||
<AnnouncementBadge />
|
||||
</div>
|
||||
</div> */}
|
||||
<h1 className="text-scale-1200 text-4xl sm:text-5xl sm:leading-none lg:text-7xl">
|
||||
<span className="block text-[#F4FFFA00] bg-clip-text bg-gradient-to-b from-scale-1200 to-scale-1200 dark:to-scale-1100">
|
||||
Build in a weekend
|
||||
@@ -80,32 +80,7 @@ const Hero = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -top-2 md:top-0 -left-10 -right-10 md:left-0 md:right-0 h-[500px] lg:h-[550px] z-0 flex items-center justify-center">
|
||||
<span className="absolute inset-0">
|
||||
<Image
|
||||
src="/images/launchweek/8/stars.svg"
|
||||
alt="stars background"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="opacity-70"
|
||||
draggable={false}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
<div className="absolute w-full max-w-[1600px] mx-auto h-[500px] lg:h-[750px] inset-0 z-0 flex items-center justify-center">
|
||||
<Image
|
||||
src="/images/launchweek/8/LW8-gradient.png"
|
||||
alt="Launch Week 8 purple background gradient"
|
||||
className="not-sr-only"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
objectPosition="top"
|
||||
priority
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 right-0 h-screen bg-gradient-to-b from-[#020405] to-transparent -z-10" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ const Developers = () => {
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" strokeLinejoin="round" strokeWidth="1.5" d={icon} />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d={icon} />
|
||||
</svg>
|
||||
)}
|
||||
{Svg && <Svg />}
|
||||
|
||||
@@ -2,8 +2,7 @@ import React, { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import Announcement from '~/components/Announcement/Announcement'
|
||||
import { Button, Badge, IconStar, IconChevronDown, LW8CountdownBanner } from 'ui'
|
||||
import { Button, Badge, IconStar, IconChevronDown } from 'ui'
|
||||
import FlyOut from '~/components/UI/FlyOut'
|
||||
import Transition from 'lib/Transition'
|
||||
|
||||
@@ -198,9 +197,6 @@ const Nav = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Announcement link="/launch-week">
|
||||
<LW8CountdownBanner />
|
||||
</Announcement>
|
||||
<div className="sticky top-0 z-40 transform" style={{ transform: 'translate3d(0,0,999px)' }}>
|
||||
<div
|
||||
className={[
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
[
|
||||
{
|
||||
"type": "Announcement",
|
||||
"title": "Supabase Beta August 2023",
|
||||
"description": "Launch Week 8 review and more things we shipped 🚀",
|
||||
"imgUrl": "/images/blog/2023-09-07-beta-update-august-2023/monthly-update-august-2023.jpg",
|
||||
"logoUrl": "/images/blog/2023-09-07-beta-update-august-2023/monthly-update-august-2023.jpg",
|
||||
"organization": "Supabase",
|
||||
"url": "/blog/beta-update-august-2023",
|
||||
"postMeta": {
|
||||
"name": "Ant Wilson",
|
||||
"publishDate": "Sep 08, 2023",
|
||||
"readLength": 5
|
||||
},
|
||||
"ctaText": "Learn more"
|
||||
},
|
||||
{
|
||||
"type": "Announcement",
|
||||
"title": "Launch Week 8 Hackathon Winners",
|
||||
@@ -13,20 +28,5 @@
|
||||
"readLength": 3
|
||||
},
|
||||
"ctaText": "See the winners"
|
||||
},
|
||||
{
|
||||
"type": "Announcement",
|
||||
"title": "Supabase Launch Week 8",
|
||||
"description": "One new feature (or more) every day during a week.",
|
||||
"imgUrl": "/images/launchweek/8/lw8-og.jpg",
|
||||
"logoUrl": "/images/launchweek/8/lw8-og.jpg",
|
||||
"organization": "Supabase",
|
||||
"url": "/launch-week",
|
||||
"postMeta": {
|
||||
"name": "Paul Copplestone",
|
||||
"publishDate": "Aug 07, 2023",
|
||||
"readLength": 5
|
||||
},
|
||||
"ctaText": "Learn more"
|
||||
}
|
||||
]
|
||||
@@ -51,8 +51,8 @@ const Partners = () => {
|
||||
<path
|
||||
d="M91 1C120.005 1 145.841 14.5702 162.505 35.708"
|
||||
stroke="url(#paint0_linear_4766_6117)"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
@@ -63,9 +63,9 @@ const Partners = () => {
|
||||
y2="21.5"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#1CF7C3" stop-opacity="0" />
|
||||
<stop offset="0.510417" stop-color="#1CF7C3" />
|
||||
<stop offset="1" stop-color="#1CF7C3" stop-opacity="0" />
|
||||
<stop stopColor="#1CF7C3" stopOpacity="0" />
|
||||
<stop offset="0.510417" stopColor="#1CF7C3" />
|
||||
<stop offset="1" stopColor="#1CF7C3" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useTheme } from 'common/Providers'
|
||||
import ComputePricingModal from '~/components/Pricing/ComputePricingModal'
|
||||
import { plans } from 'shared-data/plans'
|
||||
import { ArrowNarrowRightIcon } from '@heroicons/react/outline'
|
||||
import AnnouncementBadge from '../../components/Announcement/Badge'
|
||||
|
||||
export default function IndexPage() {
|
||||
const router = useRouter()
|
||||
@@ -151,7 +152,7 @@ export default function IndexPage() {
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className="relative z-10 py-16 lg:py-28">
|
||||
<div className="relative z-10 py-16 lg:py-20">
|
||||
<div className="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-3xl space-y-2 lg:max-w-none">
|
||||
<h1 className="text-brand text-base">Pricing</h1>
|
||||
@@ -159,45 +160,13 @@ export default function IndexPage() {
|
||||
<p className="p text-lg">
|
||||
Start building for free, collaborate with a team, then scale to millions of users.
|
||||
</p>
|
||||
<div className="w-full flex justify-center items-center opacity-0 !animate-[fadeIn_0.5s_cubic-bezier(0.25,0.25,0,1)_0.5s_both]">
|
||||
<Link href="/blog/organization-based-billing" passHref>
|
||||
<a
|
||||
target="_blank"
|
||||
className="
|
||||
group
|
||||
relative
|
||||
flex flex-row
|
||||
items-center
|
||||
pr-3 p-1
|
||||
text-sm
|
||||
w-auto
|
||||
gap-2
|
||||
text-left
|
||||
rounded-full
|
||||
bg-opacity-20
|
||||
border
|
||||
border-background-surface-100
|
||||
hover:border-background-surface-300
|
||||
overflow-hidden
|
||||
focus:outline-none focus:ring-brand-600 focus:ring-2 focus:rounded-full
|
||||
"
|
||||
>
|
||||
<Badge color="brand" size="large" className="py-1">
|
||||
Update
|
||||
</Badge>
|
||||
<span className="text-foreground">Changes to how we bill</span>
|
||||
<ArrowNarrowRightIcon className="h-4 ml-2 -translate-x-1 transition-transform group-hover:translate-x-0" />
|
||||
<div
|
||||
className="absolute inset-0 -z-10 bg-gradient-to-br
|
||||
opacity-70
|
||||
overflow-hidden rounded-full
|
||||
from-background-surface-100
|
||||
to-background-surface-300
|
||||
backdrop-blur-md
|
||||
"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="w-full inline-flex justify-center items-center pt-3 pb-6">
|
||||
<AnnouncementBadge
|
||||
url="/blog/organization-based-billing"
|
||||
badge="Update"
|
||||
announcement="Changes to how we bill"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Generated
+8
-8
@@ -14121,17 +14121,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/gotrue-js": {
|
||||
"version": "2.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.48.0.tgz",
|
||||
"integrity": "sha512-+6g5aFL6+iaZC6mFVpRLauSkU5MrQ52QZA1TlY0/evvz9Rmhqc315tIeUfUK87ZLeX7sOL6XYVGkI6ZxgeyAUA==",
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.52.0.tgz",
|
||||
"integrity": "sha512-UFCbydMYFn/LhPW08aeZ9sgFDA3kOQCjA2ieFj/sccye9m8Tv0pimMfnXh3A9TqBJ0/0utpkaGSad3XdpJ+Mbw==",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.13"
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/node-fetch": {
|
||||
"version": "2.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.13.tgz",
|
||||
"integrity": "sha512-rEHQaDVzxLZMCK3p+JW2nzEsK4AJpOQhetppaqAzrFum0Ub8wcnoM/8f1dWRZSulY5fRDP6rJaWT/8X3VleCzg==",
|
||||
"version": "2.6.14",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.14.tgz",
|
||||
"integrity": "sha512-w/Tsd22e/5fAeoxqQ4P2MX6EyF+iM6rc9kmlMVFkHuG0rAltt2TLhFbDJfemnHbtvnazWaRfy5KnFU/SYT37dQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
@@ -40754,7 +40754,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.14",
|
||||
"@supabase/gotrue-js": "^2.48.0",
|
||||
"@supabase/gotrue-js": "^2.52.0",
|
||||
"@supabase/ui": "^0.37.0-alpha.50",
|
||||
"@types/react": "^17.0.39",
|
||||
"react-use": "^17.4.0"
|
||||
|
||||
@@ -3,13 +3,20 @@ import { GoTrueClient, navigatorLock } from '@supabase/gotrue-js'
|
||||
export const STORAGE_KEY = process.env.NEXT_PUBLIC_STORAGE_KEY || 'supabase.dashboard.auth.token'
|
||||
export const AUTH_DEBUG_KEY =
|
||||
process.env.NEXT_PUBLIC_AUTH_DEBUG_KEY || 'supabase.dashboard.auth.debug'
|
||||
export const AUTH_DEBUG_PERSISTED_KEY =
|
||||
process.env.NEXT_PUBLIC_AUTH_DEBUG_PERSISTED_KEY || 'supabase.dashboard.auth.debug.persist'
|
||||
export const AUTH_NAVIGATOR_LOCK_DISABLED_KEY =
|
||||
process.env.NEXT_PUBLIC_AUTH_NAVIGATOR_LOCK_KEY || 'supabase.dashboard.auth.navigatorLock.disabled'
|
||||
process.env.NEXT_PUBLIC_AUTH_NAVIGATOR_LOCK_KEY ||
|
||||
'supabase.dashboard.auth.navigatorLock.disabled'
|
||||
|
||||
const debug =
|
||||
process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' &&
|
||||
globalThis?.localStorage?.getItem(AUTH_DEBUG_KEY) === 'true'
|
||||
|
||||
const persistedDebug =
|
||||
process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' &&
|
||||
globalThis?.localStorage?.getItem(AUTH_DEBUG_PERSISTED_KEY) === 'true'
|
||||
|
||||
const navigatorLockEnabled = !!(
|
||||
process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' &&
|
||||
!(globalThis?.localStorage?.getItem(AUTH_NAVIGATOR_LOCK_DISABLED_KEY) === 'true') &&
|
||||
@@ -20,10 +27,83 @@ if (!globalThis?.navigator?.locks) {
|
||||
console.warn('This browser does not support the Navigator Locks API. Please update it.')
|
||||
}
|
||||
|
||||
const tabId = Math.random().toString(16).substring(2)
|
||||
|
||||
let dbHandle = new Promise<IDBDatabase | null>((accept, _) => {
|
||||
if (!persistedDebug) {
|
||||
accept(null)
|
||||
return
|
||||
}
|
||||
|
||||
const request = indexedDB.open('auth-debug-log', 1)
|
||||
|
||||
request.onupgradeneeded = (event: any) => {
|
||||
const db = event?.target?.result
|
||||
|
||||
if (!db) {
|
||||
return
|
||||
}
|
||||
|
||||
db.createObjectStore('events', { autoIncrement: true })
|
||||
}
|
||||
|
||||
request.onsuccess = (event: any) => {
|
||||
console.log('Opened persisted auth debug log IndexedDB database', tabId)
|
||||
accept(event.target.result)
|
||||
}
|
||||
|
||||
request.onerror = (event: any) => {
|
||||
console.error('Failed to open persisted auth debug log IndexedDB database', event)
|
||||
accept(null)
|
||||
}
|
||||
})
|
||||
|
||||
const logIndexedDB = (message: string, ...args: any[]) => {
|
||||
console.log(message, ...args)
|
||||
|
||||
const copyArgs = structuredClone(args)
|
||||
|
||||
copyArgs.forEach((value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
delete value.user
|
||||
delete value.access_token
|
||||
delete value.token_type
|
||||
delete value.provider_token
|
||||
}
|
||||
})
|
||||
;(async () => {
|
||||
try {
|
||||
const db = await dbHandle
|
||||
|
||||
if (!db) {
|
||||
return
|
||||
}
|
||||
|
||||
const tx = db.transaction(['events'], 'readwrite')
|
||||
tx.onerror = (event: any) => {
|
||||
console.error('Failed to write to persisted auth debug log IndexedDB database', event)
|
||||
dbHandle = Promise.resolve(null)
|
||||
}
|
||||
|
||||
const events = tx.objectStore('events')
|
||||
|
||||
events.add({
|
||||
m: message.replace(/^GoTrueClient@/i, ''),
|
||||
a: copyArgs,
|
||||
l: window.location.pathname,
|
||||
t: tabId,
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.error('Failed to log to persisted auth debug log IndexedDB database', e)
|
||||
dbHandle = Promise.resolve(null)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
export const gotrueClient = new GoTrueClient({
|
||||
url: process.env.NEXT_PUBLIC_GOTRUE_URL,
|
||||
storageKey: STORAGE_KEY,
|
||||
detectSessionInUrl: true,
|
||||
debug,
|
||||
debug: debug ? (persistedDebug ? logIndexedDB : true) : false,
|
||||
lock: navigatorLockEnabled ? navigatorLock : undefined,
|
||||
})
|
||||
@@ -6,7 +6,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.14",
|
||||
"@supabase/gotrue-js": "^2.48.0",
|
||||
"@supabase/gotrue-js": "^2.52.0",
|
||||
"@supabase/ui": "^0.37.0-alpha.50",
|
||||
"@types/react": "^17.0.39",
|
||||
"react-use": "^17.4.0"
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
+12
-1
@@ -14,8 +14,10 @@ import { Alert, Button } from 'ui'
|
||||
import { BILLING_BREAKDOWN_METRICS } from './BillingBreakdown.constants'
|
||||
import BillingMetric from './BillingMetric'
|
||||
import UpcomingInvoice from './UpcomingInvoice'
|
||||
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
|
||||
|
||||
const BillingBreakdown = () => {
|
||||
const snap = useOrgSettingsPageStateSnapshot()
|
||||
const { slug: orgSlug } = useParams()
|
||||
const {
|
||||
data: usage,
|
||||
@@ -93,7 +95,16 @@ const BillingBreakdown = () => {
|
||||
variant="danger"
|
||||
title="Your organization's usage has exceeded its included quota"
|
||||
actions={[
|
||||
<Button key="upgrade-button" type="default" className="ml-8" onClick={() => {}}>
|
||||
<Button
|
||||
key="upgrade-button"
|
||||
type="default"
|
||||
className="ml-8"
|
||||
onClick={() =>
|
||||
snap.setPanelKey(
|
||||
currentPlan?.id === 'free' ? 'subscriptionPlan' : 'costControl'
|
||||
)
|
||||
}
|
||||
>
|
||||
{currentPlan?.id === 'free' ? 'Upgrade plan' : 'Change spend cap'}
|
||||
</Button>,
|
||||
]}
|
||||
|
||||
@@ -11,12 +11,13 @@ import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants'
|
||||
import { Button, IconExternalLink, IconInfo, Listbox } from 'ui'
|
||||
import { Alert, Button, IconExternalLink, IconInfo, Listbox } from 'ui'
|
||||
import Activity from './Activity'
|
||||
import Bandwidth from './Bandwidth'
|
||||
import SizeAndCounts from './SizeAndCounts'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import Link from 'next/link'
|
||||
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
|
||||
|
||||
const Usage = () => {
|
||||
const { slug, projectRef } = useParams()
|
||||
@@ -32,6 +33,9 @@ const Usage = () => {
|
||||
isError: isErrorSubscription,
|
||||
isSuccess: isSuccessSubscription,
|
||||
} = useOrgSubscriptionQuery({ orgSlug: slug })
|
||||
|
||||
const { data: usage } = useOrgUsageQuery({ orgSlug: slug })
|
||||
|
||||
const orgProjects = projects?.filter((project) => project.organization_id === organization?.id)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -88,6 +92,13 @@ const Usage = () => {
|
||||
? orgProjects?.find((it) => it.ref === selectedProjectRef)
|
||||
: undefined
|
||||
|
||||
const hasExceededAnyLimits = Boolean(
|
||||
usage?.usages.find(
|
||||
(metric) =>
|
||||
!metric.unlimited && metric.capped && metric.usage > (metric?.pricing_free_units ?? 0)
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer className="sticky top-0 border-b bg-scale-200 z-10 overflow-hidden">
|
||||
@@ -156,6 +167,33 @@ const Usage = () => {
|
||||
</div>
|
||||
</ScaffoldContainer>
|
||||
|
||||
{!selectedProject && subscription && hasExceededAnyLimits && (
|
||||
<ScaffoldContainer className="mt-5">
|
||||
<Alert
|
||||
withIcon
|
||||
variant="danger"
|
||||
title="Your organization's usage has exceeded its included quota"
|
||||
actions={[
|
||||
<Link
|
||||
key="upgrade-button"
|
||||
href={`/org/${slug}/billing?panel=${
|
||||
subscription.plan.id === 'free' ? 'subscriptionPlan' : 'costControl'
|
||||
}`}
|
||||
>
|
||||
<Button asChild key="upgrade-button" type="default" className="ml-8">
|
||||
<a>{subscription.plan.id === 'free' ? 'Upgrade plan' : 'Change spend cap'}</a>
|
||||
</Button>
|
||||
</Link>,
|
||||
]}
|
||||
>
|
||||
Your projects can become unresponsive or enter read only mode.{' '}
|
||||
{subscription.plan.id === 'free'
|
||||
? 'Please upgrade to the Pro plan to ensure that your projects remain available.'
|
||||
: 'Please disable spend cap to ensure that your projects remain available.'}
|
||||
</Alert>
|
||||
</ScaffoldContainer>
|
||||
)}
|
||||
|
||||
{selectedProjectRef && (
|
||||
<ScaffoldContainer className="mt-5">
|
||||
<InformationBox
|
||||
|
||||
@@ -228,6 +228,52 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
title: '',
|
||||
queries: {},
|
||||
},
|
||||
[Presets.STORAGE]: {
|
||||
title: 'Storage',
|
||||
queries: {
|
||||
cacheHitRate: {
|
||||
queryType: 'logs',
|
||||
// storage report does not perform any filtering
|
||||
sql: (_filters) => `
|
||||
-- cache-hit-rate
|
||||
SELECT
|
||||
timestamp_trunc(timestamp, hour) as timestamp,
|
||||
countif( h.cf_cache_status in ('HIT', 'STALE', 'REVALIDATED', 'UPDATING') ) as hit_count,
|
||||
countif( h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC') ) as miss_count
|
||||
from edge_logs f
|
||||
cross join unnest(f.metadata) as m
|
||||
cross join unnest(m.request) as r
|
||||
cross join unnest(m.response) as res
|
||||
cross join unnest(res.headers) as h
|
||||
where starts_with(r.path, '/storage/v1/object') and r.method = 'GET'
|
||||
group by timestamp
|
||||
order by timestamp desc
|
||||
`,
|
||||
},
|
||||
topCacheMisses: {
|
||||
queryType: 'logs',
|
||||
// storage report does not perform any filtering
|
||||
sql: (_filters) => `
|
||||
-- top-cache-misses
|
||||
SELECT
|
||||
r.path as path,
|
||||
r.search as search,
|
||||
count(id) as count
|
||||
from edge_logs f
|
||||
cross join unnest(f.metadata) as m
|
||||
cross join unnest(m.request) as r
|
||||
cross join unnest(m.response) as res
|
||||
cross join unnest(res.headers) as h
|
||||
where starts_with(r.path, '/storage/v1/object')
|
||||
and r.method = 'GET'
|
||||
and h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC')
|
||||
group by path, search
|
||||
order by count desc
|
||||
limit 12
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
[Presets.QUERY_PERFORMANCE]: {
|
||||
title: 'Query performance',
|
||||
queries: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DEFAULT_QUERY_PARAMS } from './Reports.constants'
|
||||
|
||||
export enum Presets {
|
||||
API = 'api',
|
||||
STORAGE = 'storage',
|
||||
AUTH = 'auth',
|
||||
QUERY_PERFORMANCE = 'query_performance',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { TextFormatter } from 'components/interfaces/Settings/Logs/LogsFormatters'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import StackedBarChart from 'components/ui/Charts/StackedBarChart'
|
||||
import { Fragment } from 'react'
|
||||
import { ReportWidgetProps, ReportWidgetRendererProps } from '../ReportWidget'
|
||||
|
||||
export const CacheHitRateChartRenderer = (
|
||||
props: ReportWidgetProps<{
|
||||
timestamp: string
|
||||
hit_count: number
|
||||
miss_count: number
|
||||
}>
|
||||
) => {
|
||||
const stackedData = props.data.flatMap((datum) => [
|
||||
{
|
||||
timestamp: datum.timestamp,
|
||||
count: datum.hit_count,
|
||||
type: 'hit',
|
||||
},
|
||||
{
|
||||
timestamp: datum.timestamp,
|
||||
count: datum.miss_count,
|
||||
type: 'miss',
|
||||
},
|
||||
])
|
||||
|
||||
return (
|
||||
<StackedBarChart
|
||||
hideHeader
|
||||
variant="percentages"
|
||||
data={stackedData}
|
||||
xAxisKey="timestamp"
|
||||
yAxisKey="count"
|
||||
stackKey="type"
|
||||
stackColors={['brand', 'amber']}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const TopCacheMissesRenderer = (
|
||||
props: ReportWidgetRendererProps<{
|
||||
path: string
|
||||
search: string
|
||||
count: number
|
||||
}>
|
||||
) => {
|
||||
if (props.data.length === 0) return null
|
||||
const headerClasses = '!text-xs !py-2 p-0 font-bold !bg-scale-400'
|
||||
const cellClasses = '!text-xs !py-2'
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>Top Cache Misses</h4>
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th className={headerClasses}>Request</Table.th>
|
||||
<Table.th className={headerClasses + ' text-right'}>Count</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
{props.data.map((datum) => (
|
||||
<Fragment key={datum.path + (datum.search || '')}>
|
||||
<Table.tr className="p-0">
|
||||
<Table.td className={[cellClasses].join(' ')}>
|
||||
<div className=" truncate max-w-sm lg:max-w-lg">
|
||||
<TextFormatter className="text-scale-1100" value={datum.path} />
|
||||
<TextFormatter
|
||||
className="max-w-sm text-scale-900 truncate "
|
||||
value={decodeURIComponent(datum.search || '')}
|
||||
/>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td className={[cellClasses, 'text-right align-top'].join(' ')}>
|
||||
{datum.count}
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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' ? (
|
||||
|
||||
+1
-1
@@ -202,7 +202,7 @@ const InputField = ({
|
||||
...(field.isNullable ? [{ value: 'null', label: 'NULL' }] : []),
|
||||
]
|
||||
|
||||
const defaultValue = field.value === null ? 'null' : field.value
|
||||
const defaultValue = field.defaultValue !== null ? field.defaultValue : 'null'
|
||||
|
||||
return (
|
||||
<Listbox
|
||||
|
||||
@@ -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,9 +1,14 @@
|
||||
import { useParams } from 'common'
|
||||
import Link from 'next/link'
|
||||
import { useMemo } from 'react'
|
||||
import { Badge } from 'ui'
|
||||
|
||||
import { getResourcesExceededLimits } from 'components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import { useProjectReadOnlyQuery } from 'data/config/project-read-only-query'
|
||||
import {
|
||||
getResourcesExceededLimits,
|
||||
getResourcesExceededLimitsOrg,
|
||||
} from 'components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
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'
|
||||
@@ -13,39 +18,55 @@ 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'
|
||||
|
||||
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: usage } = useProjectUsageQuery(
|
||||
const { data: projectUsage } = useProjectUsageQuery(
|
||||
{ projectRef },
|
||||
{ enabled: Boolean(selectedOrganization && !selectedOrganization.subscription_id) }
|
||||
)
|
||||
const { data: orgUsage } = useOrgUsageQuery(
|
||||
{ orgSlug: selectedOrganization?.slug },
|
||||
{ enabled: Boolean(selectedOrganization && selectedOrganization.subscription_id) }
|
||||
)
|
||||
|
||||
const exceedingLimits = useMemo(() => {
|
||||
if (orgUsage) {
|
||||
return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
|
||||
} else if (projectUsage) {
|
||||
return getResourcesExceededLimits(projectUsage).length > 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, [projectUsage, orgUsage])
|
||||
|
||||
// Skip with org-based-billing, as quota is for the entire org
|
||||
const { data: projectSubscription } = useProjectSubscriptionV2Query(
|
||||
{ projectRef },
|
||||
{ enabled: selectedOrganization && !selectedOrganization.subscription_id }
|
||||
)
|
||||
const resourcesExceededLimits = getResourcesExceededLimits(usage)
|
||||
|
||||
// Skip with org-based-billing, as quota is for the entire org
|
||||
const { data: subscription } = useProjectSubscriptionV2Query(
|
||||
const { data: orgSubscription } = useProjectSubscriptionV2Query(
|
||||
{ projectRef },
|
||||
{ enabled: selectedOrganization && !selectedOrganization.subscription_id }
|
||||
{ enabled: Boolean(selectedOrganization && selectedOrganization.subscription_id) }
|
||||
)
|
||||
|
||||
const subscription = useMemo(() => {
|
||||
return projectSubscription || orgSubscription
|
||||
}, [projectSubscription, orgSubscription])
|
||||
|
||||
const projectHasNoLimits = subscription?.usage_billing_enabled === true
|
||||
|
||||
const showOverUsageBadge =
|
||||
useFlag('overusageBadge') &&
|
||||
subscription !== undefined &&
|
||||
(subscription.plan.id === 'free' || subscription?.plan.id === 'pro') &&
|
||||
(subscription.plan.id === 'free' || subscription.plan.id === 'pro') &&
|
||||
!projectHasNoLimits &&
|
||||
resourcesExceededLimits.length > 0
|
||||
exceedingLimits
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -81,21 +102,15 @@ 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 href={`/project/${projectRef}/settings/billing/usage`}>
|
||||
<Link
|
||||
href={
|
||||
selectedOrganization?.subscription_id
|
||||
? `/org/${selectedOrganization.slug}/usage`
|
||||
: `/project/${projectRef}/settings/billing/usage`
|
||||
}
|
||||
>
|
||||
<a>
|
||||
<Badge color="red">Exceeding usage limits</Badge>
|
||||
</a>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PropsWithChildren } from 'react'
|
||||
import ProductMenu from 'components/ui/ProductMenu'
|
||||
import { useSelectedProject, withAuth } from 'hooks'
|
||||
import ProjectLayout from '../'
|
||||
import { generateReportsMenu } from './ReportsMenu.utils'
|
||||
import { useFlag } from 'hooks'
|
||||
|
||||
interface ReportsLayoutProps {
|
||||
title?: string
|
||||
@@ -15,12 +15,61 @@ const ReportsLayout = ({ title, children }: PropsWithChildren<ReportsLayoutProps
|
||||
|
||||
const project = useSelectedProject()
|
||||
const page = router.pathname.split('/')[4] || ''
|
||||
const ref = project?.ref ?? 'default'
|
||||
|
||||
const storageReportFlag = useFlag('storageReport')
|
||||
const menuItems = [
|
||||
{
|
||||
title: '',
|
||||
items: [
|
||||
{
|
||||
name: 'Custom reports',
|
||||
key: '',
|
||||
url: `/project/${ref}/reports`,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
name: 'API',
|
||||
key: 'api-overview',
|
||||
url: `/project/${ref}/reports/api-overview`,
|
||||
items: [],
|
||||
},
|
||||
...(storageReportFlag
|
||||
? [
|
||||
{
|
||||
name: 'Storage',
|
||||
key: 'storage',
|
||||
url: `/project/${ref}/reports/storage`,
|
||||
items: [],
|
||||
label: 'NEW',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Database',
|
||||
key: 'database',
|
||||
url: `/project/${ref}/reports/database`,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Query Performance',
|
||||
key: 'query-performance',
|
||||
url: `/project/${ref}/reports/query-performance`,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<ProjectLayout
|
||||
title={title}
|
||||
product="Reports"
|
||||
productMenu={<ProductMenu page={page} menu={generateReportsMenu(project)} />}
|
||||
productMenu={<ProductMenu page={page} menu={menuItems} />}
|
||||
>
|
||||
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Project } from 'types'
|
||||
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
|
||||
|
||||
export const generateReportsMenu = (project?: Project): ProductMenuGroup[] => {
|
||||
const ref = project?.ref ?? 'default'
|
||||
|
||||
return [
|
||||
{
|
||||
title: '',
|
||||
items: [
|
||||
{
|
||||
name: 'Custom reports',
|
||||
key: '',
|
||||
url: `/project/${ref}/reports`,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
name: 'API',
|
||||
key: 'api-overview',
|
||||
url: `/project/${ref}/reports/api-overview`,
|
||||
items: [],
|
||||
label: 'NEW',
|
||||
},
|
||||
{
|
||||
name: 'Database',
|
||||
key: 'database',
|
||||
url: `/project/${ref}/reports/database`,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Query Performance',
|
||||
key: 'query-performance',
|
||||
url: `/project/${ref}/reports/query-performance`,
|
||||
items: [],
|
||||
label: 'NEW',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,11 +1,21 @@
|
||||
import { Form, Modal, Input, Button } from 'ui'
|
||||
import dayjs from 'dayjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Form, Input, Listbox, Modal } from 'ui'
|
||||
|
||||
import { DATETIME_FORMAT } from 'lib/constants'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
export interface CustomExpiryModalProps {
|
||||
onCopyUrl: (name: string, url: string) => void
|
||||
}
|
||||
|
||||
const unitMap = {
|
||||
days: 3600 * 24,
|
||||
weeks: 3600 * 24 * 7,
|
||||
months: 3600 * 24 * 30,
|
||||
years: 3600 * 24 * 365,
|
||||
}
|
||||
|
||||
const CustomExpiryModal = ({ onCopyUrl }: CustomExpiryModalProps) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { getFileUrl, selectedFileCustomExpiry, setSelectedFileCustomExpiry } = storageExplorerStore
|
||||
@@ -25,34 +35,57 @@ const CustomExpiryModal = ({ onCopyUrl }: CustomExpiryModalProps) => {
|
||||
>
|
||||
<Form
|
||||
validateOnBlur
|
||||
initialValues={{ expiresIn: '' }}
|
||||
initialValues={{ expiresIn: '', units: 'days' }}
|
||||
onSubmit={async (values: any, { setSubmitting }: any) => {
|
||||
setSubmitting(true)
|
||||
onCopyUrl(
|
||||
selectedFileCustomExpiry.name,
|
||||
await getFileUrl(selectedFileCustomExpiry, values.expiresIn)
|
||||
await getFileUrl(
|
||||
selectedFileCustomExpiry,
|
||||
values.expiresIn * unitMap[values.units as 'days' | 'weeks' | 'months' | 'years']
|
||||
)
|
||||
)
|
||||
setSubmitting(false)
|
||||
onClose()
|
||||
}}
|
||||
validate={(values: any) => {
|
||||
const errors: any = {}
|
||||
if (values.expiresIn !== '' && values.expiresIn <= 0)
|
||||
if (values.expiresIn !== '' && values.expiresIn <= 0) {
|
||||
errors.expiresIn = 'Expiry duration cannot be less than 0'
|
||||
}
|
||||
return errors
|
||||
}}
|
||||
>
|
||||
{({ values, isSubmitting }: { values: any; isSubmitting: boolean }) => (
|
||||
<>
|
||||
<div className="py-6">
|
||||
<div className="pt-4 pb-2">
|
||||
<Modal.Content>
|
||||
<Input
|
||||
disabled={isSubmitting}
|
||||
type="number"
|
||||
id="expiresIn"
|
||||
label="Enter the duration for which the URL will be valid:"
|
||||
actions={<p className="text-sm text-scale-1000 mr-2">seconds</p>}
|
||||
/>
|
||||
<p className="text-sm text-light mb-2">
|
||||
Enter the duration for which the URL will be valid for:
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input disabled={isSubmitting} type="number" id="expiresIn" className="w-full" />
|
||||
<Listbox id="units" className="w-[150px]">
|
||||
<Listbox.Option id="days" label="days" value="days">
|
||||
days
|
||||
</Listbox.Option>
|
||||
<Listbox.Option id="weeks" label="weeks" value="weeks">
|
||||
weeks
|
||||
</Listbox.Option>
|
||||
<Listbox.Option id="months" label="months" value="months">
|
||||
months
|
||||
</Listbox.Option>
|
||||
<Listbox.Option id="years" label="years" value="years">
|
||||
years
|
||||
</Listbox.Option>
|
||||
</Listbox>
|
||||
</div>
|
||||
{values.expiresIn !== '' && (
|
||||
<p className="text-sm text-light mt-2">
|
||||
URL will expire on{' '}
|
||||
{dayjs().add(values.expiresIn, values.units).format(DATETIME_FORMAT)}
|
||||
</p>
|
||||
)}
|
||||
</Modal.Content>
|
||||
</div>
|
||||
<Modal.Separator />
|
||||
|
||||
@@ -409,6 +409,7 @@ const FileExplorerRow = ({
|
||||
/>
|
||||
) : (
|
||||
<Dropdown
|
||||
modal={false}
|
||||
side="bottom"
|
||||
align="end"
|
||||
overlay={[
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -10,28 +10,35 @@ export const CHART_COLORS = {
|
||||
}
|
||||
|
||||
// refer to packages/ui/radix-colors.js for full list of colors
|
||||
export const STACK_COLORS = [
|
||||
['brand', 9],
|
||||
['mint', 9],
|
||||
['blue', 9],
|
||||
['sky', 9],
|
||||
['lime', 9],
|
||||
['yellow', 9],
|
||||
['orange', 9],
|
||||
].map(([color, n]) => ({
|
||||
lighter: `var(--colors-${color}${(n as number) - 1})`,
|
||||
base: `var(--colors-${color}${n})`,
|
||||
darker: `var(--colors-${color}${(n as number) + 1})`,
|
||||
}))
|
||||
export type ValidStackColor =
|
||||
| 'brand'
|
||||
| 'blue'
|
||||
| 'red'
|
||||
| 'yellow'
|
||||
| 'green'
|
||||
| 'slate'
|
||||
| 'indigo'
|
||||
| 'tomato'
|
||||
| 'orange'
|
||||
| 'amber'
|
||||
|
||||
export const USAGE_COLORS = {
|
||||
200: 'hsl(var(--brand-default))',
|
||||
201: 'hsl(var(--brand-500))',
|
||||
400: 'var(--colors-amber9)',
|
||||
401: 'var(--colors-amber8)',
|
||||
404: 'var(--colors-amber7)',
|
||||
500: 'var(--colors-red9)',
|
||||
}
|
||||
export const genStackColorScales = (colors: ValidStackColor[]) =>
|
||||
colors.map((color) => {
|
||||
// override default base scale for certain colors that do not have good contrast
|
||||
const scale =
|
||||
(
|
||||
{
|
||||
slate: 11,
|
||||
} as any
|
||||
)[color] ?? 9
|
||||
return {
|
||||
lighter: `var(--colors-${color}${(scale as number) - 1})`,
|
||||
base: `var(--colors-${color}${scale})`,
|
||||
darker: `var(--colors-${color}${(scale as number) + 1})`,
|
||||
}
|
||||
})
|
||||
|
||||
export const DEFAULT_STACK_COLORS : ValidStackColor[] = ['brand', 'slate', 'blue', 'yellow', 'indigo']
|
||||
|
||||
export enum DateTimeFormats {
|
||||
FULL = 'MMM D, YYYY, hh:mma',
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from 'react'
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react'
|
||||
import { ComponentMeta } from '@storybook/react'
|
||||
import dayjs from 'dayjs'
|
||||
import Chart from './Sparkline'
|
||||
import { StackedChartProps } from './Charts.types'
|
||||
import { STACK_COLORS } from './Charts.constants'
|
||||
|
||||
export default {
|
||||
title: 'Charts/Sparkline',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'
|
||||
import dayjs from 'dayjs'
|
||||
import Chart from './StackedBarChart'
|
||||
import { StackedChartProps } from './Charts.types'
|
||||
import { STACK_COLORS } from './Charts.constants'
|
||||
import { DEFAULT_STACK_COLORS, genStackColorScales } from './Charts.constants'
|
||||
|
||||
export default {
|
||||
title: 'Charts/StackedBarChart',
|
||||
@@ -38,7 +38,7 @@ export const StackedBarChart = () => (
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<ExampleName>Default Stacked Chart Colors</ExampleName>
|
||||
<div className="flex flex-row ">
|
||||
{STACK_COLORS.map((c) => (
|
||||
{genStackColorScales(DEFAULT_STACK_COLORS).map((c) => (
|
||||
<div className="w-10 h-10" key={c.base} style={{ background: c.base }}></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { BarChart, Bar, XAxis, Tooltip, Legend, Cell, TooltipProps } from 'recharts'
|
||||
import ChartHeader from './ChartHeader'
|
||||
import { CHART_COLORS, STACK_COLORS, DateTimeFormats } from './Charts.constants'
|
||||
import { CHART_COLORS, DateTimeFormats, DEFAULT_STACK_COLORS, genStackColorScales, ValidStackColor } from './Charts.constants'
|
||||
import { CommonChartProps } from './Charts.types'
|
||||
import { timestampFormatter, useChartSize, useStacked } from './Charts.utils'
|
||||
import { precisionFormatter } from './Charts.utils'
|
||||
@@ -16,6 +16,7 @@ interface Props extends CommonChartProps<any> {
|
||||
displayDateInUtc?: boolean
|
||||
hideLegend?: boolean
|
||||
hideHeader?: boolean
|
||||
stackColors?: ValidStackColor[]
|
||||
}
|
||||
const StackedBarChart: React.FC<Props> = ({
|
||||
size,
|
||||
@@ -33,6 +34,7 @@ const StackedBarChart: React.FC<Props> = ({
|
||||
displayDateInUtc,
|
||||
hideLegend = false,
|
||||
hideHeader = false,
|
||||
stackColors = DEFAULT_STACK_COLORS
|
||||
}) => {
|
||||
const { Container } = useChartSize(size)
|
||||
const { dataKeys, stackedData, percentagesStackedData } = useStacked({
|
||||
@@ -44,6 +46,7 @@ const StackedBarChart: React.FC<Props> = ({
|
||||
})
|
||||
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
|
||||
if (!data || data.length === 0) return <NoDataPlaceholder size={size} />
|
||||
const stackColorScales = genStackColorScales(stackColors)
|
||||
return (
|
||||
<div className="w-full">
|
||||
{!hideHeader && (
|
||||
@@ -94,7 +97,7 @@ const StackedBarChart: React.FC<Props> = ({
|
||||
dataKey={datum}
|
||||
type="monotone"
|
||||
legendType="circle"
|
||||
fill={STACK_COLORS[stackIndex].base}
|
||||
fill={stackColorScales[stackIndex].base}
|
||||
stackId={1}
|
||||
animationDuration={300}
|
||||
maxBarSize={48}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { compact } from 'lodash'
|
||||
import { USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
|
||||
import { OrgUsageResponse, UsageMetric } from 'data/usage/org-usage-query'
|
||||
|
||||
export const getResourcesApproachingLimits = (usages: any) => {
|
||||
if (!usages) return []
|
||||
@@ -22,7 +23,21 @@ export const getResourcesExceededLimits = (usages: any) => {
|
||||
.filter((resourceName) => usages[resourceName] !== null)
|
||||
.map((resourceName) => {
|
||||
const resource = usages[resourceName]
|
||||
if (resource.limit > 0 && (resource.usage / resource.limit) > 1) return resourceName
|
||||
if (resource.limit > 0 && resource.usage / resource.limit > 1) return resourceName
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export const getResourcesExceededLimitsOrg = (usageMetrics: UsageMetric[]): string[] => {
|
||||
if (!usageMetrics.length) return []
|
||||
|
||||
return usageMetrics
|
||||
.filter((usageMetric) => {
|
||||
if (!usageMetric.capped || !usageMetric.available_in_plan || usageMetric.unlimited) return false
|
||||
|
||||
const freeUnits = usageMetric.pricing_free_units || 0
|
||||
|
||||
return usageMetric.usage > freeUnits
|
||||
})
|
||||
.map((it) => it.metric)
|
||||
}
|
||||
+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
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { ReportsLayout } from 'components/layouts'
|
||||
import {
|
||||
PRESET_CONFIG,
|
||||
REPORTS_DATEPICKER_HELPERS,
|
||||
} from 'components/interfaces/Reports/Reports.constants'
|
||||
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
||||
import { queriesFactory } from 'components/interfaces/Reports/Reports.utils'
|
||||
import {
|
||||
CacheHitRateChartRenderer,
|
||||
TopCacheMissesRenderer,
|
||||
} from 'components/interfaces/Reports/renderers/StorageRenderers'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
|
||||
import { DatePickerToFrom, LogsEndpointParams } from 'components/interfaces/Settings/Logs'
|
||||
import { useParams } from 'common'
|
||||
import ShimmerLine from 'components/ui/ShimmerLine'
|
||||
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
import DatePickers from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
|
||||
export const StorageReport: NextPageWithLayout = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const report = useStorageReport()
|
||||
|
||||
const { data: subscription } = useProjectSubscriptionV2Query({ projectRef })
|
||||
const plan = subscription?.plan
|
||||
|
||||
const handleDatepickerChange = ({ from, to }: DatePickerToFrom) => {
|
||||
report.mergeParams({
|
||||
iso_timestamp_start: from || '',
|
||||
iso_timestamp_end: to || '',
|
||||
})
|
||||
}
|
||||
|
||||
const datepickerHelpers = useMemo(
|
||||
() =>
|
||||
REPORTS_DATEPICKER_HELPERS.map((helper, index) => ({
|
||||
...helper,
|
||||
disabled: (index > 0 && plan?.id === 'free') || (index > 1 && plan?.id !== 'pro'),
|
||||
})),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<ReportPadding>
|
||||
<ReportHeader title="Storage" isLoading={report.isLoading} onRefresh={report.refresh} />
|
||||
<div className="w-full flex flex-col gap-1">
|
||||
<div>
|
||||
<DatePickers
|
||||
onChange={handleDatepickerChange}
|
||||
to={report.params.cacheHitRate.iso_timestamp_end || ''}
|
||||
from={report.params.cacheHitRate.iso_timestamp_start || ''}
|
||||
helpers={datepickerHelpers}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-2 w-full">
|
||||
<ShimmerLine active={report.isLoading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReportWidget
|
||||
isLoading={report.isLoading}
|
||||
params={report.params.cacheHitRate}
|
||||
title="Request Caching"
|
||||
tooltip="The number of storage requests that are cached at the edge level. A higher number of hits is better."
|
||||
data={report.data.cacheHitRate || []}
|
||||
renderer={CacheHitRateChartRenderer}
|
||||
append={TopCacheMissesRenderer}
|
||||
appendProps={{ data: report.data.topCacheMisses || [] }}
|
||||
/>
|
||||
</ReportPadding>
|
||||
)
|
||||
}
|
||||
|
||||
// hook to fetch data
|
||||
const useStorageReport = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const queryHooks = queriesFactory<keyof typeof PRESET_CONFIG.storage.queries>(
|
||||
PRESET_CONFIG.storage.queries,
|
||||
projectRef ?? 'default'
|
||||
)
|
||||
const cacheHitRate = queryHooks.cacheHitRate()
|
||||
const topCacheMisses = queryHooks.topCacheMisses()
|
||||
const activeHooks = [cacheHitRate, topCacheMisses]
|
||||
|
||||
const handleRefresh = async () => {
|
||||
activeHooks.forEach((hook) => hook.runQuery())
|
||||
}
|
||||
const handleSetParams = (params: Partial<LogsEndpointParams>) => {
|
||||
activeHooks.forEach((hook) => {
|
||||
hook.setParams?.((prev: LogsEndpointParams) => ({ ...prev, ...params }))
|
||||
})
|
||||
}
|
||||
useEffect(()=>{
|
||||
|
||||
if (cacheHitRate.changeQuery) {
|
||||
cacheHitRate.changeQuery(PRESET_CONFIG.storage.queries.cacheHitRate.sql([]))
|
||||
}
|
||||
|
||||
if (topCacheMisses.changeQuery) {
|
||||
topCacheMisses.changeQuery(PRESET_CONFIG.storage.queries.topCacheMisses.sql([]))
|
||||
}
|
||||
|
||||
}, [])
|
||||
const isLoading = activeHooks.some((hook) => hook.isLoading)
|
||||
return {
|
||||
data: {
|
||||
cacheHitRate: cacheHitRate.logData,
|
||||
topCacheMisses: topCacheMisses.logData,
|
||||
},
|
||||
params: {
|
||||
cacheHitRate: cacheHitRate.params,
|
||||
topCacheMisses: topCacheMisses.params,
|
||||
},
|
||||
mergeParams: handleSetParams,
|
||||
isLoading,
|
||||
refresh: handleRefresh,
|
||||
}
|
||||
}
|
||||
|
||||
StorageReport.getLayout = (page) => <ReportsLayout>{page}</ReportsLayout>
|
||||
|
||||
export default observer(StorageReport)
|
||||
@@ -0,0 +1,42 @@
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { render } from '../../../helpers'
|
||||
import { waitFor, screen } from '@testing-library/react'
|
||||
import { StorageReport } from 'pages/project/[ref]/reports/storage'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
beforeEach(() => {
|
||||
// reset mocks between tests
|
||||
get.mockReset()
|
||||
get.mockImplementation(async (_url) => [{ result: [] }])
|
||||
})
|
||||
|
||||
test(`static elements`, async () => {
|
||||
render(<StorageReport />)
|
||||
await screen.findByText('Request Caching')
|
||||
await screen.findByText(/Last 24 hours/)
|
||||
await screen.findByText(/Custom/)
|
||||
})
|
||||
|
||||
test('refresh button', async () => {
|
||||
render(<StorageReport />)
|
||||
await waitFor(() => expect(get).toBeCalled())
|
||||
get.mockReset()
|
||||
userEvent.click(await screen.findByText(/Refresh/))
|
||||
await waitFor(() => expect(get).toBeCalled())
|
||||
})
|
||||
|
||||
test('append - top cache misses', async () => {
|
||||
get.mockImplementation(async (url) => {
|
||||
if (decodeURIComponent(url).includes('misses')) {
|
||||
return {
|
||||
result: [{ path: 'mypath', search: 'some-query', count: 22 }],
|
||||
}
|
||||
}
|
||||
return { result: [{ timestamp: new Date().toISOString(), miss_count: 123, hit_count: 123 }] }
|
||||
})
|
||||
render(<StorageReport />)
|
||||
await waitFor(() => expect(get).toBeCalled())
|
||||
await screen.findAllByText(/mypath/)
|
||||
await screen.findAllByText(/some\-query/)
|
||||
await screen.findAllByText(/22/)
|
||||
})
|
||||
Reference in new issue
Block a user