-
Request body
+
Request Metadata
- Response{method ? ` ${method}` : null} body
+ Response Metadata
= ({ setSentCategory }) => {
}
}, [uploadedFiles])
+ const { data: profile } = useProfileQuery()
+ const respondToEmail = profile?.primary_email ?? 'your email'
+
if (!isInitialized) {
return (
@@ -576,6 +580,11 @@ const SupportForm: FC
= ({ setSentCategory }) => {
+
+
+ We will contact you at {respondToEmail}.
+
+
= ({
= {
uuid: [
{
- name: 'uuid_generate_v4()',
- value: 'uuid_generate_v4()',
+ name: 'gen_random_uuid()',
+ value: 'gen_random_uuid()',
description: 'Generates a version 4 UUID',
},
],
diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx
index daa71d81168..eac3e0c2540 100644
--- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx
+++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx
@@ -241,7 +241,7 @@ const ColumnManagement: FC = ({
>
Can either be a literal or an expression. When using an expression wrap your
- expression in brackets, e.g. (uuid_generate_v4())
+ expression in brackets, e.g. (gen_random_uuid())
diff --git a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx
index 11c972561ba..9df62f126e9 100644
--- a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx
+++ b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx
@@ -26,6 +26,7 @@ export interface ProjectLayoutProps {
hideHeader?: boolean
hideIconBar?: boolean
children: ReactNode
+ selectedTable?: string
}
const ProjectLayout = ({
@@ -36,12 +37,14 @@ const ProjectLayout = ({
children,
hideHeader = false,
hideIconBar = false,
+ selectedTable,
}: ProjectLayoutProps) => {
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 (
= ({
const resolvedHighlightedLabel =
(focusDataIndex !== null &&
data &&
- data[focusDataIndex] &&
+ data[focusDataIndex] !== undefined &&
day(data[focusDataIndex][xAxisKey]).format(customDateFormat)) ||
highlightedLabel
const resolvedHighlightedValue =
- (focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : null) || highlightedValue
+ (focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue)
return (
diff --git a/studio/data/analytics/constants.ts b/studio/data/analytics/constants.ts
new file mode 100644
index 00000000000..d12ba4a1c19
--- /dev/null
+++ b/studio/data/analytics/constants.ts
@@ -0,0 +1,17 @@
+export type DataPoint = {
+ loopId: number | string
+ period_start: string
+ periodStartFormatted?: string
+} & {
+ // Attribute name will be the key
+ [key: string]: string
+}
+
+export interface AnalyticsData {
+ data: DataPoint[]
+ format: string
+ total: number
+ totalAverage: number
+ yAxisLimit: number
+ hasNoData?: boolean
+}
diff --git a/studio/data/analytics/daily-stats-query.ts b/studio/data/analytics/daily-stats-query.ts
new file mode 100644
index 00000000000..c25f78d85ad
--- /dev/null
+++ b/studio/data/analytics/daily-stats-query.ts
@@ -0,0 +1,189 @@
+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 DailyStatsVariables = {
+ projectRef?: string
+ attribute:
+ | 'total_db_egress_bytes'
+ | 'total_db_size_bytes'
+ | 'total_egress_modified'
+
+ // Realtime
+ | 'total_realtime_ingress'
+ | 'total_realtime_egress'
+ | 'total_realtime_message_count'
+ | 'total_realtime_peak_connection'
+ | 'total_realtime_requests'
+ | 'total_realtime_get_requests'
+ | 'total_realtime_post_requests'
+ | 'total_realtime_patch_requests'
+ | 'total_realtime_delete_requests'
+ | 'total_realtime_options_requests'
+
+ // Rest
+ | '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'
+
+ // Auth
+ | 'total_auth_billing_period_mau'
+ | 'total_auth_billing_period_sso_mau'
+ | 'total_auth_ingress'
+ | 'total_auth_egress'
+ | 'total_auth_texts'
+ | 'total_auth_users'
+ | 'total_auth_emails'
+ | 'total_auth_requests'
+ | 'total_auth_get_requests'
+ | 'total_auth_post_requests'
+ | 'total_auth_patch_requests'
+ | 'total_auth_delete_requests'
+ | 'total_auth_options_requests'
+
+ // Storage
+ | 'total_storage_ingress'
+ | 'total_storage_egress'
+ | 'total_storage_size_bytes'
+ | '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'
+
+ // Edge functions
+ | 'total_func_count'
+ | 'total_func_invocations'
+ | 'total_func_exec_time_ms'
+ | 'total_func_ingress'
+ | 'total_func_egress'
+
+ // Combined
+ | '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
+ dateFormat?: string
+ modifier?: (x: number) => 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/constants/metrics.tsx b/studio/lib/constants/metrics.tsx
index 39ee4a62c4c..4e6df5d3b31 100644
--- a/studio/lib/constants/metrics.tsx
+++ b/studio/lib/constants/metrics.tsx
@@ -217,7 +217,7 @@ export const METRICS = [
},
{
key: 'total_storage_image_render_count',
- label: 'Storage Images Transformed',
+ label: 'Storage Image Transformations',
provider: 'daily-stats',
category: METRIC_CATEGORIES.API_STORAGE,
},
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 (
{
params={report.params.totalRequests}
title="Total Requests"
data={report.data.totalRequests || []}
- renderer={renderTotalRequests}
+ renderer={TotalRequestsChartRenderer}
+ append={TopApiRoutesRenderer}
+ appendProps={{ data: report.data.topRoutes || [] }}
/>
{
title="Response Errors"
tooltip="Error responses with 4XX or 5XX status codes"
data={report.data.errorCounts || []}
- renderer={renderErrorCounts}
+ renderer={ErrorCountsChartRenderer}
+ appendProps={{ data: report.data.topErrorRoutes || [] }}
+ append={TopApiRoutesRenderer}
/>
{
title="Response Speed"
tooltip="Average response speed (in miliseconds) of a request"
data={report.data.responseSpeed || []}
- renderer={renderResponseSpeed}
+ renderer={ResponseSpeedChartRenderer}
+ appendProps={{ data: report.data.topSlowRoutes || [] }}
+ append={TopApiRoutesRenderer}
/>
)
@@ -94,9 +101,19 @@ const useApiReport = () => {
projectRef ?? 'default'
)
const totalRequests = queryHooks.totalRequests()
+ const topRoutes = queryHooks.topRoutes()
const errorCounts = queryHooks.errorCounts()
+ const topErrorRoutes = queryHooks.topErrorRoutes()
const responseSpeed = queryHooks.responseSpeed()
- const activeHooks = [totalRequests, errorCounts, responseSpeed]
+ const topSlowRoutes = queryHooks.topSlowRoutes()
+ const activeHooks = [
+ totalRequests,
+ topRoutes,
+ errorCounts,
+ topErrorRoutes,
+ responseSpeed,
+ topSlowRoutes,
+ ]
const [filters, setFilters] = useState([])
const addFilter = (filter: ReportFilterItem) => {
// use a deep equal when comparing objects.
@@ -127,12 +144,23 @@ const useApiReport = () => {
if (totalRequests[1].changeQuery) {
totalRequests[1].changeQuery(PRESET_CONFIG.api.queries.totalRequests.sql(filters))
}
+ if (topRoutes[1].changeQuery) {
+ topRoutes[1].changeQuery(PRESET_CONFIG.api.queries.topRoutes.sql(filters))
+ }
if (errorCounts[1].changeQuery) {
errorCounts[1].changeQuery(PRESET_CONFIG.api.queries.errorCounts.sql(filters))
}
+
+ if (topErrorRoutes[1].changeQuery) {
+ topErrorRoutes[1].changeQuery(PRESET_CONFIG.api.queries.topErrorRoutes.sql(filters))
+ }
if (responseSpeed[1].changeQuery) {
responseSpeed[1].changeQuery(PRESET_CONFIG.api.queries.responseSpeed.sql(filters))
}
+
+ if (topSlowRoutes[1].changeQuery) {
+ topSlowRoutes[1].changeQuery(PRESET_CONFIG.api.queries.topSlowRoutes.sql(filters))
+ }
}, [JSON.stringify(filters)])
const handleRefresh = async () => {
@@ -151,11 +179,17 @@ const useApiReport = () => {
totalRequests: totalRequests[0].logData,
errorCounts: errorCounts[0].logData,
responseSpeed: responseSpeed[0].logData,
+ topRoutes: topRoutes[0].logData,
+ topErrorRoutes: topErrorRoutes[0].logData,
+ topSlowRoutes: topSlowRoutes[0].logData,
},
params: {
totalRequests: totalRequests[0].params,
errorCounts: errorCounts[0].params,
responseSpeed: responseSpeed[0].params,
+ topRoutes: topRoutes[0].params,
+ topErrorRoutes: topErrorRoutes[0].params,
+ topSlowRoutes: topSlowRoutes[0].params,
},
mergeParams: handleSetParams,
filters,
diff --git a/studio/pages/project/[ref]/settings/billing/update/enterprise.tsx b/studio/pages/project/[ref]/settings/billing/update/enterprise.tsx
index 90c6d4a9c94..07e07e49304 100644
--- a/studio/pages/project/[ref]/settings/billing/update/enterprise.tsx
+++ b/studio/pages/project/[ref]/settings/billing/update/enterprise.tsx
@@ -111,6 +111,7 @@ const BillingUpdateEnterprise: NextPageWithLayout = () => {
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
diff --git a/studio/tests/components/Reports/ReportWidget.test.js b/studio/tests/components/Reports/ReportWidget.test.js
index b657ec2d897..3eeb8da5e94 100644
--- a/studio/tests/components/Reports/ReportWidget.test.js
+++ b/studio/tests/components/Reports/ReportWidget.test.js
@@ -1,46 +1,12 @@
-// mock the fetch function
-jest.mock('lib/common/fetch')
import { get } from 'lib/common/fetch'
-
-// mock mobx
-jest.mock('mobx-react-lite')
-import { observer } from 'mobx-react-lite'
-observer.mockImplementation((v) => v)
-
-// mock the router
-jest.mock('next/router')
import { useRouter } from 'next/router'
-const defaultRouterMock = () => {
- const router = jest.fn()
- router.query = {}
- router.push = jest.fn()
- router.pathname = 'logs/path'
- return router
-}
-useRouter.mockReturnValue(defaultRouterMock())
-
-// mock usage flags
-jest.mock('components/ui/Flag/Flag')
-import Flag from 'components/ui/Flag/Flag'
-Flag.mockImplementation(({ children }) => <>{children}>)
-jest.mock('hooks')
-import { useFlag } from 'hooks'
-useFlag.mockReturnValue(true)
-
-import { fireEvent, waitFor, screen, act } from '@testing-library/react'
+import { screen } from '@testing-library/react'
import { render } from '../../helpers'
-import userEvent from '@testing-library/user-event'
-import { wait } from '@testing-library/user-event/dist/utils'
-import { logDataFixture } from '../../fixtures'
-import { LogsTableName } from 'components/interfaces/Settings/Logs'
-import { Presets } from 'components/interfaces/Reports/Reports.types'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
-import { clickDropdown } from 'tests/helpers'
+import userEvent from '@testing-library/user-event'
beforeEach(() => {
// reset mocks between tests
get.mockReset()
- useRouter.mockReset()
- useRouter.mockReturnValue(defaultRouterMock())
})
test('static elements', async () => {
@@ -48,3 +14,9 @@ test('static elements', async () => {
await screen.findByText(/Some chart/)
await screen.findByText(/something/)
})
+
+test("append", async ()=>{
+ const appendable = ()=> "some text"
+ render( null} append={appendable} />)
+ await screen.findByText(/some text/)
+})
diff --git a/studio/tests/pages/projects/reports/api-report.test.js b/studio/tests/pages/projects/reports/api-report.test.js
index 92cb474d077..be214ef6b73 100644
--- a/studio/tests/pages/projects/reports/api-report.test.js
+++ b/studio/tests/pages/projects/reports/api-report.test.js
@@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event'
beforeEach(() => {
// reset mocks between tests
get.mockReset()
- get.mockImplementation(async (_url) => [{ data: [] }])
+ get.mockImplementation(async (_url) => [{ result: [] }])
})
test(`static elements`, async () => {
@@ -26,3 +26,86 @@ test('refresh button', async () => {
userEvent.click(await screen.findByText(/Refresh/))
await waitFor(() => expect(get).toBeCalled())
})
+
+test('append - api request routes', async () => {
+ get.mockImplementation(async (url) => {
+ if (decodeURIComponent(url).includes("request.path")) {
+ return { result: [{ path: 'mypath', method: 'GET', status_code: 200, search: 'some-query', count: 22 }] }
+ }
+ return { result: [{ timestamp: new Date().toISOString(), count: 123 }] }
+ })
+ render( )
+ await waitFor(() => expect(get).toBeCalled())
+ await screen.findAllByText(/mypath/)
+ await screen.findAllByText(/GET/)
+ await screen.findAllByText(/200/)
+ await screen.findAllByText(/some\-query/)
+ await screen.findAllByText(/22/)
+})
+
+
+test('append - error routes', async () => {
+ get.mockImplementation(async (url) => {
+ const uri = decodeURIComponent(url)
+ if (uri.includes("400")) {
+ return { result: [{ path: 'mypath', method: 'GET', status_code: 200, search: 'some-query', count: 22 }] }
+ }
+ return { result: [{ timestamp: new Date().toISOString(), count: 123 }] }
+ })
+ render( )
+ await waitFor(() => expect(get).toBeCalled())
+ await screen.findAllByText(/mypath/)
+ await screen.findAllByText(/GET/)
+ await screen.findAllByText(/200/)
+ await screen.findAllByText(/some\-query/)
+ await screen.findAllByText(/22/)
+})
+
+
+test('append - error routes', async () => {
+ get.mockImplementation(async (url) => {
+ const uri = decodeURIComponent(url)
+ if (uri.includes("avg") && uri.includes("request.path")) {
+ return { result: [{ path: 'mypath', method: 'GET', status_code: 200, avg: 534, search: 'some-query', count: 22 }] }
+ }
+ return { result: [{ timestamp: new Date().toISOString(), count: 123 }] }
+ })
+ render( )
+ await waitFor(() => expect(get).toBeCalled())
+ await screen.findAllByText(/mypath/)
+ await screen.findAllByText(/GET/)
+ await screen.findAllByText(/200/)
+ await screen.findAllByText(/some\-query/)
+ await screen.findAllByText(/22/)
+ await screen.findAllByText(/534\.00ms/)
+})
+
+
+
+// test('expandable error routes', async () => {
+// get.mockImplementation(async (url) => {
+// if (url.includes('path')) {
+// return [{ data: [{ path: '/my-path', method: 'GET', search: '?=123', count: 22 }] }]
+// }
+// return [{ data: [{ timestamp: new Date().toISOString(), count: 123 }] }]
+// })
+
+// render( )
+// await screen.findByText(/\/my\-path/)
+// await screen.findByText(/\?\=123/)
+// await screen.findByText(/22/)
+// })
+
+// test('expandable high latency routes', async () => {
+// get.mockImplementation(async (url) => {
+// if (url.includes('path')) {
+// return [{ data: [{ path: '/my-path', method: 'GET', search: '?=123', avg_ms: 55, count: 22 }] }]
+// }
+// return [{ data: [{ timestamp: new Date().toISOString(), avg_ms: 123 }] }]
+// })
+// render( )
+// await screen.findByText(/\/my\-path/)
+// await screen.findByText(/\?\=123/)
+// await screen.findByText(/22/)
+// await screen.findByText(/55/)
+// })