Add chart component for usage

This commit is contained in:
Joshen Lim committed 2023-04-28 17:52:19 +08:00
1 parent 7abc50e4d6
commit 648bda3a2c
13 files changed
+563 -1

No files matched your search

@@ -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 (
<div className="w-full h-[200px]">
<ResponsiveContainer width="100%" height={200}>
<ComposedChart data={data} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis dataKey="" />
<YAxis width={28} axisLine={false} tickLine={{ stroke: 'none' }} />
<Bar dataKey={attribute} fill="#EEE" />
{reference && (
<ReferenceLine
y={reference.value}
label={
<foreignObject
x={reference.x}
y={reference.y}
width={reference.width}
height={reference.height}
>
<div className="text-[0.7rem] font-bold text-amber-900 bg-amber-300 w-fit p-1.5 py-0.5 rounded-md">
{reference.label}
</div>
</foreignObject>
}
/>
)}
</ComposedChart>
</ResponsiveContainer>
</div>
)
}
export default BarChart
@@ -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 (
<>
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
<div className="grid grid-cols-12">
<div className="col-span-5">
<div className="sticky top-16">
<p className="text-base">IO Budget</p>
<p className="text-sm text-scale-1000">Some description here</p>
</div>
</div>
<div className="col-span-7 space-y-6">
<div className="space-y-1">
<p>IO budget per day</p>
<p className="text-sm text-scale-1000">
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
</p>
</div>
{isLoadingIoBudgetData ? (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
) : (
<BarChart
attribute="disk_io_budget"
data={generateUsageData('disk_io_budget', 30)}
reference={{
value: 65,
label: 'FREE QUOTA',
x: 60,
y: 49,
width: 200,
height: 24,
}}
/>
)}
</div>
</div>
</div>
</div>
</>
)
}
export default Infrastructure
@@ -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<string>(ref as string)
const { data: projects, isLoading, isSuccess } = useProjectsQuery()
const { data: subscription } = useProjectSubscriptionQuery({ projectRef: ref })
useEffect(() => {}, [isSuccess])
return (
<>
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col px-5 lg:px-16 2xl:px-32 pt-6 space-y-4">
<h3 className="text-scale-1200 text-xl">Usage</h3>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{/* <Billing cycle picker /> */}
{isLoading ? (
<ShimmeringLoader className="w-[200px]" />
) : (
<Listbox size="small" value={ref} onChange={() => {}} className="w-[200px]">
{(projects ?? []).map((project) => (
<Listbox.Option
key={project.ref}
value={project.ref}
label={project.name}
className="!w-[200px]"
>
{project.name}
</Listbox.Option>
))}
</Listbox>
)}
{subscription !== undefined && (
<p className="text-sm">Project is on {subscription.tier.name}</p>
)}
</div>
<div className="flex items-center space-x-2">
<Button type="default">View invoices</Button>
<Button type="default">View billing</Button>
</div>
</div>
<div className="flex items-center space-x-6 !mt-2">
<div className="flex items-center opacity-50 space-x-2 py-3 hover:opacity-100 transition cursor-pointer">
<IconCheckCircle size={15} strokeWidth={2} className="text-brand-900" />
<p className="text-sm">Infrastructure</p>
</div>
<div className="flex items-center opacity-50 space-x-2 py-3 hover:opacity-100 transition cursor-pointer">
<IconCheckCircle size={15} strokeWidth={2} className="text-brand-900" />
<p className="text-sm">Bandwidth</p>
</div>
<div className="flex items-center opacity-50 space-x-2 py-3 hover:opacity-100 transition cursor-pointer">
<IconCheckCircle size={15} strokeWidth={2} className="text-brand-900" />
<p className="text-sm">Size & Counts</p>
</div>
<div className="flex items-center opacity-50 space-x-2 py-3 hover:opacity-100 transition cursor-pointer">
<IconCheckCircle size={15} strokeWidth={2} className="text-brand-900" />
<p className="text-sm">Activity</p>
</div>
</div>
</div>
</div>
<Infrastructure />
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
Segment 2
</div>
</div>
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
Segment 3
</div>
</div>
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
Segment 4
</div>
</div>
</>
)
}
export default Usage
@@ -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(),
}
})
}
+15
View File
@@ -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
}
+107
View File
@@ -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<ReturnType<typeof getDailyStats>>
export type DailyStatsError = unknown
export const useDailyStatsQuery = <TData = DailyStatsData>(
{ projectRef, attribute, startDate, endDate, interval = '1d' }: DailyStatsVariables,
{ enabled = true, ...options }: UseQueryOptions<DailyStatsData, DailyStatsError, TData> = {}
) =>
useQuery<DailyStatsData, DailyStatsError, TData>(
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])
}
@@ -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<ReturnType<typeof getInfraMonitoring>>
export type InfraMonitoringError = unknown
export const useInfraMonitoringQuery = <TData = InfraMonitoringData>(
{ projectRef, attribute, startDate, endDate, interval = '1d' }: InfraMonitoringVariables,
{
enabled = true,
...options
}: UseQueryOptions<InfraMonitoringData, InfraMonitoringError, TData> = {}
) =>
useQuery<InfraMonitoringData, InfraMonitoringError, TData>(
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])
}
+25
View File
@@ -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,
}
+4
View File
@@ -0,0 +1,4 @@
export const projectKeys = {
list: () => ['projects'] as const,
detail: (projectRef: string | undefined) => ['projects', projectRef, 'detail'] as const,
}
@@ -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<ReturnType<typeof getProjectDetail>>
export type ProjectDetailError = unknown
export const useProjectDetailQuery = <TData = ProjectDetailData>(
{ ref }: ProjectDetailVariables,
{ enabled = true, ...options }: UseQueryOptions<ProjectDetailData, ProjectDetailError, TData> = {}
) =>
useQuery<ProjectDetailData, ProjectDetailError, TData>(
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])
}
+37
View File
@@ -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<ReturnType<typeof getProjects>>
export type OrganizationRolesError = unknown
export const useProjectsQuery = <TData = OrganizationRolesData>({
enabled = true,
...options
}: UseQueryOptions<OrganizationRolesData, OrganizationRolesError, TData> = {}) =>
useQuery<OrganizationRolesData, OrganizationRolesError, TData>(
projectKeys.list(),
({ signal }) => getProjects(signal),
{ enabled: enabled, ...options }
)
export const useProjectsPrefetch = () => {
const client = useQueryClient()
return useCallback(() => {
client.prefetchQuery(projectKeys.list(), ({ signal }) => getProjects(signal))
}, [])
}
@@ -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 <Usage />
return (
<div className="w-full h-full overflow-y-auto content">
+14
View File
@@ -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;
}
}