diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.constants.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.constants.ts index 5bd920c7e5..b4e6309f82 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.constants.ts +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.constants.ts @@ -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', diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx index 449012f73b..476370e1f9 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx @@ -174,7 +174,7 @@ const BillingMetric = ({ {subscription.usage_billing_enabled === false && relativeToSubscription && (isApproachingLimit || isExceededLimit) && ( -
+

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. diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx index fa29bb8eaf..4d5dd46d63 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx @@ -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) ) ) diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.constants.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.constants.tsx index 3cfb495614..aebafdd48a 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.constants.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.constants.tsx @@ -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 ( +

+ {(isApproachingLimit || isExceededLimit) && isCapped && ( + +
+
+ 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."} +
+
+
+ )} +
+ ) + }, + } + : { + 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 ( -
- {(isApproachingLimit || isExceededLimit) && isCapped && ( - -
-
- 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.'} -
-
-
- )} -
- ) - }, - }, { anchor: 'storageSize', key: PricingMetric.STORAGE_SIZE, diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index c07f5abd38..f8b780933a 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -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 diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx index aacb86d1c5..de7799d0f4 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx @@ -174,7 +174,7 @@ const AttributeUsage = ({ {usageMeta && (

- Included in {subscription?.plan?.name.toLowerCase()} plan + Included in {subscription?.plan?.name} Plan

{usageMeta.unlimited ? (

Unlimited

diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx new file mode 100644 index 0000000000..b88e217c13 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx @@ -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 ( +
+ + {isLoading && ( +
+ + + +
+ )} + {isError && } + {isSuccess && ( +
+ {currentBillingCycleSelected && + subscription?.usage_billing_enabled === false && + hasProjectsExceedingDiskSize && ( + + + Projects exceeding quota + + 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. + + + )} + +
+
+
+

{attribute.name} usage

+
+
+ +
+

+ Included in {subscription?.plan?.name} Plan +

+

8 GB GP3 disk per project

+
+ +
+

Overages in period

+

+ {gp3UsageInPeriod?.usage ?? 0} GP3 GB-Hrs + {io2UsageInPeriod?.usage ? ` / ${io2UsageInPeriod.usage} IO2 GB-Hrs` : ``} +

+
+
+ + {currentBillingCycleSelected ? ( +
+
+

Current disk size per project

+

+ Breakdown of disk per project. Head to your project's disk management section to + see database space used. +

+
+ + {relevantProjects.length === 0 && ( + + +
+

No active projects

+

+ You don't have any active projects in this organization. +

+
+
+
+ )} + + {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 ( +
+
+ + {project.name} + + +
+
+
+ + + + {' '} + GB Disk provisioned + + +

{primaryDiskUsage} GB for Primary Database

+ {replicaDbs.length > 0 && ( + <> +

+ {replicaDiskUsage} GB for {replicaDbs.length} Read{' '} + {replicaDbs.length === 1 ? 'Replica' : 'Replicas'} +

+

+ Read replicas have their own disk and use 25% more disk to account + for WAL files. +

+ + )} +
+
+
+
+ ) + })} +
+ ) : ( + + +
+

Data not available

+

+ Switch to current billing cycle to see current disk size per project. +

+
+
+
+ )} +
+ )} +
+
+ ) +} + +export default DiskUsage diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx index 21f118c093..9c5b29e470 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx @@ -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 = ({ - {categoryMeta.attributes.map((attribute) => ( - 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' ? ( + + ) : ( + x.metric === attribute.key)} + chartMeta={chartMeta} + subscription={subscription} + error={usageError} + isLoading={isLoadingUsage} + isError={isErrorUsage} + isSuccess={isSuccessUsage} + currentBillingCycleSelected={currentBillingCycleSelected} + /> + ) + )} ) } diff --git a/apps/studio/data/projects/keys.ts b/apps/studio/data/projects/keys.ts index e4be8b5a3d..b56dbaf49e 100644 --- a/apps/studio/data/projects/keys.ts +++ b/apps/studio/data/projects/keys.ts @@ -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, } diff --git a/apps/studio/data/projects/org-projects.ts b/apps/studio/data/projects/org-projects.ts new file mode 100644 index 0000000000..f4284e9a45 --- /dev/null +++ b/apps/studio/data/projects/org-projects.ts @@ -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 { + 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> +export type OrgProjectsError = ResponseError + +export const useOrgProjectsQuery = ( + { orgSlug }: OrgProjectsVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + projectKeys.orgProjects(orgSlug), + ({ signal }) => getOrgProjects({ orgSlug }, signal), + { + enabled: enabled && typeof orgSlug !== 'undefined', + ...options, + } + ) diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 63adaeaa46..15d57c796e 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -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 @@ -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: {