feat: new disk size usage section (#29862)

This commit is contained in:
Kevin Grüneberg
2024-10-14 14:48:45 +08:00
committed by GitHub
parent d916261f05
commit b64999e287
11 changed files with 499 additions and 95 deletions

View File

@@ -95,10 +95,11 @@ export const BILLING_BREAKDOWN_METRICS: Metric[] = [
{
key: PricingMetric.DISK_SIZE_GB_HOURS_GP3,
name: 'Disk Size',
anchor: 'diskSize',
units: 'absolute',
unitName: 'GB-Hrs',
category: 'Database',
tip: 'Each project gets provisioned with 8 GB of GP3 disk for free. When you get close to the disk size limit, we autoscale your disk by 1.5x. Each GB of provisioned disk size beyond 8 GB incurs a GB-Hr every hour. Each extra GB is billed at $0.125/month, prorated down to the hour.',
tip: 'Each project gets provisioned with 8 GB of GP3 disk for free. When you get close to the disk size limit, we autoscale your disk by 1.5x. Each GB of provisioned disk size beyond 8 GB incurs a GB-Hr every hour. Each extra GB is billed at $0.125/month ($0.000171/GB-Hr), prorated down to the hour.',
docLink: {
title: 'Read more',
url: 'https://supabase.com/docs/guides/platform/org-based-billing#disk-size',
@@ -107,6 +108,7 @@ export const BILLING_BREAKDOWN_METRICS: Metric[] = [
{
key: PricingMetric.DISK_SIZE_GB_HOURS_IO2,
name: 'Disk Size IO2',
anchor: 'diskSize',
units: 'absolute',
unitName: 'GB-Hrs',
category: 'Database',

View File

@@ -174,7 +174,7 @@ const BillingMetric = ({
{subscription.usage_billing_enabled === false &&
relativeToSubscription &&
(isApproachingLimit || isExceededLimit) && (
<div className="mt-2">
<div className="my-2">
<p className="text-sm">
Exceeding your plans included usage will lead to restrictions to your project.
Upgrade to a usage-based plan or disable the spend cap to avoid restrictions.

View File

@@ -7,6 +7,7 @@ import { useOrgUsageQuery } from 'data/usage/org-usage-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
import { CriticalIcon, WarningIcon } from 'ui'
import { PricingMetric } from 'data/analytics/org-daily-stats-query'
export const Restriction = () => {
const org = useSelectedOrganization()
@@ -18,7 +19,10 @@ export const Restriction = () => {
const hasExceededAnyLimits = Boolean(
usage?.usages.find(
(metric) =>
!metric.unlimited && metric.capped && metric.usage > (metric?.pricing_free_units ?? 0)
metric.metric !== PricingMetric.DISK_SIZE_GB_HOURS_GP3 &&
!metric.unlimited &&
metric.capped &&
metric.usage > (metric?.pricing_free_units ?? 0)
)
)

View File

@@ -100,73 +100,93 @@ export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[
name: 'Database & Storage Size',
description: 'Amount of resources your project is consuming',
attributes: [
{
anchor: 'dbSize',
key: PricingMetric.DATABASE_SIZE,
attributes: [{ key: PricingMetric.DATABASE_SIZE.toLowerCase(), color: 'white' }],
name: 'Database size',
chartPrefix: 'Average',
unit: 'bytes',
description:
subscription?.usage_based_billing_project_addons === true
? 'Database size refers to the monthly average database space usage, as reported by Postgres. Paid Plans use auto-scaling disks and are billed based on provisioned disk size, rather than database space used.'
: 'Database size refers to the monthly average database space usage, as reported by Postgres. Paid Plans use auto-scaling disks.\nBilling is based on the average daily database size used in GB throughout the billing period. Billing is independent of the provisioned disk size.',
links: [
{
name: 'Documentation',
url: 'https://supabase.com/docs/guides/platform/database-size',
subscription?.plan.id === 'free' || subscription?.usage_based_billing_project_addons !== true
? {
anchor: 'dbSize',
key: PricingMetric.DATABASE_SIZE,
attributes: [{ key: PricingMetric.DATABASE_SIZE.toLowerCase(), color: 'white' }],
name: 'Database size',
chartPrefix: 'Average',
unit: 'bytes',
description:
'Database size refers to the monthly average database space usage, as reported by Postgres. Paid Plans use auto-scaling disks.\nBilling is based on the average daily database size used in GB throughout the billing period. Billing is independent of the provisioned disk size.',
links: [
{
name: 'Documentation',
url: 'https://supabase.com/docs/guides/platform/database-size',
},
...(subscription?.usage_based_billing_project_addons === true
? [
{
name: 'Disk Management',
url: 'https://supabase.com/docs/guides/platform/database-size#disk-management',
},
]
: []),
],
chartDescription: 'The data refreshes every 24 hours.',
additionalInfo: (usage?: OrgUsageResponse) => {
const usageMeta = usage?.usages.find((x) => x.metric === PricingMetric.DATABASE_SIZE)
const usageRatio =
typeof usageMeta !== 'number'
? (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0)
: 0
const hasLimit = usageMeta && (usageMeta?.pricing_free_units ?? 0) > 0
const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD
const isExceededLimit = hasLimit && usageRatio >= 1
const isCapped = usageMeta?.capped
const onFreePlan = subscription?.plan?.name === 'Free'
return (
<div>
{(isApproachingLimit || isExceededLimit) && isCapped && (
<Alert
withIcon
variant={isExceededLimit ? 'danger' : 'warning'}
title={
isExceededLimit
? 'Exceeding database size limit'
: 'Nearing database size limit'
}
>
<div className="flex w-full items-center flex-col justify-center space-y-2 md:flex-row md:justify-between">
<div>
When you reach your database size limit, your project can go into
read-only mode.{' '}
{onFreePlan
? 'Please upgrade your Plan.'
: "Disable your spend cap to scale seamlessly, and pay for over-usage beyond your Plan's quota."}
</div>
</div>
</Alert>
)}
</div>
)
},
}
: {
anchor: 'diskSize',
key: 'diskSize',
attributes: [],
name: 'Disk size',
chartPrefix: 'Average',
unit: 'bytes',
description:
"Each Supabase project comes with a dedicated disk. Each project gets 8 GB of disk for free. Billing is based on the provisioned disk size. Disk automatically scales up when you get close to it's size.\nEach hour your project is using more than 8 GB of GP3 disk, it incurs the overages in GB-Hrs, i.e. a 16 GB disk incurs 8 GB-Hrs every hour. Extra disk size costs $0.125/GB/month ($0.000171/GB-Hr).",
links: [
{
name: 'Documentation',
url: 'https://supabase.com/docs/guides/platform/org-based-billing#disk-size',
},
{
name: 'Disk Management',
url: 'https://supabase.com/docs/guides/platform/database-size#disk-management',
},
],
chartDescription: '',
},
...(subscription?.usage_based_billing_project_addons === true
? [
{
name: 'Disk Management',
url: 'https://supabase.com/docs/guides/platform/database-size#disk-management',
},
]
: []),
],
chartDescription: 'The data refreshes every 24 hours.',
additionalInfo: (usage?: OrgUsageResponse) => {
const usageMeta = usage?.usages.find((x) => x.metric === PricingMetric.DATABASE_SIZE)
const usageRatio =
typeof usageMeta !== 'number'
? (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0)
: 0
const hasLimit = usageMeta && (usageMeta?.pricing_free_units ?? 0) > 0
const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD
const isExceededLimit = hasLimit && usageRatio >= 1
const isCapped = usageMeta?.capped
const onFreePlan = subscription?.plan?.name === 'Free'
return (
<div>
{(isApproachingLimit || isExceededLimit) && isCapped && (
<Alert
withIcon
variant={isExceededLimit ? 'danger' : 'warning'}
title={
isExceededLimit
? 'Exceeding database size limit'
: 'Nearing database size limit'
}
>
<div className="flex w-full items-center flex-col justify-center space-y-2 md:flex-row md:justify-between">
<div>
When you reach your database size limit, your project can go into read-only
mode.{' '}
{onFreePlan
? 'Please upgrade your Plan.'
: 'Disable your spend cap to scale seamlessly and pay for over-usage beyond your Plans quota.'}
</div>
</div>
</Alert>
)}
</div>
)
},
},
{
anchor: 'storageSize',
key: PricingMetric.STORAGE_SIZE,

View File

@@ -57,20 +57,23 @@ const Usage = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectRef, isSuccess])
const billingCycleStart = dayjs.unix(subscription?.current_period_start ?? 0).utc()
const billingCycleEnd = dayjs.unix(subscription?.current_period_end ?? 0).utc()
const billingCycleStart = useMemo(() => {
return dayjs.unix(subscription?.current_period_start ?? 0).utc()
}, [subscription])
const billingCycleEnd = useMemo(() => {
return dayjs.unix(subscription?.current_period_end ?? 0).utc()
}, [subscription])
const currentBillingCycleSelected = useMemo(() => {
// Selected by default
if (!dateRange?.period_start || !dateRange?.period_end || !subscription) return true
const { current_period_start, current_period_end } = subscription
if (!dateRange?.period_start || !dateRange?.period_end) return true
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))
dayjs(dateRange.period_start.date).isSame(billingCycleStart) &&
dayjs(dateRange.period_end.date).isSame(billingCycleEnd)
)
}, [dateRange, subscription])
}, [dateRange, billingCycleStart, billingCycleEnd])
const startDate = useMemo(() => {
// If end date is in future, set end date to now

View File

@@ -174,7 +174,7 @@ const AttributeUsage = ({
{usageMeta && (
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-foreground-light">
Included in {subscription?.plan?.name.toLowerCase()} plan
Included in {subscription?.plan?.name} Plan
</p>
{usageMeta.unlimited ? (
<p className="text-xs">Unlimited</p>

View File

@@ -0,0 +1,232 @@
import AlertError from 'components/ui/AlertError'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import type { OrgSubscription } from 'data/subscriptions/types'
import SectionContent from '../SectionContent'
import { CategoryAttribute } from '../Usage.constants'
import { useOrgProjectsQuery } from 'data/projects/org-projects'
import { PROJECT_STATUS } from 'lib/constants'
import {
Button,
Alert_Shadcn_,
CriticalIcon,
AlertTitle_Shadcn_,
AlertDescription_Shadcn_,
} from 'ui'
import MotionNumber from 'motion-number'
import Link from 'next/link'
import { useMemo } from 'react'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import { OrgUsageResponse } from 'data/usage/org-usage-query'
import { PricingMetric } from 'data/analytics/org-daily-stats-query'
import Panel from 'components/ui/Panel'
export interface DiskUsageProps {
slug: string
projectRef?: string
attribute: CategoryAttribute
subscription?: OrgSubscription
usage?: OrgUsageResponse
currentBillingCycleSelected: boolean
}
const DiskUsage = ({
slug,
projectRef,
attribute,
subscription,
usage,
currentBillingCycleSelected,
}: DiskUsageProps) => {
const {
data: diskUsage,
isError,
isLoading,
isSuccess,
error,
} = useOrgProjectsQuery(
{
orgSlug: slug,
},
{
enabled: currentBillingCycleSelected,
}
)
const hasProjectsExceedingDiskSize = useMemo(() => {
if (diskUsage) {
return diskUsage.projects.some((it) =>
it.databases.some(
(db) =>
db.type === 'READ_REPLICA' || (db.disk_volume_size_gb && db.disk_volume_size_gb > 8)
)
)
} else {
return false
}
}, [diskUsage])
const gp3UsageInPeriod = usage?.usages.find((it) => it.metric === PricingMetric.DISK_IOPS_GP3)
const io2UsageInPeriod = usage?.usages.find((it) => it.metric === PricingMetric.DISK_IOPS_IO2)
const relevantProjects = useMemo(() => {
return diskUsage
? diskUsage.projects
.filter((it) => !it.is_branch && it.status !== PROJECT_STATUS.INACTIVE)
.filter((it) => it.ref === projectRef || !projectRef)
: []
}, [diskUsage, projectRef])
return (
<div id={attribute.anchor} className="scroll-my-12">
<SectionContent section={attribute}>
{isLoading && (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)}
{isError && <AlertError subject="Failed to retrieve usage data" error={error} />}
{isSuccess && (
<div className="space-y-4">
{currentBillingCycleSelected &&
subscription?.usage_billing_enabled === false &&
hasProjectsExceedingDiskSize && (
<Alert_Shadcn_ variant="warning">
<CriticalIcon />
<AlertTitle_Shadcn_>Projects exceeding quota</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
You have projects that are exceeding 8 GB of provisioned disk size, but do not
allow any overages with the Spend Cap on. Reduce the disk size or disable the
spend cap.
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
<div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<p className="text-sm">{attribute.name} usage</p>
</div>
</div>
<div className="flex items-center justify-between border-b py-1">
<p className="text-xs text-foreground-light">
Included in {subscription?.plan?.name} Plan
</p>
<p className="text-xs">8 GB GP3 disk per project</p>
</div>
<div className="flex items-center justify-between">
<p className="text-xs text-foreground-light">Overages in period</p>
<p className="text-xs">
{gp3UsageInPeriod?.usage ?? 0} GP3 GB-Hrs
{io2UsageInPeriod?.usage ? ` / ${io2UsageInPeriod.usage} IO2 GB-Hrs` : ``}
</p>
</div>
</div>
{currentBillingCycleSelected ? (
<div className="space-y-4">
<div className="space-y-1">
<p className="text-sm">Current disk size per project</p>
<p className="text-sm text-foreground-light">
Breakdown of disk per project. Head to your project's disk management section to
see database space used.
</p>
</div>
{relevantProjects.length === 0 && (
<Panel>
<Panel.Content>
<div className="flex flex-col items-center justify-center">
<p className="text-sm">No active projects</p>
<p className="text-sm text-foreground-light">
You don't have any active projects in this organization.
</p>
</div>
</Panel.Content>
</Panel>
)}
{relevantProjects.map((project, idx) => {
const primaryDiskUsage = project.databases
.filter((it) => it.type === 'PRIMARY')
.reduce((acc, curr) => acc + (curr.disk_volume_size_gb ?? 8), 0)
const replicaDbs = project.databases.filter((it) => it.type !== 'PRIMARY')
const replicaDiskUsage = replicaDbs.reduce(
(acc, curr) => acc + (curr.disk_volume_size_gb ?? 8),
0
)
const totalDiskUsage = primaryDiskUsage + replicaDiskUsage
return (
<div className={idx !== relevantProjects.length - 1 ? 'border-b pb-2' : ''}>
<div className="flex justify-between">
<span className="text-foreground-light flex items-center gap-2">
{project.name}
</span>
<Button asChild type="default" size={'tiny'}>
<Link href={`/project/${project.ref}/settings/database#disk-management`}>
Manage Disk
</Link>
</Button>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center h-6 gap-3">
<span className="text-foreground-light text-sm font-mono flex items-center gap-2">
<span className="text-foreground font-semibold -mt-[2px]">
<MotionNumber
value={totalDiskUsage}
style={{ lineHeight: 0.8 }}
transition={{
y: { type: 'spring', duration: 0.35, bounce: 0 },
}}
className="font-mono"
/>
</span>{' '}
GB Disk provisioned
</span>
<InfoTooltip side="top">
<p>{primaryDiskUsage} GB for Primary Database</p>
{replicaDbs.length > 0 && (
<>
<p>
{replicaDiskUsage} GB for {replicaDbs.length} Read{' '}
{replicaDbs.length === 1 ? 'Replica' : 'Replicas'}
</p>
<p className="mt-1">
Read replicas have their own disk and use 25% more disk to account
for WAL files.
</p>
</>
)}
</InfoTooltip>
</div>
</div>
</div>
)
})}
</div>
) : (
<Panel>
<Panel.Content>
<div className="flex flex-col items-center justify-center">
<p className="text-sm">Data not available</p>
<p className="text-sm text-foreground-light">
Switch to current billing cycle to see current disk size per project.
</p>
</div>
</Panel.Content>
</Panel>
)}
</div>
)}
</SectionContent>
</div>
)
}
export default DiskUsage

View File

@@ -5,6 +5,7 @@ import { useOrgUsageQuery } from 'data/usage/org-usage-query'
import SectionHeader from '../SectionHeader'
import { CategoryMetaKey, USAGE_CATEGORIES } from '../Usage.constants'
import AttributeUsage from './AttributeUsage'
import DiskUsage from './DiskUsage'
export interface ChartMeta {
[key: string]: { data: DataPoint[]; margin: number; isLoading: boolean }
@@ -48,23 +49,35 @@ const UsageSection = ({
<ScaffoldDivider />
{categoryMeta.attributes.map((attribute) => (
<AttributeUsage
key={attribute.name}
slug={orgSlug}
projectRef={projectRef}
attribute={attribute}
usage={usage}
usageMeta={usage?.usages.find((x) => x.metric === attribute.key)}
chartMeta={chartMeta}
subscription={subscription}
error={usageError}
isLoading={isLoadingUsage}
isError={isErrorUsage}
isSuccess={isSuccessUsage}
currentBillingCycleSelected={currentBillingCycleSelected}
/>
))}
{categoryMeta.attributes.map((attribute) =>
attribute.key === 'diskSize' ? (
<DiskUsage
key={attribute.name}
slug={orgSlug}
projectRef={projectRef}
attribute={attribute}
subscription={subscription}
currentBillingCycleSelected={currentBillingCycleSelected}
usage={usage}
/>
) : (
<AttributeUsage
key={attribute.name}
slug={orgSlug}
projectRef={projectRef}
attribute={attribute}
usage={usage}
usageMeta={usage?.usages.find((x) => x.metric === attribute.key)}
chartMeta={chartMeta}
subscription={subscription}
error={usageError}
isLoading={isLoadingUsage}
isError={isErrorUsage}
isSuccess={isSuccessUsage}
currentBillingCycleSelected={currentBillingCycleSelected}
/>
)
)}
</>
)
}

View File

@@ -14,4 +14,6 @@ export const projectKeys = {
) => ['projects', 'transfer', projectRef, targetOrganizationSlug, 'preview'] as const,
pauseStatus: (projectRef: string | undefined) =>
['projects', projectRef, 'pause-status'] as const,
orgProjects: (slug: string | undefined) => ['projects', 'org', slug] as const,
}

View File

@@ -0,0 +1,43 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import type { components } from 'data/api'
import { get, handleError } from 'data/fetchers'
import type { ResponseError } from 'types'
import { projectKeys } from './keys'
export type OrgProjectsVariables = {
orgSlug?: string
}
export type OrgProjectsResponse = components['schemas']['OrganizationProjectsResponse']
export async function getOrgProjects(
{ orgSlug }: OrgProjectsVariables,
signal?: AbortSignal
): Promise<OrgProjectsResponse> {
if (!orgSlug) throw new Error('orgSlug is required')
const { data, error } = await get(`/platform/organizations/{slug}/projects`, {
params: {
path: { slug: orgSlug },
},
signal,
})
if (error) handleError(error)
return data
}
export type OrgProjectsData = Awaited<ReturnType<typeof getOrgProjects>>
export type OrgProjectsError = ResponseError
export const useOrgProjectsQuery = <TData = OrgProjectsData>(
{ orgSlug }: OrgProjectsVariables,
{ enabled = true, ...options }: UseQueryOptions<OrgProjectsData, OrgProjectsError, TData> = {}
) =>
useQuery<OrgProjectsData, OrgProjectsError, TData>(
projectKeys.orgProjects(orgSlug),
({ signal }) => getOrgProjects({ orgSlug }, signal),
{
enabled: enabled && typeof orgSlug !== 'undefined',
...options,
}
)

View File

@@ -457,6 +457,10 @@ export interface paths {
/** Sets up a payment method */
post: operations['SetupIntentOrgController_setUpPaymentMethod']
}
'/platform/organizations/{slug}/projects': {
/** Gets all projects for the given organization */
get: operations['OrganizationProjectsController_getProjects']
}
'/platform/organizations/{slug}/roles': {
/** Gets the given organization's roles */
get: operations['OrganizationRolesController_getAllRolesV2']
@@ -3116,6 +3120,21 @@ export interface components {
| 'RESTARTING'
| 'RESIZING'
}
/** @enum {string} */
DatabaseStatus:
| 'ACTIVE_HEALTHY'
| 'ACTIVE_UNHEALTHY'
| 'COMING_UP'
| 'GOING_DOWN'
| 'INIT_FAILED'
| 'REMOVED'
| 'RESTORING'
| 'UNKNOWN'
| 'UPGRADING'
| 'INIT_READ_REPLICA'
| 'INIT_READ_REPLICA_FAILED'
| 'RESTARTING'
| 'RESIZING'
DatabaseStatusResponse: {
identifier: string
replicaInitializationStatus?: Record<string, never>
@@ -3135,6 +3154,8 @@ export interface components {
| 'RESTARTING'
| 'RESIZING'
}
/** @enum {string} */
DatabaseType: 'PRIMARY' | 'READ_REPLICA'
DatabaseUpgradeStatus: {
/** @enum {string} */
error?:
@@ -4169,6 +4190,9 @@ export interface components {
*/
supabase_org_id: string
}
OrganizationProjectsResponse: {
projects: components['schemas']['ProjectWithDatabases'][]
}
OrganizationResponse: {
billing_email: string | null
id: number
@@ -4669,6 +4693,19 @@ export interface components {
release_channel: components['schemas']['ReleaseChannel']
version: string
}
ProjectDatabase: {
cloud_provider: string
disk_last_modified_at?: string
disk_throughput_mbps?: number
/** @enum {string} */
disk_type?: 'gp3' | 'io2'
disk_volume_size_gb?: number
identifier: string
infra_compute_size?: components['schemas']['DbInstanceSize']
region: string
status: components['schemas']['DatabaseStatus']
type: components['schemas']['DatabaseType']
}
ProjectDetailResponse: {
cloud_provider: string
connectionString: string
@@ -4852,6 +4889,23 @@ export interface components {
ssl_enforced: boolean
status: string
}
/** @enum {string} */
ProjectStatus:
| 'ACTIVE_HEALTHY'
| 'ACTIVE_UNHEALTHY'
| 'COMING_UP'
| 'GOING_DOWN'
| 'INACTIVE'
| 'INIT_FAILED'
| 'REMOVED'
| 'RESTARTING'
| 'UNKNOWN'
| 'UPGRADING'
| 'PAUSING'
| 'RESTORING'
| 'RESTORE_FAILED'
| 'PAUSE_FAILED'
| 'RESIZING'
ProjectUnpauseVersionInfo: {
postgres_engine: components['schemas']['PostgresEngine']
release_channel: components['schemas']['ReleaseChannel']
@@ -4876,6 +4930,14 @@ export interface components {
postgres_version: components['schemas']['PostgresEngine']
release_channel: components['schemas']['ReleaseChannel']
}
ProjectWithDatabases: {
databases: components['schemas']['ProjectDatabase'][]
is_branch: boolean
name: string
ref: string
region: string
status: components['schemas']['ProjectStatus']
}
Provider: {
created_at?: string
domains?: components['schemas']['Domain'][]
@@ -9195,6 +9257,29 @@ export interface operations {
}
}
}
/** Gets all projects for the given organization */
OrganizationProjectsController_getProjects: {
parameters: {
path: {
/** @description Organization slug */
slug: string
}
}
responses: {
200: {
content: {
'application/json': components['schemas']['OrganizationProjectsResponse']
}
}
403: {
content: never
}
/** @description Failed to retrieve projects */
500: {
content: never
}
}
}
/** Gets the given organization's roles */
OrganizationRolesController_getAllRolesV2: {
parameters: {