Usage.utils.ts143 lines · main
| 1 | import dayjs from 'dayjs' |
| 2 | import { groupBy } from 'lodash' |
| 3 | |
| 4 | import { DataPoint } from '@/data/analytics/constants' |
| 5 | import type { OrgDailyUsageResponse, PricingMetric } from '@/data/analytics/org-daily-stats-query' |
| 6 | import type { OrgSubscription } from '@/data/subscriptions/types' |
| 7 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 8 | |
| 9 | // [Joshen] This is just for development to generate some test data for chart rendering |
| 10 | export const generateUsageData = (attribute: string, days: number): DataPoint[] => { |
| 11 | const tempArray = new Array(days).fill(0) |
| 12 | return tempArray.map((_x, idx) => { |
| 13 | return { |
| 14 | loopId: (idx + 1).toString(), |
| 15 | period_start: `${idx + 1}`, |
| 16 | [attribute]: Math.floor(Math.random() * 100).toString(), |
| 17 | } |
| 18 | }) |
| 19 | } |
| 20 | |
| 21 | export function useGetUpgradeUrl(slug: string, subscription?: OrgSubscription, source?: string) { |
| 22 | const { billingAll } = useIsFeatureEnabled(['billing:all']) |
| 23 | |
| 24 | if (!billingAll) { |
| 25 | const subject = `Enquiry to upgrade plan for organization` |
| 26 | const message = `Organization Slug: ${slug}\nRequested plan: <Specify which plan to upgrade to: Pro | Team | Enterprise | Platform>` |
| 27 | |
| 28 | return `/support/new?orgSlug=${slug}&projectRef=no-project&category=Plan_upgrade&subject=${subject}&message=${encodeURIComponent(message)}` |
| 29 | } |
| 30 | |
| 31 | if (!subscription) { |
| 32 | return `/org/${slug}/billing` |
| 33 | } |
| 34 | |
| 35 | return subscription?.plan?.id === 'pro' && subscription?.usage_billing_enabled === false |
| 36 | ? `/org/${slug}/billing#cost-control` |
| 37 | : `/org/${slug}/billing?panel=subscriptionPlan&source=usage${source}` |
| 38 | } |
| 39 | |
| 40 | const compactNumberFormatter = new Intl.NumberFormat('en-US', { |
| 41 | notation: 'compact', |
| 42 | compactDisplay: 'short', |
| 43 | }) |
| 44 | |
| 45 | /** |
| 46 | * For the y-axis, we don't need to be as precise, to avoid showing 58.597MB. |
| 47 | */ |
| 48 | export const ChartYFormatterCompactNumber = (number: number | string, unit: string) => { |
| 49 | if (typeof number === 'string') return number |
| 50 | |
| 51 | if (unit === 'bytes') { |
| 52 | const formattedBytes = formatBytesCompact(number).replace(/\s/g, '') |
| 53 | |
| 54 | return formattedBytes === '0bytes' ? '0' : formattedBytes |
| 55 | } else if (unit === 'gigabytes') { |
| 56 | return compactNumberFormatter.format(number) + 'GB' |
| 57 | } else { |
| 58 | return compactNumberFormatter.format(number) |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * For the chart tooltip, we want to be more precise and show more decimals. |
| 64 | */ |
| 65 | export const ChartTooltipValueFormatter = (number: number | string, unit: string) => { |
| 66 | if (typeof number === 'string') return number |
| 67 | |
| 68 | if (unit === 'bytes') { |
| 69 | const formattedBytes = formatBytesPrecision(number).replace(/\s/g, '') |
| 70 | |
| 71 | return formattedBytes === '0bytes' ? '0' : formattedBytes |
| 72 | } else if (unit === 'gigabytes') { |
| 73 | return compactNumberFormatter.format(number) + 'GB' |
| 74 | } else { |
| 75 | return compactNumberFormatter.format(number) |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] |
| 80 | |
| 81 | const formatBytesCompact = (bytes: number) => { |
| 82 | if (bytes === 0 || bytes === undefined) return '0 bytes' |
| 83 | |
| 84 | const k = 1024 |
| 85 | const i = Math.floor(Math.log(bytes) / Math.log(k)) |
| 86 | |
| 87 | const unit = sizes[i] |
| 88 | |
| 89 | let dm = 2 |
| 90 | if (['bytes', 'KB', 'MB'].includes(unit)) { |
| 91 | dm = 0 |
| 92 | } else if (['GB'].includes(unit)) { |
| 93 | dm = 1 |
| 94 | } |
| 95 | |
| 96 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + unit |
| 97 | } |
| 98 | |
| 99 | const formatBytesPrecision = (bytes: any) => { |
| 100 | if (bytes === 0 || bytes === undefined) return '0 bytes' |
| 101 | |
| 102 | const k = 1024 |
| 103 | const i = Math.floor(Math.log(bytes) / Math.log(k)) |
| 104 | |
| 105 | const unit = sizes[i] |
| 106 | |
| 107 | return parseFloat((bytes / Math.pow(k, i)).toFixed(3)) + ' ' + unit |
| 108 | } |
| 109 | |
| 110 | export function dailyUsageToDataPoints( |
| 111 | dailyUsage: OrgDailyUsageResponse | undefined, |
| 112 | includeMetric: (metric: PricingMetric) => boolean |
| 113 | ): DataPoint[] { |
| 114 | if (!dailyUsage || !dailyUsage.usages.length) return [] |
| 115 | |
| 116 | const groupedByDate = groupBy( |
| 117 | dailyUsage.usages.filter((it) => includeMetric(it.metric as PricingMetric)), |
| 118 | 'date' |
| 119 | ) |
| 120 | |
| 121 | const dataPoints: DataPoint[] = [] |
| 122 | |
| 123 | Object.entries(groupedByDate).forEach(([date, usages]) => { |
| 124 | const dataPoint: DataPoint = { |
| 125 | period_start: date, |
| 126 | periodStartFormatted: dayjs(date).format('DD MMM'), |
| 127 | } |
| 128 | |
| 129 | for (const usage of usages) { |
| 130 | dataPoint[usage.metric.toLowerCase()] = usage.usage_original |
| 131 | |
| 132 | if (usage.breakdown) { |
| 133 | for (const [key, value] of Object.entries(usage.breakdown)) { |
| 134 | dataPoint[key.toLowerCase()] = value |
| 135 | } |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | dataPoints.push(dataPoint) |
| 140 | }) |
| 141 | |
| 142 | return dataPoints |
| 143 | } |