mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
feat(storage): break down retained storage on the org usage page
Splits Storage Size into current versus noncurrent objects, and attributes the noncurrent portion to the buckets responsible, so the cost of object versioning sits with the metric it inflates. The split is flag-aware. With the preview off, Storage Size renders exactly the single real series it does today; `USAGE_CATEGORIES` takes an options argument and only emits the two stacked segments when the preview is on. Noncurrent is deliberately not split further into noncurrent versions and soft-deleted files: the platform can't reliably tell the two apart after the fact, and the user's lever for shrinking either is the same lifecycle policy. Using S3's own terms also reads better than Studio-only words like "live". - `storage-retention-usage-query.ts` — org-scoped (the prototype keyed this org-level data under `['projects', undefined, …]`), real `queryOptions` shape with the endpoint stubbed - `StorageRetention.constants.ts` — one definition of the two segments, used by both the chart attributes and the breakdown table, so labels, colors and key casing cannot drift - `StorageRetention.utils.ts` + tests — maps retention days onto chart points - `StorageRetentionBreakdown.tsx` — the segment table, retained-data warning, and per-bucket list, rendered through the existing `additionalInfo` hook - `Usage.colors.ts` — `COLOR_MAP` and `AttributeColor` moved out of `Usage.constants` so the breakdown can read a color token without a cycle back through the module that renders it Known limitation: with the preview on, the stacked chart and the breakdown read zeros until the retention endpoint exists. No mock data is shipped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
795537377b
commit
a87e49685f
13 files changed
+344
-47
No files matched your search
@@ -3,7 +3,8 @@ import { useMemo } from 'react'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { SectionContent } from './SectionContent'
|
||||
import { Attribute, AttributeColor } from './Usage.constants'
|
||||
import type { AttributeColor } from './Usage.colors'
|
||||
import { Attribute } from './Usage.constants'
|
||||
import { dailyUsageToDataPoints } from './Usage.utils'
|
||||
import UsageBarChart from './UsageBarChart'
|
||||
import Panel from '@/components/ui/Panel'
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { toStorageSizeChartData } from './StorageRetention/StorageRetention.utils'
|
||||
import { dailyUsageToDataPoints } from './Usage.utils'
|
||||
import UsageSection from './UsageSection/UsageSection'
|
||||
import { useIsStorageVersioningEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { DataPoint } from '@/data/analytics/constants'
|
||||
import { PricingMetric, type OrgDailyUsageResponse } from '@/data/analytics/org-daily-stats-query'
|
||||
import { storageRetentionUsageQueryOptions } from '@/data/storage/versioning/storage-retention-usage-query'
|
||||
import type { OrgSubscription } from '@/data/subscriptions/types'
|
||||
|
||||
export interface SizeAndCountsProps {
|
||||
@@ -25,17 +30,34 @@ const SizeAndCounts = ({
|
||||
startDate,
|
||||
endDate,
|
||||
}: SizeAndCountsProps) => {
|
||||
const isStorageVersioningEnabled = useIsStorageVersioningEnabled()
|
||||
|
||||
// The daily org stats don't break Storage Size into segments; that split comes
|
||||
// from the retention endpoint.
|
||||
const { data: retention, isPending: isLoadingRetention } = useQuery({
|
||||
...storageRetentionUsageQueryOptions({ orgSlug }),
|
||||
enabled: isStorageVersioningEnabled,
|
||||
})
|
||||
|
||||
const storageSizeChartMeta = isStorageVersioningEnabled
|
||||
? {
|
||||
isLoading: isLoadingRetention,
|
||||
margin: 14,
|
||||
data: toStorageSizeChartData(retention?.daily ?? []),
|
||||
}
|
||||
: {
|
||||
isLoading: isLoadingOrgDailyStats,
|
||||
margin: 14,
|
||||
data: dailyUsageToDataPoints(
|
||||
orgDailyStats,
|
||||
(metric) => metric === PricingMetric.STORAGE_SIZE
|
||||
),
|
||||
}
|
||||
|
||||
const chartMeta: {
|
||||
[key: string]: { data: DataPoint[]; margin: number; isLoading: boolean }
|
||||
} = {
|
||||
[PricingMetric.STORAGE_SIZE]: {
|
||||
isLoading: isLoadingOrgDailyStats,
|
||||
margin: 14,
|
||||
data: dailyUsageToDataPoints(
|
||||
orgDailyStats,
|
||||
(metric) => metric === PricingMetric.STORAGE_SIZE
|
||||
),
|
||||
},
|
||||
[PricingMetric.STORAGE_SIZE]: storageSizeChartMeta,
|
||||
[PricingMetric.DATABASE_SIZE]: {
|
||||
isLoading: isLoadingOrgDailyStats,
|
||||
margin: 14,
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import type { AttributeColor } from '../Usage.colors'
|
||||
import type { StorageRetentionTotals } from '@/data/storage/versioning/storage-retention-usage-query'
|
||||
|
||||
interface StorageSizeSegment {
|
||||
/** The stacked chart series key, matching what the usage endpoint emits. */
|
||||
attributeKey: string
|
||||
/** The corresponding field on the retention totals. */
|
||||
totalsKey: keyof StorageRetentionTotals
|
||||
name: string
|
||||
color: AttributeColor
|
||||
}
|
||||
|
||||
/**
|
||||
* What makes up Storage Size once versioning is in play. Single source of truth
|
||||
* for both the stacked bars and the breakdown table, so they can't drift.
|
||||
*/
|
||||
export const STORAGE_SIZE_SEGMENTS: readonly StorageSizeSegment[] = [
|
||||
{ attributeKey: 'current', totalsKey: 'current', name: 'Current objects', color: 'white' },
|
||||
{
|
||||
attributeKey: 'noncurrent',
|
||||
totalsKey: 'noncurrent',
|
||||
name: 'Noncurrent objects',
|
||||
color: 'yellow',
|
||||
},
|
||||
]
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { STORAGE_SIZE_SEGMENTS } from './StorageRetention.constants'
|
||||
import { toStorageSizeChartData } from './StorageRetention.utils'
|
||||
import type { StorageRetentionDayPoint } from '@/data/storage/versioning/storage-retention-usage-query'
|
||||
|
||||
const day = (overrides: Partial<StorageRetentionDayPoint> = {}): StorageRetentionDayPoint => ({
|
||||
date: '2026-08-01T00:00:00Z',
|
||||
current: 100,
|
||||
noncurrent: 25,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('toStorageSizeChartData', () => {
|
||||
it('returns an empty series for no data, rather than a placeholder point', () => {
|
||||
expect(toStorageSizeChartData([])).toEqual([])
|
||||
})
|
||||
|
||||
it('emits one point per day, in the order given', () => {
|
||||
const points = toStorageSizeChartData([
|
||||
day({ date: '2026-08-01T00:00:00Z' }),
|
||||
day({ date: '2026-08-02T00:00:00Z' }),
|
||||
])
|
||||
expect(points.map((point) => point.period_start)).toEqual([
|
||||
'2026-08-01T00:00:00Z',
|
||||
'2026-08-02T00:00:00Z',
|
||||
])
|
||||
})
|
||||
|
||||
it('maps each retention total onto the chart series key the attribute list declares', () => {
|
||||
// A mismatch here means a stacked bar silently renders as zero, so the keys
|
||||
// must come from the same source the attributes do.
|
||||
const [point] = toStorageSizeChartData([day()])
|
||||
expect(point.current).toBe(100)
|
||||
expect(point.noncurrent).toBe(25)
|
||||
})
|
||||
|
||||
it('emits a series for every declared segment', () => {
|
||||
const [point] = toStorageSizeChartData([day()])
|
||||
for (const segment of STORAGE_SIZE_SEGMENTS) {
|
||||
expect(point, segment.attributeKey).toHaveProperty(segment.attributeKey)
|
||||
}
|
||||
})
|
||||
|
||||
it('formats the axis label as day and short month', () => {
|
||||
const [point] = toStorageSizeChartData([day({ date: '2026-08-09T00:00:00Z' })])
|
||||
expect(point.periodStartFormatted).toBe('09 Aug')
|
||||
})
|
||||
|
||||
it('keeps zeros as zeros so an empty segment still stacks', () => {
|
||||
const [point] = toStorageSizeChartData([day({ noncurrent: 0 })])
|
||||
expect(point.noncurrent).toBe(0)
|
||||
})
|
||||
})
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { STORAGE_SIZE_SEGMENTS } from './StorageRetention.constants'
|
||||
import type { DataPoint } from '@/data/analytics/constants'
|
||||
import type { StorageRetentionDayPoint } from '@/data/storage/versioning/storage-retention-usage-query'
|
||||
|
||||
/**
|
||||
* The retention daily series as `DataPoint[]`, keyed by the same attribute keys
|
||||
* the Storage Size attribute list declares, so every bar has a series to draw.
|
||||
*/
|
||||
export const toStorageSizeChartData = (daily: StorageRetentionDayPoint[]): DataPoint[] =>
|
||||
daily.map((day) => {
|
||||
const segments = Object.fromEntries(
|
||||
STORAGE_SIZE_SEGMENTS.map((segment) => [segment.attributeKey, day[segment.totalsKey]])
|
||||
)
|
||||
|
||||
return {
|
||||
period_start: day.date,
|
||||
periodStartFormatted: dayjs(day.date).format('DD MMM'),
|
||||
...segments,
|
||||
}
|
||||
})
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { cn } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { COLOR_MAP } from '../Usage.colors'
|
||||
import { STORAGE_SIZE_SEGMENTS } from './StorageRetention.constants'
|
||||
import { storageRetentionUsageQueryOptions } from '@/data/storage/versioning/storage-retention-usage-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { formatBytes } from '@/lib/helpers'
|
||||
|
||||
/**
|
||||
* What is driving Storage Size — current objects versus the noncurrent data
|
||||
* versioning retains — attributed to the buckets responsible. Rendered via
|
||||
* `additionalInfo` so the noncurrent cost sits with the metric it inflates.
|
||||
*/
|
||||
export const StorageRetentionBreakdown = () => {
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const { data, isPending, isSuccess } = useQuery(
|
||||
storageRetentionUsageQueryOptions({ orgSlug: organization?.slug })
|
||||
)
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!isSuccess) return null
|
||||
|
||||
const { totals, byBucket } = data
|
||||
const bucketsWithNoncurrentData = byBucket.filter((bucket) => bucket.noncurrent > 0)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="mb-2 text-sm">What’s driving this</p>
|
||||
{STORAGE_SIZE_SEGMENTS.map((segment) => (
|
||||
<div
|
||||
key={segment.attributeKey}
|
||||
className="flex items-center justify-between border-b py-1"
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className={cn('h-3 w-3 rounded-xs', COLOR_MAP[segment.color].marker)} />
|
||||
<p className="text-xs text-foreground-light">{segment.name}</p>
|
||||
</div>
|
||||
<p className="text-xs">{formatBytes(totals[segment.totalsKey])}</p>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<p className="text-xs text-foreground-light">Total</p>
|
||||
<p className="text-xs">{formatBytes(totals.current + totals.noncurrent)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{totals.noncurrent > 0 && (
|
||||
<Admonition
|
||||
type="warning"
|
||||
title={`${formatBytes(totals.noncurrent)} is retained recovery data`}
|
||||
>
|
||||
<p className="text-xs">
|
||||
Noncurrent objects stay billable after an overwrite or delete. They are freed when a
|
||||
lifecycle policy expires them, when the retention limit is reached, or when you delete
|
||||
the version yourself.
|
||||
</p>
|
||||
</Admonition>
|
||||
)}
|
||||
|
||||
{bucketsWithNoncurrentData.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-2 text-sm">Noncurrent data by bucket</p>
|
||||
{bucketsWithNoncurrentData.map((bucket) => (
|
||||
<div key={bucket.bucket} className="flex items-center justify-between border-b py-1">
|
||||
<p className="font-mono text-xs text-foreground-light">{bucket.bucket}</p>
|
||||
<p className="w-16 text-right text-xs">{formatBytes(bucket.noncurrent)}</p>
|
||||
</div>
|
||||
))}
|
||||
<p className="pt-2 text-xs text-foreground-lighter">
|
||||
Retention is set per bucket, in the bucket’s settings.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Separate from `Usage.constants` so modules that only need a color token don't
|
||||
* import the category tree — a cycle for anything `Usage.constants` renders.
|
||||
*/
|
||||
export const COLOR_MAP = {
|
||||
white: { bar: 'fill-foreground', marker: 'bg-foreground' },
|
||||
green: { bar: 'fill-green-800', marker: 'bg-green-800' },
|
||||
'dark-green': { bar: 'fill-green-1000', marker: 'bg-green-1000' },
|
||||
blue: { bar: 'fill-blue-900', marker: 'bg-blue-900' },
|
||||
yellow: { bar: 'fill-yellow-800', marker: 'bg-yellow-800' },
|
||||
'dark-yellow': { bar: 'fill-yellow-1000', marker: 'bg-yellow-1000' },
|
||||
orange: { bar: 'fill-orange-800', marker: 'bg-orange-800' },
|
||||
'dark-orange': { bar: 'fill-orange-1000', marker: 'bg-orange-1100' },
|
||||
teal: { bar: 'fill-teal-600', marker: 'bg-teal-700' },
|
||||
red: { bar: 'fill-red-800', marker: 'bg-red-800' },
|
||||
'dark-red': { bar: 'fill-red-1000', marker: 'bg-red-1000' },
|
||||
purple: { bar: 'fill-purple-900', marker: 'bg-purple-900' },
|
||||
}
|
||||
|
||||
export type AttributeColor = keyof typeof COLOR_MAP
|
||||
@@ -1,47 +1,21 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
import { STORAGE_SIZE_SEGMENTS } from './StorageRetention/StorageRetention.constants'
|
||||
import { StorageRetentionBreakdown } from './StorageRetention/StorageRetentionBreakdown'
|
||||
import type { AttributeColor } from './Usage.colors'
|
||||
import { USAGE_APPROACHING_THRESHOLD } from '@/components/interfaces/Billing/Billing.constants'
|
||||
import { EgressType, PricingMetric } from '@/data/analytics/org-daily-stats-query'
|
||||
import type { OrgSubscription } from '@/data/subscriptions/types'
|
||||
import type { OrgUsageResponse } from '@/data/usage/org-usage-query'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
export const COLOR_MAP = {
|
||||
white: { bar: 'fill-foreground', marker: 'bg-foreground' },
|
||||
green: { bar: 'fill-green-800', marker: 'bg-green-800' },
|
||||
'dark-green': { bar: 'fill-green-1000', marker: 'bg-green-1000' },
|
||||
blue: { bar: 'fill-blue-900', marker: 'bg-blue-900' },
|
||||
yellow: { bar: 'fill-yellow-800', marker: 'bg-yellow-800' },
|
||||
'dark-yellow': { bar: 'fill-yellow-1000', marker: 'bg-yellow-1000' },
|
||||
orange: { bar: 'fill-orange-800', marker: 'bg-orange-800' },
|
||||
'dark-orange': { bar: 'fill-orange-1000', marker: 'bg-orange-1100' },
|
||||
teal: { bar: 'fill-teal-600', marker: 'bg-teal-700' },
|
||||
red: { bar: 'fill-red-800', marker: 'bg-red-800' },
|
||||
'dark-red': { bar: 'fill-red-1000', marker: 'bg-red-1000' },
|
||||
purple: { bar: 'fill-purple-900', marker: 'bg-purple-900' },
|
||||
}
|
||||
|
||||
export const USAGE_STATUS = {
|
||||
NORMAL: 'NORMAL',
|
||||
APPROACHING: 'APPROACHING',
|
||||
EXCEEDED: 'EXCEEDED',
|
||||
}
|
||||
|
||||
export type AttributeColor =
|
||||
| 'white'
|
||||
| 'blue'
|
||||
| 'green'
|
||||
| 'yellow'
|
||||
| 'orange'
|
||||
| 'purple'
|
||||
| 'red'
|
||||
| 'dark-red'
|
||||
| 'dark-orange'
|
||||
| 'dark-yellow'
|
||||
| 'dark-green'
|
||||
| 'teal'
|
||||
|
||||
export interface Attribute {
|
||||
key: string
|
||||
name?: string
|
||||
@@ -73,9 +47,18 @@ export interface CategoryMeta {
|
||||
attributes: CategoryAttribute[]
|
||||
}
|
||||
|
||||
export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[] = (
|
||||
subscription
|
||||
) => {
|
||||
export interface UsageCategoryOptions {
|
||||
/**
|
||||
* On, Storage Size splits into current / noncurrent segments and gains a
|
||||
* retention breakdown. Off, it stays the single real series.
|
||||
*/
|
||||
isStorageVersioningEnabled?: boolean
|
||||
}
|
||||
|
||||
export const USAGE_CATEGORIES: (
|
||||
subscription?: OrgSubscription,
|
||||
options?: UsageCategoryOptions
|
||||
) => CategoryMeta[] = (subscription, { isStorageVersioningEnabled = false } = {}) => {
|
||||
const egressAttributes: CategoryAttribute[] = [
|
||||
{
|
||||
anchor: 'egress',
|
||||
@@ -215,13 +198,23 @@ export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[
|
||||
databaseAndStorageSizeAttributes.push({
|
||||
anchor: 'storageSize',
|
||||
key: PricingMetric.STORAGE_SIZE,
|
||||
attributes: [{ key: PricingMetric.STORAGE_SIZE.toLowerCase(), color: 'white' }],
|
||||
attributes: isStorageVersioningEnabled
|
||||
? STORAGE_SIZE_SEGMENTS.map(({ attributeKey, name, color }) => ({
|
||||
key: attributeKey,
|
||||
name,
|
||||
color,
|
||||
}))
|
||||
: [{ key: PricingMetric.STORAGE_SIZE.toLowerCase(), color: 'white' }],
|
||||
name: 'Storage Size',
|
||||
chartPrefix: 'Average',
|
||||
unit: 'bytes',
|
||||
description:
|
||||
'Sum of all objects in your storage buckets.\nBilling is prorated down to the hour and will be displayed GB-Hrs.',
|
||||
description: isStorageVersioningEnabled
|
||||
? 'Sum of all objects in your storage buckets, including noncurrent objects retained by object versioning.\nBilling is prorated down to the hour and will be displayed GB-Hrs.'
|
||||
: 'Sum of all objects in your storage buckets.\nBilling is prorated down to the hour and will be displayed GB-Hrs.',
|
||||
chartDescription: 'The data refreshes every hour.',
|
||||
...(isStorageVersioningEnabled && {
|
||||
additionalInfo: () => <StorageRetentionBreakdown />,
|
||||
}),
|
||||
links: [
|
||||
{
|
||||
name: 'Storage',
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
} from 'recharts'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { Attribute, COLOR_MAP } from './Usage.constants'
|
||||
import { COLOR_MAP } from './Usage.colors'
|
||||
import { Attribute } from './Usage.constants'
|
||||
import { MultiAttributeTooltipContent, SingleAttributeTooltipContent } from './UsageChartTooltips'
|
||||
import { DataPoint } from '@/data/analytics/constants'
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Payload, ValueType } from 'recharts/types/component/DefaultTooltipContent'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { Attribute, COLOR_MAP } from './Usage.constants'
|
||||
import { COLOR_MAP } from './Usage.colors'
|
||||
import { Attribute } from './Usage.constants'
|
||||
|
||||
export interface SingleAttributeTooltipContentProps {
|
||||
name: string
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CategoryMetaKey, USAGE_CATEGORIES } from '../Usage.constants'
|
||||
import AttributeUsage from './AttributeUsage'
|
||||
import DatabaseSizeUsage from './DatabaseSizeUsage'
|
||||
import { DiskUsage } from './DiskUsage'
|
||||
import { useIsStorageVersioningEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { ScaffoldContainer } from '@/components/layouts/Scaffold'
|
||||
import { DataPoint } from '@/data/analytics/constants'
|
||||
import { PricingMetric } from '@/data/analytics/org-daily-stats-query'
|
||||
@@ -47,7 +48,9 @@ const UsageSection = ({
|
||||
end: !currentBillingCycleSelected && endDate ? new Date(endDate) : undefined,
|
||||
})
|
||||
|
||||
const categoryMeta = USAGE_CATEGORIES(subscription).find(
|
||||
const isStorageVersioningEnabled = useIsStorageVersioningEnabled()
|
||||
|
||||
const categoryMeta = USAGE_CATEGORIES(subscription, { isStorageVersioningEnabled }).find(
|
||||
(category) => category.key === categoryKey
|
||||
)
|
||||
if (!categoryMeta) return null
|
||||
|
||||
@@ -74,6 +74,9 @@ export const storageKeys = {
|
||||
objectName,
|
||||
...(lifecyclePolicy ? [lifecyclePolicy] : []),
|
||||
] as const,
|
||||
/** Org-scoped: storage retention is billed per organization, not per project. */
|
||||
retentionUsage: (orgSlug: string | undefined) =>
|
||||
['organizations', orgSlug, 'storage-retention-usage'] as const,
|
||||
icebergNamespaces: ({ projectRef, warehouse }: { projectRef?: string; warehouse?: string }) =>
|
||||
[projectRef, 'warehouse', warehouse, 'namespaces'] as const,
|
||||
icebergNamespace: ({
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { storageKeys } from '../keys'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
import type { ResponseError } from '@/types'
|
||||
|
||||
/** Storage bytes split by what is keeping them around. All values are bytes. */
|
||||
export interface StorageRetentionTotals {
|
||||
/** The version served when an object is fetched without a version ID. */
|
||||
current: number
|
||||
/**
|
||||
* Everything still billable that isn't the current object: noncurrent versions
|
||||
* plus the empty delete-marker placeholders behind archived files. Not split
|
||||
* further — the platform can't reliably tell the two apart after the fact, and
|
||||
* the user's lever for shrinking either is the same lifecycle policy.
|
||||
*/
|
||||
noncurrent: number
|
||||
}
|
||||
|
||||
export interface StorageRetentionDayPoint extends StorageRetentionTotals {
|
||||
date: string
|
||||
}
|
||||
|
||||
export interface StorageRetentionBucketSummary extends StorageRetentionTotals {
|
||||
bucket: string
|
||||
}
|
||||
|
||||
export interface StorageRetentionUsage {
|
||||
totals: StorageRetentionTotals
|
||||
daily: StorageRetentionDayPoint[]
|
||||
byBucket: StorageRetentionBucketSummary[]
|
||||
}
|
||||
|
||||
export type StorageRetentionUsageVariables = {
|
||||
/** Retention usage is billed per organization, not per project. */
|
||||
orgSlug?: string
|
||||
}
|
||||
|
||||
export type StorageRetentionUsageError = ResponseError
|
||||
|
||||
const EMPTY_USAGE: StorageRetentionUsage = {
|
||||
totals: { current: 0, noncurrent: 0 },
|
||||
daily: [],
|
||||
byBucket: [],
|
||||
}
|
||||
|
||||
async function getStorageRetentionUsage(
|
||||
{ orgSlug }: StorageRetentionUsageVariables,
|
||||
_signal?: AbortSignal
|
||||
): Promise<StorageRetentionUsage> {
|
||||
if (!orgSlug) throw new Error('orgSlug is required')
|
||||
|
||||
// TODO(storage-versioning): replace with the real endpoint once the platform
|
||||
// reports retention usage. The breakdown renders zeros until then.
|
||||
return EMPTY_USAGE
|
||||
}
|
||||
|
||||
export type StorageRetentionUsageData = Awaited<ReturnType<typeof getStorageRetentionUsage>>
|
||||
|
||||
export const storageRetentionUsageQueryOptions = ({ orgSlug }: StorageRetentionUsageVariables) =>
|
||||
queryOptions({
|
||||
queryKey: storageKeys.retentionUsage(orgSlug),
|
||||
queryFn: ({ signal }) => getStorageRetentionUsage({ orgSlug }, signal),
|
||||
enabled: IS_PLATFORM && typeof orgSlug !== 'undefined',
|
||||
})
|
||||
Reference in new issue
Block a user