-
Request body
+
Request Metadata
- Response{method ? ` ${method}` : null} body
+ Response Metadata
{
const { ui } = useStore()
const router = useRouter()
const { ref: projectRef } = useParams()
const ongoingIncident = useFlag('ongoingIncident')
const projectName = ui.selectedProject?.name
+ const organizationName = ui.selectedOrganization?.name
const isPaused = ui.selectedProject?.status === PROJECT_STATUS.INACTIVE
const ignorePausedState =
@@ -52,7 +55,15 @@ const ProjectLayout = ({
- {title ? `${title} | Supabase` : projectName ? `${projectName} | Supabase` : 'Supabase'}
+ {title
+ ? `${title} | Supabase`
+ : selectedTable
+ ? `${selectedTable} | ${projectName} | ${organizationName} | Supabase`
+ : projectName
+ ? `${projectName} | ${organizationName} | Supabase`
+ : organizationName
+ ? `${organizationName} | Supabase`
+ : 'Supabase'}
@@ -128,6 +139,8 @@ const ContentWrapper = observer(({ isLoading, children }: ContentWrapperProps) =
'/project/[ref]/settings/general',
'/project/[ref]/settings/database',
'/project/[ref]/settings/billing/subscription',
+ '/project/[ref]/settings/billing/usage',
+ '/project/[ref]/settings/billing/invoices',
'/project/[ref]/settings/billing/update',
'/project/[ref]/settings/billing/update/free',
'/project/[ref]/settings/billing/update/pro',
diff --git a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx
index af9d3c9d067..60143db8f77 100644
--- a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx
+++ b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx
@@ -19,6 +19,7 @@ import useTableRowsPrefetchWrapper from './TableEditorLayout.utils'
export interface TableEditorLayoutProps {
selectedSchema?: string
+ selectedTable?: string
onSelectSchema: (schema: string) => void
onAddTable: () => void
onEditTable: (table: Entity) => void
@@ -28,6 +29,7 @@ export interface TableEditorLayoutProps {
const TableEditorLayout = ({
selectedSchema,
+ selectedTable,
onSelectSchema = noop,
onAddTable = noop,
onEditTable = noop,
@@ -123,6 +125,7 @@ const TableEditorLayout = ({
return (
number
+}
+
+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',
+ dateFormat = 'DD MMM',
+ modifier,
+ }: 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' &&
+ typeof attribute !== 'undefined' &&
+ typeof startDate !== 'undefined' &&
+ typeof endDate !== 'undefined',
+
+ select(data) {
+ const noDataYet = Number(data.data[0].period_start) === 0
+
+ // [Joshen] Ideally handled by API, like infra-monitoring
+ if (noDataYet) {
+ const days = dayjs(endDate).diff(dayjs(startDate), 'days')
+ const tempArray = new Array(days).fill(0)
+ const mockData = tempArray.map((x, idx) => {
+ return {
+ loopId: idx,
+ period_start: dayjs(startDate).add(idx, 'day').format('DD MMM YYYY'),
+ periodStartFormatted: dayjs(startDate).add(idx, 'day').format(dateFormat),
+ [attribute]: 0,
+ }
+ })
+ return { ...data, data: mockData, hasNoData: true } as TData
+ } else {
+ return {
+ ...data,
+ hasNoData: false,
+ data: data.data.map((x) => {
+ return {
+ ...x,
+ [attribute]:
+ modifier !== undefined ? modifier(Number(x[attribute])) : Number(x[attribute]),
+ periodStartFormatted:
+ Number(x.period_start) > 0
+ ? dayjs(x.period_start).format(dateFormat)
+ : x.period_start,
+ }
+ }),
+ } as TData
+ }
+ },
+ ...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..028bd927c15
--- /dev/null
+++ b/studio/data/analytics/infra-monitoring-query.ts
@@ -0,0 +1,102 @@
+import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
+import dayjs from 'dayjs'
+import { get } from 'lib/common/fetch'
+import { API_URL } from 'lib/constants'
+import { useCallback } from 'react'
+import { AnalyticsData } from './constants'
+import { analyticsKeys } from './keys'
+
+export type InfraMonitoringVariables = {
+ projectRef?: string
+ attribute: 'cpu_usage' | 'disk_io_budget' | 'ram_usage'
+ startDate?: string
+ endDate?: string
+ interval?: string
+ dateFormat?: string
+ modifier?: (x: number) => number
+}
+
+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('End 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',
+ dateFormat = 'DD MMM',
+ modifier,
+ }: 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' &&
+ typeof attribute !== 'undefined' &&
+ typeof startDate !== 'undefined' &&
+ typeof endDate !== 'undefined',
+ select(data) {
+ return {
+ ...data,
+ data: data.data.map((x) => {
+ return {
+ ...x,
+ [attribute]:
+ modifier !== undefined ? modifier(Number(x[attribute])) : Number(x[attribute]),
+ periodStartFormatted: dayjs(x.period_start).format(dateFormat),
+ }
+ }),
+ } as TData
+ },
+ ...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..7429a1fd7c5
--- /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 ProjectsData = Awaited>
+export type ProjectsError = 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/data/usage/project-usage-query.ts b/studio/data/usage/project-usage-query.ts
index dca03ce536f..1292eba7e26 100644
--- a/studio/data/usage/project-usage-query.ts
+++ b/studio/data/usage/project-usage-query.ts
@@ -8,37 +8,7 @@ export type ProjectUsageVariables = {
projectRef?: string
}
-export interface DbSize {
- usage: number | null
- limit: number
- cost: number
- current: number
- available_in_plan: boolean
-}
-
-export interface DbEgress {
- usage: number
- limit: number
- cost: number
- available_in_plan: boolean
-}
-
-export interface StorageSize {
- usage: number | null
- limit: number
- cost: number
- current: number
- available_in_plan: boolean
-}
-
-export interface StorageEgress {
- usage: number
- limit: number
- cost: number
- available_in_plan: boolean
-}
-
-export interface MonthlyActiveUsers {
+export interface UsageMetric {
usage: number
limit: number
cost: number
@@ -46,11 +16,17 @@ export interface MonthlyActiveUsers {
}
export type ProjectUsageResponse = {
- db_size: DbSize
- db_egress: DbEgress
- storage_size: StorageSize
- storage_egress: StorageEgress
- monthly_active_users: MonthlyActiveUsers
+ db_size: UsageMetric
+ db_egress: UsageMetric
+ storage_size: UsageMetric
+ storage_egress: UsageMetric
+ storage_image_render_count: UsageMetric
+ monthly_active_users: UsageMetric
+ monthly_active_sso_users: UsageMetric
+ realtime_message_count: UsageMetric
+ realtime_peak_connection: UsageMetric
+ func_count: UsageMetric
+ func_invocations: UsageMetric
disk_volume_size_gb: number
}
@@ -83,6 +59,22 @@ export const useProjectUsageQuery = (
({ signal }) => getProjectUsage({ projectRef }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined',
+ select(data) {
+ return Object.fromEntries(
+ Object.entries(data).map(([key, value]) => {
+ if (typeof value === 'object') {
+ const formattedValue = {
+ ...value,
+ usage: Number(value.usage),
+ }
+
+ return [key, formattedValue]
+ } else {
+ return [key, value]
+ }
+ })
+ )
+ },
...options,
}
)
diff --git a/studio/lib/helpers.ts b/studio/lib/helpers.ts
index d11dd083a6e..759643225f0 100644
--- a/studio/lib/helpers.ts
+++ b/studio/lib/helpers.ts
@@ -136,15 +136,17 @@ export const propsAreEqual = (prevProps: any, nextProps: any) => {
}
}
-export const formatBytes = (bytes: any, decimals = 2) => {
- if (bytes === 0 || bytes === undefined) return '0 bytes'
-
+export const formatBytes = (
+ bytes: any,
+ decimals = 2,
+ size?: 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB' | 'EB' | 'ZB' | 'YB'
+) => {
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
- const i = Math.floor(Math.log(bytes) / Math.log(k))
-
+ if (bytes === 0 || bytes === undefined) return size !== undefined ? `0 ${size}` : '0 bytes'
+ const i = size !== undefined ? sizes.indexOf(size) : Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}
diff --git a/studio/pages/new/[slug].tsx b/studio/pages/new/[slug].tsx
index 919e2b3ea23..8aec44d5a22 100644
--- a/studio/pages/new/[slug].tsx
+++ b/studio/pages/new/[slug].tsx
@@ -112,14 +112,14 @@ const Wizard: NextPageWithLayout = () => {
}
}, [])
- useEffect(() => {
- async function getPaymentMethods(slug: string) {
- const { data: paymentMethods, error } = await get(`${API_URL}/organizations/${slug}/payments`)
- if (!error) {
- setPaymentMethods(paymentMethods)
- }
+ async function getPaymentMethods(slug: string) {
+ const { data: paymentMethods, error } = await get(`${API_URL}/organizations/${slug}/payments`)
+ if (!error) {
+ setPaymentMethods(paymentMethods)
}
+ }
+ useEffect(() => {
if (slug) {
getPaymentMethods(slug as string)
}
@@ -147,6 +147,12 @@ const Wizard: NextPageWithLayout = () => {
setDbPricingTierKey(value)
}
+ function onPaymentMethodAdded() {
+ if (slug) {
+ return getPaymentMethods(slug)
+ }
+ }
+
async function checkPasswordStrength(value: any) {
const { message, warning, strength } = await passwordStrength(value)
setPasswordStrengthScore(strength)
@@ -443,7 +449,7 @@ const Wizard: NextPageWithLayout = () => {
)}
{!isSelectFreeTier && isEmptyPaymentMethod && (
-
+
)}
)}
diff --git a/studio/pages/project/[ref]/editor/[id].tsx b/studio/pages/project/[ref]/editor/[id].tsx
index 0b202c9f83a..863f1dda3c8 100644
--- a/studio/pages/project/[ref]/editor/[id].tsx
+++ b/studio/pages/project/[ref]/editor/[id].tsx
@@ -270,6 +270,7 @@ const TableEditorPage: NextPageWithLayout = () => {
return (
{
currentSubscription={subscription}
isLoadingPaymentMethods={isLoadingPaymentMethods}
paymentMethods={paymentMethods || []}
+ onPaymentMethodAdded={() => getPaymentMethods()}
/>
)
}
diff --git a/studio/pages/project/[ref]/settings/billing/update/pro.tsx b/studio/pages/project/[ref]/settings/billing/update/pro.tsx
index a175d3688f1..7884a282b3a 100644
--- a/studio/pages/project/[ref]/settings/billing/update/pro.tsx
+++ b/studio/pages/project/[ref]/settings/billing/update/pro.tsx
@@ -119,6 +119,7 @@ const BillingUpdatePro: NextPageWithLayout = () => {
isLoadingPaymentMethods={isLoadingPaymentMethods}
paymentMethods={paymentMethods || []}
onSelectBack={() => router.push(`/project/${projectRef}/settings/billing/update`)}
+ onPaymentMethodAdded={() => getPaymentMethods()}
/>
)
}
diff --git a/studio/pages/project/[ref]/settings/billing/update/team.tsx b/studio/pages/project/[ref]/settings/billing/update/team.tsx
index 316e1bb5976..5907f56e72f 100644
--- a/studio/pages/project/[ref]/settings/billing/update/team.tsx
+++ b/studio/pages/project/[ref]/settings/billing/update/team.tsx
@@ -118,6 +118,7 @@ const BillingUpdateTeam: NextPageWithLayout = () => {
currentSubscription={subscription}
isLoadingPaymentMethods={isLoadingPaymentMethods}
paymentMethods={paymentMethods || []}
+ onPaymentMethodAdded={() => getPaymentMethods()}
/>
)
}
diff --git a/studio/pages/project/[ref]/settings/billing/usage.tsx b/studio/pages/project/[ref]/settings/billing/usage.tsx
index 676a784d041..5d41670e8e0 100644
--- a/studio/pages/project/[ref]/settings/billing/usage.tsx
+++ b/studio/pages/project/[ref]/settings/billing/usage.tsx
@@ -1,10 +1,10 @@
import dayjs from 'dayjs'
import { observer } from 'mobx-react-lite'
-import { FC, useEffect, useState } from 'react'
-import { IconArrowRight, Loading } from 'ui'
+import { useEffect, useState } from 'react'
+import { IconArrowRight, Loading, Toggle } from 'ui'
-import { Project, NextPageWithLayout } from 'types'
-import { useStore } from 'hooks'
+import { NextPageWithLayout } from 'types'
+import { useStore, useFlag } from 'hooks'
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
import { STRIPE_PRODUCT_IDS, TIME_PERIODS_REPORTS, TIME_PERIODS_BILLING } from 'lib/constants'
import { SettingsLayout } from 'components/layouts'
@@ -12,16 +12,33 @@ 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 { ui } = useStore()
- const project = ui.selectedProject
+ const enableUsageV2 = useFlag('usagev2')
+ const [showNewUsageUI, setShowNewUsageUI] = useState(enableUsageV2)
return (
-
-
-
-
+
+ {enableUsageV2 && (
+
+
setShowNewUsageUI(!showNewUsageUI)}
+ />
+ Preview new interface
+
+ )}
+ {enableUsageV2 && showNewUsageUI ? (
+
+ ) : (
+
+ )}
)
}
@@ -32,12 +49,9 @@ ProjectBillingUsage.getLayout = (page) => (
export default observer(ProjectBillingUsage)
-interface SettingsProps {
- project?: Project
-}
-
-const Settings: FC
= ({ project }) => {
+const Settings = () => {
const { ui } = useStore()
+ const project = ui.selectedProject
const {
data: subscription,
diff --git a/studio/styles/ui.scss b/studio/styles/ui.scss
index 12e09fa2017..e080e04ecde 100644
--- a/studio/styles/ui.scss
+++ b/studio/styles/ui.scss
@@ -438,3 +438,14 @@
@apply stroke-green-500;
}
}
+
+.recharts-cartesian-axis-tick {
+ text {
+ @apply text-xs;
+ }
+}
+.recharts-reference-line {
+ line {
+ @apply stroke-scale-1000;
+ }
+}
\ No newline at end of file