mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-24 22:15:18 +00:00
Major additions: - Complete Next.js studio application with 1600+ components - Docker support (Dockerfile.combined, docker-compose.yml) - GCP deployment documentation and benchmarks - SQL benchmark scripts for performance testing - Sentry integration for monitoring - Comprehensive test suite and mocks Studio features: - Dashboard and admin interfaces - Data visualization components - Authentication and user management - API integration with RuVector backend - Static data and public assets 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import { groupBy } from 'lodash'
|
|
|
|
import { DataPoint } from 'data/analytics/constants'
|
|
import type { OrgDailyUsageResponse, PricingMetric } from 'data/analytics/org-daily-stats-query'
|
|
import type { OrgSubscription } from 'data/subscriptions/types'
|
|
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
|
|
|
// [Joshen] This is just for development to generate some test data for chart rendering
|
|
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: `${idx + 1}`,
|
|
[attribute]: Math.floor(Math.random() * 100).toString(),
|
|
}
|
|
})
|
|
}
|
|
|
|
export function useGetUpgradeUrl(slug: string, subscription?: OrgSubscription, source?: string) {
|
|
const { billingAll } = useIsFeatureEnabled(['billing:all'])
|
|
|
|
if (!billingAll) {
|
|
const subject = `Enquiry to upgrade plan for organization`
|
|
const message = `Organization Slug: ${slug}\nRequested plan: <Specify which plan to upgrade to: Pro | Team | Enterprise | Platform>`
|
|
|
|
return `/support/new?orgSlug=${slug}&projectRef=no-project&category=Plan_upgrade&subject=${subject}&message=${encodeURIComponent(message)}`
|
|
}
|
|
|
|
if (!subscription) {
|
|
return `/org/${slug}/billing`
|
|
}
|
|
|
|
return subscription?.plan?.id === 'pro' && subscription?.usage_billing_enabled === false
|
|
? `/org/${slug}/billing#cost-control`
|
|
: `/org/${slug}/billing?panel=subscriptionPlan&source=usage${source}`
|
|
}
|
|
|
|
const compactNumberFormatter = new Intl.NumberFormat('en-US', {
|
|
notation: 'compact',
|
|
compactDisplay: 'short',
|
|
})
|
|
|
|
/**
|
|
* For the y-axis, we don't need to be as precise, to avoid showing 58.597MB.
|
|
*/
|
|
export const ChartYFormatterCompactNumber = (number: number | string, unit: string) => {
|
|
if (typeof number === 'string') return number
|
|
|
|
if (unit === 'bytes') {
|
|
const formattedBytes = formatBytesCompact(number).replace(/\s/g, '')
|
|
|
|
return formattedBytes === '0bytes' ? '0' : formattedBytes
|
|
} else if (unit === 'gigabytes') {
|
|
return compactNumberFormatter.format(number) + 'GB'
|
|
} else {
|
|
return compactNumberFormatter.format(number)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For the chart tooltip, we want to be more precise and show more decimals.
|
|
*/
|
|
export const ChartTooltipValueFormatter = (number: number | string, unit: string) => {
|
|
if (typeof number === 'string') return number
|
|
|
|
if (unit === 'bytes') {
|
|
const formattedBytes = formatBytesPrecision(number).replace(/\s/g, '')
|
|
|
|
return formattedBytes === '0bytes' ? '0' : formattedBytes
|
|
} else if (unit === 'gigabytes') {
|
|
return compactNumberFormatter.format(number) + 'GB'
|
|
} else {
|
|
return compactNumberFormatter.format(number)
|
|
}
|
|
}
|
|
|
|
const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
|
|
const formatBytesCompact = (bytes: number) => {
|
|
if (bytes === 0 || bytes === undefined) return '0 bytes'
|
|
|
|
const k = 1024
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
|
|
const unit = sizes[i]
|
|
|
|
let dm = 2
|
|
if (['bytes', 'KB', 'MB'].includes(unit)) {
|
|
dm = 0
|
|
} else if (['GB'].includes(unit)) {
|
|
dm = 1
|
|
}
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + unit
|
|
}
|
|
|
|
const formatBytesPrecision = (bytes: any) => {
|
|
if (bytes === 0 || bytes === undefined) return '0 bytes'
|
|
|
|
const k = 1024
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
|
|
const unit = sizes[i]
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(3)) + ' ' + unit
|
|
}
|
|
|
|
export function dailyUsageToDataPoints(
|
|
dailyUsage: OrgDailyUsageResponse | undefined,
|
|
includeMetric: (metric: PricingMetric) => boolean
|
|
): DataPoint[] {
|
|
if (!dailyUsage || !dailyUsage.usages.length) return []
|
|
|
|
const groupedByDate = groupBy(
|
|
dailyUsage.usages.filter((it) => includeMetric(it.metric as PricingMetric)),
|
|
'date'
|
|
)
|
|
|
|
const dataPoints: DataPoint[] = []
|
|
|
|
Object.entries(groupedByDate).forEach(([date, usages]) => {
|
|
const dataPoint: DataPoint = {
|
|
period_start: date,
|
|
periodStartFormatted: dayjs(date).format('DD MMM'),
|
|
}
|
|
|
|
for (const usage of usages) {
|
|
dataPoint[usage.metric.toLowerCase()] = usage.usage_original
|
|
|
|
if (usage.breakdown) {
|
|
for (const [key, value] of Object.entries(usage.breakdown)) {
|
|
dataPoint[key.toLowerCase()] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
dataPoints.push(dataPoint)
|
|
})
|
|
|
|
return dataPoints
|
|
}
|