diff --git a/studio/components/interfaces/BillingV2/Usage/BarChart.tsx b/studio/components/interfaces/BillingV2/Usage/BarChart.tsx
new file mode 100644
index 00000000000..ee0f627ff85
--- /dev/null
+++ b/studio/components/interfaces/BillingV2/Usage/BarChart.tsx
@@ -0,0 +1,62 @@
+import { DataPoint } from 'data/analytics/constants'
+import {
+ ResponsiveContainer,
+ ComposedChart,
+ CartesianGrid,
+ YAxis,
+ XAxis,
+ Bar,
+ ReferenceLine,
+} from 'recharts'
+
+// [Joshen] I don't think I should be replacing ChartHandler just yet as the styling of the usage charts
+// are slightly different from that on the other pages, just in case.
+
+interface Reference {
+ value: number
+ label: string
+ width: number
+ height: number
+ x: number
+ y: number
+}
+
+export interface BarChartProps {
+ data: DataPoint[]
+ attribute: string
+ reference?: Reference
+}
+
+const BarChart = ({ data, attribute, reference }: BarChartProps) => {
+ return (
+
+
+
+
+
+
+
+ {reference && (
+
+
+ {reference.label}
+
+
+ }
+ />
+ )}
+
+
+
+ )
+}
+
+export default BarChart
diff --git a/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx b/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx
new file mode 100644
index 00000000000..7ecea76d216
--- /dev/null
+++ b/studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx
@@ -0,0 +1,71 @@
+import { useParams } from 'common'
+import ShimmeringLoader from 'components/ui/ShimmeringLoader'
+import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
+import dayjs from 'dayjs'
+import { DATE_FORMAT } from 'lib/constants'
+import BarChart from './BarChart'
+import { generateUsageData } from './Usage.utils'
+
+const Infrastructure = () => {
+ const { ref } = useParams()
+
+ const currentDate = 1682669326710 / 1000
+ const startDate = dayjs.unix(currentDate).subtract(24, 'hour').format(DATE_FORMAT)
+ const endDate = dayjs.unix(currentDate).format(DATE_FORMAT)
+
+ const { data: ioBudgetData, isLoading: isLoadingIoBudgetData } = useInfraMonitoringQuery({
+ projectRef: ref,
+ attribute: 'disk_io_budget',
+ startDate,
+ endDate,
+ interval: '1h',
+ })
+
+ return (
+ <>
+
+
+
+
+
+
IO Budget
+
Some description here
+
+
+
+
+
IO budget per day
+
+ Just FYI for now its a percentage for IO budget, which is different from what the
+ figma designs are (in minutes). We will need o11y or infra to help update this
+
+
+ {isLoadingIoBudgetData ? (
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ >
+ )
+}
+
+export default Infrastructure
diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.tsx b/studio/components/interfaces/BillingV2/Usage/Usage.tsx
new file mode 100644
index 00000000000..45fd184500a
--- /dev/null
+++ b/studio/components/interfaces/BillingV2/Usage/Usage.tsx
@@ -0,0 +1,95 @@
+import { useParams } from 'common'
+import ShimmeringLoader from 'components/ui/ShimmeringLoader'
+import { useProjectsQuery } from 'data/projects/projects-query'
+import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
+import { useEffect, useState } from 'react'
+import { Button, IconCheckCircle, Listbox } from 'ui'
+import Infrastructure from './Infrastructure'
+
+const Usage = () => {
+ const { ref } = useParams()
+ const [selectedProjectRef, setSelectedProjectRef] = useState(ref as string)
+
+ const { data: projects, isLoading, isSuccess } = useProjectsQuery()
+ const { data: subscription } = useProjectSubscriptionQuery({ projectRef: ref })
+
+ useEffect(() => {}, [isSuccess])
+
+ return (
+ <>
+
+
+
Usage
+
+
+
+ {/*
*/}
+ {isLoading ? (
+
+ ) : (
+
{}} className="w-[200px]">
+ {(projects ?? []).map((project) => (
+
+ {project.name}
+
+ ))}
+
+ )}
+ {subscription !== undefined && (
+
Project is on {subscription.tier.name}
+ )}
+
+
+ View invoices
+ View billing
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default Usage
diff --git a/studio/components/interfaces/BillingV2/Usage/Usage.utils.ts b/studio/components/interfaces/BillingV2/Usage/Usage.utils.ts
new file mode 100644
index 00000000000..9406c4fbcce
--- /dev/null
+++ b/studio/components/interfaces/BillingV2/Usage/Usage.utils.ts
@@ -0,0 +1,13 @@
+import { DataPoint } from 'data/analytics/constants'
+
+// [Joshen] This is just for development
+export const generateUsageData = (attribute: string, days: number): DataPoint[] => {
+ const tempArray = new Array(days).fill(0)
+ return tempArray.map((x, idx) => {
+ return {
+ loopId: (idx + 1).toString(),
+ period_start: '',
+ [attribute]: Math.floor(Math.random() * 100).toString(),
+ }
+ })
+}
diff --git a/studio/data/analytics/constants.ts b/studio/data/analytics/constants.ts
new file mode 100644
index 00000000000..ef173509814
--- /dev/null
+++ b/studio/data/analytics/constants.ts
@@ -0,0 +1,15 @@
+export type DataPoint = {
+ loopId: number | string
+ period_start: string
+} & {
+ // Attribute name will be the key
+ [key: string]: string
+}
+
+export interface AnalyticsData {
+ data: DataPoint[]
+ format: string
+ total: number
+ totalAverage: number
+ yAxisLimit: number
+}
diff --git a/studio/data/analytics/daily-stats-query.ts b/studio/data/analytics/daily-stats-query.ts
new file mode 100644
index 00000000000..70bac3c155b
--- /dev/null
+++ b/studio/data/analytics/daily-stats-query.ts
@@ -0,0 +1,107 @@
+import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
+import { get } from 'lib/common/fetch'
+import { API_URL } from 'lib/constants'
+import { useCallback } from 'react'
+import { analyticsKeys } from './keys'
+import { AnalyticsData } from './constants'
+
+export type DailyStatsVariables = {
+ projectRef?: string
+ attribute?:
+ | 'total_realtime_egress'
+ | 'total_realtime_requests'
+ | 'total_realtime_ingress'
+ | 'total_rest_ingress'
+ | 'total_rest_egress'
+ | 'total_rest_requests'
+ | 'total_rest_get_requests'
+ | 'total_rest_post_requests'
+ | 'total_rest_patch_requests'
+ | 'total_rest_delete_requests'
+ | 'total_rest_options_requests'
+ | 'total_auth_billing_period_mau'
+ | 'total_auth_billing_period_sso_mau'
+ | 'total_auth_ingress'
+ | 'total_auth_egress'
+ | 'total_auth_requests'
+ | 'total_auth_get_requests'
+ | 'total_auth_post_requests'
+ | 'total_auth_patch_requests'
+ | 'total_auth_delete_requests'
+ | 'total_auth_options_requests'
+ | 'total_storage_ingress'
+ | 'total_storage_egress'
+ | 'total_storage_image_render_count'
+ | 'total_storage_requests'
+ | 'total_storage_get_requests'
+ | 'total_storage_post_requests'
+ | 'total_storage_patch_requests'
+ | 'total_storage_delete_requests'
+ | 'total_storage_options_requests'
+ | 'total_ingress'
+ | 'total_egress'
+ | 'total_requests'
+ | 'total_get_requests'
+ | 'total_post_requests'
+ | 'total_patch_requests'
+ | 'total_delete_requests'
+ | 'total_options_requests'
+ startDate?: string
+ endDate?: string
+ interval?: string
+}
+
+export async function getDailyStats(
+ { projectRef, attribute, startDate, endDate, interval = '1d' }: DailyStatsVariables,
+ signal?: AbortSignal
+) {
+ if (!projectRef) throw new Error('Project ref is required')
+ if (!attribute) throw new Error('Attribute is required')
+ if (!startDate) throw new Error('Start date is required')
+ if (!endDate) throw new Error('Start date is required')
+
+ const data = await get(
+ `${API_URL}/projects/${projectRef}/daily-stats?attribute=${attribute}&startDate=${encodeURIComponent(
+ startDate
+ )}&endDate=${encodeURIComponent(endDate)}&interval=${interval}`,
+ { signal }
+ )
+ if (data.error) throw data.error
+ return data as AnalyticsData
+}
+
+export type DailyStatsData = Awaited>
+export type DailyStatsError = unknown
+
+export const useDailyStatsQuery = (
+ { projectRef, attribute, startDate, endDate, interval = '1d' }: DailyStatsVariables,
+ { enabled = true, ...options }: UseQueryOptions = {}
+) =>
+ useQuery(
+ analyticsKeys.dailyStats(projectRef, { attribute, startDate, endDate, interval }),
+ ({ signal }) => getDailyStats({ projectRef, attribute, startDate, endDate, interval }, signal),
+ {
+ enabled: enabled && typeof projectRef !== 'undefined',
+ ...options,
+ }
+ )
+
+export const useDailyStatsPrefetch = ({
+ projectRef,
+ attribute,
+ startDate,
+ endDate,
+ interval = '1d',
+}: DailyStatsVariables) => {
+ const client = useQueryClient()
+
+ return useCallback(() => {
+ if (projectRef && attribute && startDate && endDate && interval) {
+ client.prefetchQuery(
+ analyticsKeys.dailyStats(projectRef, { attribute, startDate, endDate, interval }),
+ ({ signal }) =>
+ getDailyStats({ projectRef, attribute, startDate, endDate, interval }, signal)
+ )
+ }
+ }, [projectRef])
+}
diff --git a/studio/data/analytics/infra-monitoring-query.ts b/studio/data/analytics/infra-monitoring-query.ts
new file mode 100644
index 00000000000..c9bdf9a42fb
--- /dev/null
+++ b/studio/data/analytics/infra-monitoring-query.ts
@@ -0,0 +1,73 @@
+import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
+import { get } from 'lib/common/fetch'
+import { API_URL } from 'lib/constants'
+import { useCallback } from 'react'
+import { analyticsKeys } from './keys'
+import { AnalyticsData } from './constants'
+
+export type InfraMonitoringVariables = {
+ projectRef?: string
+ attribute?: 'cpu_usage' | 'disk_io_budget' | 'ram_usage'
+ startDate?: string
+ endDate?: string
+ interval?: string
+}
+
+export async function getInfraMonitoring(
+ { projectRef, attribute, startDate, endDate, interval = '1d' }: InfraMonitoringVariables,
+ signal?: AbortSignal
+) {
+ if (!projectRef) throw new Error('Project ref is required')
+ if (!attribute) throw new Error('Attribute is required')
+ if (!startDate) throw new Error('Start date is required')
+ if (!endDate) throw new Error('Start date is required')
+
+ const data = await get(
+ `${API_URL}/projects/${projectRef}/infra-monitoring?attribute=${attribute}&startDate=${encodeURIComponent(
+ startDate
+ )}&endDate=${encodeURIComponent(endDate)}&interval=${interval}`,
+ { signal }
+ )
+ if (data.error) throw data.error
+ return data as AnalyticsData
+}
+
+export type InfraMonitoringData = Awaited>
+export type InfraMonitoringError = unknown
+
+export const useInfraMonitoringQuery = (
+ { projectRef, attribute, startDate, endDate, interval = '1d' }: InfraMonitoringVariables,
+ {
+ enabled = true,
+ ...options
+ }: UseQueryOptions = {}
+) =>
+ useQuery(
+ analyticsKeys.dailyStats(projectRef, { attribute, startDate, endDate, interval }),
+ ({ signal }) =>
+ getInfraMonitoring({ projectRef, attribute, startDate, endDate, interval }, signal),
+ {
+ enabled: enabled && typeof projectRef !== 'undefined',
+ ...options,
+ }
+ )
+
+export const useInfraMonitoringPrefetch = ({
+ projectRef,
+ attribute,
+ startDate,
+ endDate,
+ interval = '1d',
+}: InfraMonitoringVariables) => {
+ const client = useQueryClient()
+
+ return useCallback(() => {
+ if (projectRef && attribute && startDate && endDate && interval) {
+ client.prefetchQuery(
+ analyticsKeys.dailyStats(projectRef, { attribute, startDate, endDate, interval }),
+ ({ signal }) =>
+ getInfraMonitoring({ projectRef, attribute, startDate, endDate, interval }, signal)
+ )
+ }
+ }, [projectRef])
+}
diff --git a/studio/data/analytics/keys.ts b/studio/data/analytics/keys.ts
index b360a8f7132..049d7d0a0a0 100644
--- a/studio/data/analytics/keys.ts
+++ b/studio/data/analytics/keys.ts
@@ -5,4 +5,29 @@ export const analyticsKeys = {
projectRef: string | undefined,
{ interval, functionId }: { functionId: string | undefined; interval: string | undefined }
) => ['projects', projectRef, 'functions-inv-stats', { interval, functionId }] as const,
+ dailyStats: (
+ projectRef: string | undefined,
+ {
+ attribute,
+ startDate,
+ endDate,
+ interval,
+ }: { attribute?: string; startDate?: string; endDate?: string; interval?: string }
+ ) =>
+ ['projects', projectRef, 'daily-stats', { attribute, startDate, endDate, interval }] as const,
+ infraMonitoring: (
+ projectRef: string | undefined,
+ {
+ attribute,
+ startDate,
+ endDate,
+ interval,
+ }: { attribute?: string; startDate?: string; endDate?: string; interval?: string }
+ ) =>
+ [
+ 'projects',
+ projectRef,
+ 'infra-monitoring',
+ { attribute, startDate, endDate, interval },
+ ] as const,
}
diff --git a/studio/data/projects/keys.ts b/studio/data/projects/keys.ts
new file mode 100644
index 00000000000..7a3d357e9c6
--- /dev/null
+++ b/studio/data/projects/keys.ts
@@ -0,0 +1,4 @@
+export const projectKeys = {
+ list: () => ['projects'] as const,
+ detail: (projectRef: string | undefined) => ['projects', projectRef, 'detail'] as const,
+}
diff --git a/studio/data/projects/project-detail-query.ts b/studio/data/projects/project-detail-query.ts
new file mode 100644
index 00000000000..552ede409d3
--- /dev/null
+++ b/studio/data/projects/project-detail-query.ts
@@ -0,0 +1,43 @@
+import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
+import { get } from 'lib/common/fetch'
+import { API_URL } from 'lib/constants'
+import { useCallback } from 'react'
+import { Project } from 'types'
+import { projectKeys } from './keys'
+
+export type ProjectDetailVariables = { ref?: string }
+
+export async function getProjectDetail({ ref }: ProjectDetailVariables, signal?: AbortSignal) {
+ if (!ref) throw new Error('Project ref is required')
+ const data = await get(`${API_URL}/projects/${ref}`, { signal })
+ if (data.error) throw data.error
+ return data as Project
+}
+
+export type ProjectDetailData = Awaited>
+export type ProjectDetailError = unknown
+
+export const useProjectDetailQuery = (
+ { ref }: ProjectDetailVariables,
+ { enabled = true, ...options }: UseQueryOptions = {}
+) =>
+ useQuery(
+ projectKeys.detail(ref),
+ ({ signal }) => getProjectDetail({ ref }, signal),
+ {
+ enabled: enabled && typeof ref !== 'undefined',
+ ...options,
+ }
+ )
+
+export const useProjectDetailPrefetch = ({ ref }: ProjectDetailVariables) => {
+ const client = useQueryClient()
+
+ return useCallback(() => {
+ if (ref) {
+ client.prefetchQuery(projectKeys.detail(ref), ({ signal }) =>
+ getProjectDetail({ ref }, signal)
+ )
+ }
+ }, [ref])
+}
diff --git a/studio/data/projects/projects-query.ts b/studio/data/projects/projects-query.ts
new file mode 100644
index 00000000000..a60dc03b378
--- /dev/null
+++ b/studio/data/projects/projects-query.ts
@@ -0,0 +1,37 @@
+import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
+import { get } from 'lib/common/fetch'
+import { API_URL } from 'lib/constants'
+import { useCallback } from 'react'
+import { Project } from 'types'
+import { projectKeys } from './keys'
+
+export type ProjectsVariables = {
+ ref?: string
+}
+
+export async function getProjects(signal?: AbortSignal) {
+ const data = await get(`${API_URL}/projects`, { signal })
+ if (data.error) throw data.error
+ return data as Project[]
+}
+
+export type OrganizationRolesData = Awaited>
+export type OrganizationRolesError = unknown
+
+export const useProjectsQuery = ({
+ enabled = true,
+ ...options
+}: UseQueryOptions = {}) =>
+ useQuery(
+ projectKeys.list(),
+ ({ signal }) => getProjects(signal),
+ { enabled: enabled, ...options }
+ )
+
+export const useProjectsPrefetch = () => {
+ const client = useQueryClient()
+
+ return useCallback(() => {
+ client.prefetchQuery(projectKeys.list(), ({ signal }) => getProjects(signal))
+ }, [])
+}
diff --git a/studio/pages/project/[ref]/settings/billing/usage.tsx b/studio/pages/project/[ref]/settings/billing/usage.tsx
index 0d025264da9..94d6c7d5db0 100644
--- a/studio/pages/project/[ref]/settings/billing/usage.tsx
+++ b/studio/pages/project/[ref]/settings/billing/usage.tsx
@@ -12,9 +12,12 @@ import LoadingUI from 'components/ui/Loading'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import { PAYGUsage } from 'components/interfaces/Billing'
import ProjectUsageBars from 'components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars'
+import Usage from 'components/interfaces/BillingV2/Usage/Usage'
const ProjectBillingUsage: NextPageWithLayout = () => {
- const showUsageV2 = useFlag('usageV2')
+ const showNewUsageUI = useFlag('usagev2')
+
+ if (showNewUsageUI) return
return (
diff --git a/studio/styles/ui.scss b/studio/styles/ui.scss
index 12e09fa2017..54b9fc584d4 100644
--- a/studio/styles/ui.scss
+++ b/studio/styles/ui.scss
@@ -438,3 +438,17 @@
@apply stroke-green-500;
}
}
+
+.recharts-cartesian-axis-tick {
+ text {
+ @apply text-xs;
+ }
+}
+.recharts-reference-line {
+ line {
+ @apply stroke-amber-900;
+ }
+ text {
+ @apply text-xs fill-amber-900 bg-amber-600;
+ }
+}
\ No newline at end of file