From 648bda3a2c02cddf427d04325de82c91f0efd45f Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 28 Apr 2023 17:52:19 +0800 Subject: [PATCH] Add chart component for usage --- .../interfaces/BillingV2/Usage/BarChart.tsx | 62 ++++++++++ .../BillingV2/Usage/Infrastructure.tsx | 71 ++++++++++++ .../interfaces/BillingV2/Usage/Usage.tsx | 95 ++++++++++++++++ .../interfaces/BillingV2/Usage/Usage.utils.ts | 13 +++ studio/data/analytics/constants.ts | 15 +++ studio/data/analytics/daily-stats-query.ts | 107 ++++++++++++++++++ .../data/analytics/infra-monitoring-query.ts | 73 ++++++++++++ studio/data/analytics/keys.ts | 25 ++++ studio/data/projects/keys.ts | 4 + studio/data/projects/project-detail-query.ts | 43 +++++++ studio/data/projects/projects-query.ts | 37 ++++++ .../project/[ref]/settings/billing/usage.tsx | 5 +- studio/styles/ui.scss | 14 +++ 13 files changed, 563 insertions(+), 1 deletion(-) create mode 100644 studio/components/interfaces/BillingV2/Usage/BarChart.tsx create mode 100644 studio/components/interfaces/BillingV2/Usage/Infrastructure.tsx create mode 100644 studio/components/interfaces/BillingV2/Usage/Usage.tsx create mode 100644 studio/components/interfaces/BillingV2/Usage/Usage.utils.ts create mode 100644 studio/data/analytics/constants.ts create mode 100644 studio/data/analytics/daily-stats-query.ts create mode 100644 studio/data/analytics/infra-monitoring-query.ts create mode 100644 studio/data/projects/keys.ts create mode 100644 studio/data/projects/project-detail-query.ts create mode 100644 studio/data/projects/projects-query.ts 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}

+ )} +
+
+ + +
+
+ +
+
+ +

Infrastructure

+
+
+ +

Bandwidth

+
+
+ +

Size & Counts

+
+
+ +

Activity

+
+
+
+
+ + + +
+
+ Segment 2 +
+
+
+
+ Segment 3 +
+
+
+
+ Segment 4 +
+
+ + ) +} + +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