mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
Bring out infra activity into its own page for org billing v2
This commit is contained in:
1 parent
2f70479665
commit
d97bf81f23
6 files changed
+514
-22
No files matched your search
@@ -50,7 +50,7 @@ const Subscription = ({}: SubscriptionProps) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-b">
|
||||
<div className={`${isOrgBilling ? '' : 'border-b'}`}>
|
||||
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
|
||||
<AddOns />
|
||||
</div>
|
||||
|
||||
@@ -181,8 +181,8 @@ const AttributeUsage = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{projectRef === undefined && (
|
||||
{projectRef === undefined && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between border-b py-1">
|
||||
<p className="text-xs text-scale-1000">
|
||||
Included in {subscription?.plan?.name.toLowerCase()} plan
|
||||
@@ -197,22 +197,19 @@ const AttributeUsage = ({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{currentBillingCycleSelected && (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<p className="text-xs text-scale-1000">
|
||||
{attribute.chartPrefix || 'Used '} in period
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
{attribute.unit === 'bytes'
|
||||
? `${usageMeta?.usage ?? 0} GB`
|
||||
: (usageMeta?.usage ?? 0).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{projectRef === undefined &&
|
||||
currentBillingCycleSelected &&
|
||||
(usageMeta?.pricing_free_units ?? 0) > 0 && (
|
||||
{currentBillingCycleSelected && (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<p className="text-xs text-scale-1000">
|
||||
{attribute.chartPrefix || 'Used '} in period
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
{attribute.unit === 'bytes'
|
||||
? `${usageMeta?.usage ?? 0} GB`
|
||||
: (usageMeta?.usage ?? 0).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{currentBillingCycleSelected && (usageMeta?.pricing_free_units ?? 0) > 0 && (
|
||||
<div className="flex items-center justify-between border-t py-1">
|
||||
<p className="text-xs text-scale-1000">Overage in period</p>
|
||||
<p className="text-xs">
|
||||
@@ -224,7 +221,8 @@ const AttributeUsage = ({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{attribute.additionalInfo?.(usage)}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// [Joshen TODO] Please clean up - a lot of repeated code from other files here
|
||||
// all of these interfaces, see what can be reused
|
||||
|
||||
// [Joshen TODO] Remove any dependency on BillingV2 stuff so that we can deprecate easily
|
||||
|
||||
export interface Attribute {
|
||||
key: string
|
||||
name?: string
|
||||
color: 'white' | 'blue' | 'green' | 'yellow' | 'orange'
|
||||
}
|
||||
export interface CategoryAttribute {
|
||||
anchor: string
|
||||
key: string // Property from organization usage
|
||||
attributes: Attribute[] // For querying against stats-daily / infra-monitoring
|
||||
name: string
|
||||
unit: 'bytes' | 'absolute' | 'percentage'
|
||||
links: {
|
||||
name: string
|
||||
url: string
|
||||
}[]
|
||||
description: string
|
||||
chartPrefix?: 'Max' | 'Average'
|
||||
chartDescription: string
|
||||
}
|
||||
|
||||
export interface CategoryMeta {
|
||||
key: string
|
||||
name: string
|
||||
description: string
|
||||
attributes: CategoryAttribute[]
|
||||
}
|
||||
|
||||
export const INFRA_ACTIVITY_METRICS: CategoryMeta[] = [
|
||||
{
|
||||
key: 'infra',
|
||||
name: 'Infrastructure',
|
||||
description: 'Usage statistics related to your server instance',
|
||||
attributes: [
|
||||
{
|
||||
anchor: 'cpu',
|
||||
key: 'max_cpu_usage',
|
||||
attributes: [{ key: 'max_cpu_usage', color: 'white' }],
|
||||
name: 'CPU',
|
||||
unit: 'percentage',
|
||||
description: 'Max CPU usage of your server',
|
||||
chartDescription: '',
|
||||
links: [
|
||||
{
|
||||
name: 'Compute Add-Ons',
|
||||
url: 'https://supabase.com/docs/guides/platform/compute-add-ons',
|
||||
},
|
||||
{ name: 'High CPU Usage', url: 'https://supabase.com/docs/guides/platform/exhaust-cpu' },
|
||||
{
|
||||
name: 'Metrics',
|
||||
url: 'https://supabase.com/docs/guides/platform/metrics',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
anchor: 'ram',
|
||||
key: 'ram_usage',
|
||||
attributes: [{ key: 'ram_usage', color: 'white' }],
|
||||
name: 'Memory',
|
||||
unit: 'percentage',
|
||||
description:
|
||||
'Memory usage of your server.\nYou might observe elevated memory usage, even with little to no load. Besides Postgres, a wide range of services is running under the hood resulting in an elevated base memory usage.',
|
||||
chartDescription: '',
|
||||
links: [
|
||||
{
|
||||
name: 'Compute Add-Ons',
|
||||
url: 'https://supabase.com/docs/guides/platform/compute-add-ons',
|
||||
},
|
||||
{ name: 'High RAM Usage', url: 'https://supabase.com/docs/guides/platform/exhaust-ram' },
|
||||
{
|
||||
name: 'Metrics',
|
||||
url: 'https://supabase.com/docs/guides/platform/metrics',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
anchor: 'disk_io',
|
||||
key: 'disk_io_consumption',
|
||||
attributes: [{ key: 'disk_io_consumption', color: 'white' }],
|
||||
name: 'Disk IO Bandwidth',
|
||||
unit: 'percentage',
|
||||
links: [
|
||||
{
|
||||
name: 'Disk Throughput and IOPS',
|
||||
url: 'https://supabase.com/docs/guides/platform/compute-add-ons#disk-throughput-and-iops',
|
||||
},
|
||||
{
|
||||
name: 'Metrics',
|
||||
url: 'https://supabase.com/docs/guides/platform/metrics',
|
||||
},
|
||||
],
|
||||
description:
|
||||
'The disk performance of your workload is determined by the Disk IO bandwidth.\nSmaller compute instances (below 4XL) can burst up to their largest throughput and IOPS for 30 minutes in a day. Beyond that, the performance reverts to the baseline. Your disk budget gets replenished throughout the day.',
|
||||
chartDescription: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,379 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Link from 'next/link'
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDivider,
|
||||
ScaffoldSection,
|
||||
ScaffoldSectionContent,
|
||||
ScaffoldSectionDetail,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
|
||||
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 { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
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 SectionHeader from '../../Organization/Usage/SectionHeader'
|
||||
import UsageBarChart from '../../Organization/Usage/UsageBarChart'
|
||||
import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants'
|
||||
|
||||
const InfrastructureActivity = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const organization = useSelectedOrganization()
|
||||
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: addons, isLoading } = useProjectAddonsQuery({ projectRef })
|
||||
const selectedAddons = addons?.selected_addons ?? []
|
||||
const { computeInstance } = getAddons(selectedAddons)
|
||||
const currentComputeInstanceSpecs = computeInstance?.variant?.meta ?? {
|
||||
baseline_disk_io_mbs: 87,
|
||||
max_disk_io_mbs: 2085,
|
||||
cpu_cores: 2,
|
||||
cpu_dedicated: true,
|
||||
memory_gb: 1,
|
||||
}
|
||||
|
||||
const startDate = useMemo(() => {
|
||||
// If end date is in future, set end date to now
|
||||
if (!dateRange?.period_start?.date) {
|
||||
return undefined
|
||||
} else {
|
||||
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
||||
return new Date(dateRange?.period_start?.date).toISOString().slice(0, -5) + 'Z'
|
||||
}
|
||||
}, [dateRange, subscription])
|
||||
|
||||
const endDate = useMemo(() => {
|
||||
// If end date is in future, set end date to end of current day
|
||||
if (dateRange?.period_end?.date && dayjs(dateRange.period_end.date).isAfter(dayjs())) {
|
||||
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
||||
// In order to have full days from Prometheus metrics when using 1d interval,
|
||||
// the time needs to be greater or equal than the time of the start date
|
||||
return dayjs().endOf('day').toISOString().slice(0, -5) + 'Z'
|
||||
} else if (dateRange?.period_end?.date) {
|
||||
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
||||
return new Date(dateRange.period_end.date).toISOString().slice(0, -5) + 'Z'
|
||||
}
|
||||
}, [dateRange, subscription])
|
||||
|
||||
// Switch to hourly interval, if the timeframe is <48 hours
|
||||
let interval: '1d' | '1h' = '1d'
|
||||
let dateFormat = 'DD MMM'
|
||||
if (startDate && endDate) {
|
||||
const diffInHours = dayjs(endDate).diff(startDate, 'hours')
|
||||
|
||||
if (diffInHours <= 48) {
|
||||
interval = '1h'
|
||||
dateFormat = 'h a'
|
||||
}
|
||||
}
|
||||
|
||||
const { data: cpuUsageData, isLoading: isLoadingCpuUsageData } = useInfraMonitoringQuery({
|
||||
projectRef,
|
||||
attribute: 'max_cpu_usage',
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
dateFormat,
|
||||
})
|
||||
|
||||
const { data: memoryUsageData, isLoading: isLoadingMemoryUsageData } = useInfraMonitoringQuery({
|
||||
projectRef,
|
||||
attribute: 'ram_usage',
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
dateFormat,
|
||||
})
|
||||
|
||||
const { data: ioBudgetData, isLoading: isLoadingIoBudgetData } = useInfraMonitoringQuery({
|
||||
projectRef,
|
||||
attribute: 'disk_io_consumption',
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
dateFormat,
|
||||
})
|
||||
|
||||
const highestIoBudgetConsumption = Math.max(
|
||||
...(ioBudgetData?.data || []).map((x) => Number(x.disk_io_consumption) ?? 0),
|
||||
0
|
||||
)
|
||||
|
||||
const chartMeta: { [key: string]: { data: DataPoint[]; isLoading: boolean } } = {
|
||||
max_cpu_usage: {
|
||||
isLoading: isLoadingCpuUsageData,
|
||||
data: cpuUsageData?.data ?? [],
|
||||
},
|
||||
ram_usage: {
|
||||
isLoading: isLoadingMemoryUsageData,
|
||||
data: memoryUsageData?.data ?? [],
|
||||
},
|
||||
disk_io_consumption: {
|
||||
isLoading: isLoadingIoBudgetData,
|
||||
data: ioBudgetData?.data ?? [],
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<SectionHeader
|
||||
title="Infrastructure Activity"
|
||||
description="Activity statistics related to your server instance"
|
||||
/>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldDivider />
|
||||
<ScaffoldContainer className="sticky top-0 py-6 border-b bg-scale-200 z-10">
|
||||
<div className="flex items-center space-x-4">
|
||||
{!isLoadingSubscription && (
|
||||
<>
|
||||
<DateRangePicker
|
||||
id="billingCycle"
|
||||
name="billingCycle"
|
||||
onChange={setDateRange}
|
||||
value={TIME_PERIODS_BILLING[0].key}
|
||||
options={[...TIME_PERIODS_BILLING, ...TIME_PERIODS_REPORTS]}
|
||||
loading={isLoadingSubscription}
|
||||
currentBillingPeriodStart={subscription?.current_period_start}
|
||||
className="!w-[200px]"
|
||||
/>
|
||||
<p className="text-sm text-scale-1000">
|
||||
{dayjs(startDate).format('DD MMM YYYY')} - {dayjs(endDate).format('DD MMM YYYY')}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ScaffoldContainer>
|
||||
|
||||
{categoryMeta?.attributes.map((attribute) => {
|
||||
const chartData = chartMeta[attribute.key]?.data ?? []
|
||||
|
||||
return (
|
||||
<Fragment key={attribute.key}>
|
||||
<ScaffoldContainer id={attribute.anchor}>
|
||||
<ScaffoldSection>
|
||||
<ScaffoldSectionDetail>
|
||||
<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>
|
||||
</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">
|
||||
{value}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-scale-1200 dark:text-scale-1100 mb-2">
|
||||
More information
|
||||
</p>
|
||||
{attribute.links.map((link) => (
|
||||
<div key={link.url}>
|
||||
<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>
|
||||
<IconExternalLink size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScaffoldSectionDetail>
|
||||
<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}
|
||||
<div className="space-y-1">
|
||||
<p>Disk IO Bandwidth</p>
|
||||
|
||||
{currentComputeInstanceSpecs.baseline_disk_io_mbs ===
|
||||
currentComputeInstanceSpecs.max_disk_io_mbs ? (
|
||||
<p className="text-sm text-scale-1000">
|
||||
Your current compute can has a baseline and maximum disk throughput of{' '}
|
||||
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-scale-1000">
|
||||
Your current compute can burst up to{' '}
|
||||
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps for
|
||||
30 minutes a day and reverts to the baseline performance of{' '}
|
||||
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
|
||||
Mbps.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm mb-2">Overview</p>
|
||||
<div className="flex items-center justify-between border-b py-1">
|
||||
<p className="text-xs text-scale-1000">Current compute instance</p>
|
||||
<p className="text-xs">{computeInstance?.variant?.name ?? 'Micro'}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b py-1">
|
||||
<p className="text-xs text-scale-1000">
|
||||
Maximum IO Bandwidth (burst limit)
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b py-1">
|
||||
<p className="text-xs text-scale-1000">Baseline IO Bandwidth</p>
|
||||
<p className="text-xs">
|
||||
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
|
||||
Mbps
|
||||
</p>
|
||||
</div>
|
||||
{currentComputeInstanceSpecs.max_disk_io_mbs !==
|
||||
currentComputeInstanceSpecs?.baseline_disk_io_mbs && (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<p className="text-xs text-scale-1000">Daily burst time limit</p>
|
||||
<p className="text-xs">30 mins</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-row justify-between">
|
||||
{attribute.key === 'disk_io_consumption' ? (
|
||||
<p>Disk IO consumed per {interval === '1d' ? 'day' : 'hour'}</p>
|
||||
) : (
|
||||
<p>
|
||||
Max{' '}
|
||||
<span className={attribute.key === 'ram_usage' ? 'lowercase' : ''}>
|
||||
{attribute.name}
|
||||
</span>{' '}
|
||||
utilization per {interval === '1d' ? 'day' : 'hour'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{attribute.key === 'ram_usage' && (
|
||||
<div className="text-sm text-scale-1000">
|
||||
<p>
|
||||
Your compute instance has {currentComputeInstanceSpecs.memory_gb} GB of
|
||||
memory.
|
||||
</p>
|
||||
{currentComputeInstanceSpecs.memory_gb === 1 && (
|
||||
<p>
|
||||
As your project is running on the smallest compute instance, it is not
|
||||
unusual for your project to have a base memory usage of ~50%.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attribute.key === 'max_cpu_usage' && (
|
||||
<p className="text-sm text-scale-1000">
|
||||
Your compute instance has {currentComputeInstanceSpecs.cpu_cores} CPU cores.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{attribute.chartDescription.split('\n').map((paragraph, idx) => (
|
||||
<p key={`para-${idx}`} className="text-sm text-scale-1000">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{chartMeta[attribute.key].isLoading ? (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
) : chartData.length ? (
|
||||
<UsageBarChart
|
||||
name={`${attribute.chartPrefix || ''}${attribute.name}`}
|
||||
unit={attribute.unit}
|
||||
attributes={attribute.attributes}
|
||||
data={chartData}
|
||||
yFormatter={(value) => `${Math.round(Number(value))}%`}
|
||||
tooltipFormatter={(value) => `${value}%`}
|
||||
yLimit={100}
|
||||
/>
|
||||
) : (
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="flex flex-col items-center justify-center space-y-2">
|
||||
<IconBarChart2 className="text-scale-1100 mb-2" />
|
||||
<p className="text-sm">No data in period</p>
|
||||
<p className="text-sm text-scale-1000">May take a few minutes to show</p>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
)}
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldDivider />
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfrastructureActivity
|
||||
@@ -107,9 +107,9 @@ export const generateSettingsMenu = (
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Usage',
|
||||
name: 'Infrastructure',
|
||||
key: 'usage',
|
||||
url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/billing/usage`,
|
||||
url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/infrastructure`,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import InfrastructureActivity from 'components/interfaces/Settings/Infrastructure/InfrastructureActivity'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const ProjectInfrastructure: NextPageWithLayout = () => {
|
||||
return <InfrastructureActivity />
|
||||
}
|
||||
|
||||
ProjectInfrastructure.getLayout = (page) => (
|
||||
<SettingsLayout title="Infrastructure">{page}</SettingsLayout>
|
||||
)
|
||||
|
||||
export default ProjectInfrastructure
|
||||
Reference in new issue
Block a user