Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/docs/public/humans.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Eduardo Gurgel
Egor Romanov
Eleftheria Trivyzaki
Emmett Folger
Eric Kharitonashvili
Etienne Stalmans
Fabrizio Fenoglio
Felipe Stival
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,6 @@ export const ReplicationPipelineStatus = () => {
<div className="text-sm text-foreground">
{statusConfig.description}
</div>
{'lag' in table.state && (
<div className="text-xs text-foreground-light">
Lag: {table.state.lag}ms
</div>
)}
{table.state.name === 'error' && (
<ErroredTableDetails
state={table.state}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function getAvailableRegions(cloudProvider: CloudProvider): Region {
switch (cloudProvider) {
case 'AWS':
case 'AWS_K8S':
case 'AWS_NIMBUS':
return AWS_REGIONS
case 'FLY':
return FLY_REGIONS
Expand Down
120 changes: 21 additions & 99 deletions apps/studio/components/interfaces/Reports/ReportChart.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,42 @@
/**
* ReportChart
*
* A wrapper component that uses the useChartData hook to fetch data for a chart
* and then passes the data and loading state to the ComposedChartHandler.
*
* This component acts as a bridge between the data-fetching logic and the
* presentational chart component.
*/

import Link from 'next/link'
import { useRef, useState } from 'react'

import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
import LogChartHandler from 'components/ui/Charts/LogChartHandler'
import Panel from 'components/ui/Panel'
import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted'
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useChartData } from 'hooks/useChartData'
import type { UpdateDateRange } from 'pages/project/[ref]/reports/database'
import { Button, cn } from 'ui'
import { ReportChartUpsell } from './v2/ReportChartUpsell'

const ReportChart = ({
chart,
startDate,
endDate,
interval,
updateDateRange,
functionIds,
isLoading,
className,
}: {
interface ReportChartProps {
chart: any
startDate: string
endDate: string
interval: string
updateDateRange: UpdateDateRange
functionIds?: string[]
isLoading?: boolean
className?: string
}) => {
}

/**
* A wrapper component that uses the useChartData hook to fetch data for a chart
* and then passes the data and loading state to the ComposedChartHandler.
*
* This component acts as a bridge between the data-fetching logic and the
* presentational chart component.
*/
export const ReportChart = ({
chart,
startDate,
endDate,
interval,
updateDateRange,
functionIds,
isLoading,
}: ReportChartProps) => {
const { data: org } = useSelectedOrganizationQuery()
const { plan: orgPlan } = useCurrentOrgPlan()
const orgPlanId = orgPlan?.id

const [isHoveringUpgrade, setIsHoveringUpgrade] = useState(false)
const isAvailable =
chart.availableIn === undefined || (orgPlanId && chart.availableIn.includes(orgPlanId))

Expand Down Expand Up @@ -87,77 +79,8 @@ const ReportChart = ({
? filledData
: chartDataArray

const getExpDemoChartData = () =>
new Array(20).fill(0).map((_, index) => ({
period_start: new Date(startDate).getTime() + index * 1000,
demo: Math.floor(Math.pow(1.25, index) * 10),
max_demo: 1000,
}))

const getDemoChartData = () =>
new Array(20).fill(0).map((_, index) => ({
period_start: new Date(startDate).getTime() + index * 1000,
demo: Math.floor(Math.random() * 10) + 1,
max_demo: 1000,
}))

// [Jordi] useRef to prevent re-rendering making the chart change.
const demoChartData = useRef(getDemoChartData())
const exponentialChartData = useRef(getExpDemoChartData())

const chartData = isHoveringUpgrade ? exponentialChartData.current : demoChartData.current

if (!isAvailable && !isLoading) {
return (
<Panel
title={<p className="text-sm">{chart.label}</p>}
className={cn('h-[260px] relative', className)}
>
<div className="z-10 flex flex-col items-center justify-center space-y-2 h-full absolute top-0 left-0 w-full bg-surface-100/70 backdrop-blur-md">
<h2>{chart.label}</h2>
<p className="text-sm text-foreground-light">
This chart is available from{' '}
<span className="capitalize">
{!!chart.availableIn?.length ? chart.availableIn[0] : 'Pro'}
</span>{' '}
plan and above
</p>
<Button
asChild
type="primary"
onMouseEnter={() => setIsHoveringUpgrade(true)}
onMouseLeave={() => setIsHoveringUpgrade(false)}
>
<Link href={`/org/${org?.slug}/billing?panel=subscriptionPlan&source=reports`}>
Upgrade to{' '}
<span className="capitalize">
{!!chart.availableIn?.length ? chart.availableIn[0] : 'Pro'}
</span>
</Link>
</Button>
</div>
<div className="absolute top-0 left-0 w-full h-full z-0">
<LogChartHandler
attributes={[
{
attribute: 'demo',
enabled: true,
label: 'Demo',
provider: 'logs',
},
]}
label={chart.label}
startDate={startDate}
endDate={endDate}
interval={interval}
data={chartData as any}
isLoading={false}
highlightedValue={0}
updateDateRange={updateDateRange}
/>
</div>
</Panel>
)
return <ReportChartUpsell report={chart} orgSlug={org?.slug ?? ''} />
}

return (
Expand All @@ -171,4 +94,3 @@ const ReportChart = ({
/>
)
}
export default ReportChart
44 changes: 44 additions & 0 deletions apps/studio/components/interfaces/Reports/Reports.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, it, expect } from 'vitest'
import { formatTimestamp } from './Reports.utils'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'

dayjs.extend(utc)

describe('formatTimestamp', () => {
it('formats milliseconds timestamp correctly', () => {
const timestamp = 1640995200000 // 2022-01-01 00:00:00 UTC in milliseconds
const result = formatTimestamp(timestamp, { returnUtc: true })
expect(result).toBe('Jan 1, 12:00am')
})

it('formats microseconds timestamp correctly', () => {
const timestamp = 1640995200000000 // 2022-01-01 00:00:00 UTC in microseconds
const result = formatTimestamp(timestamp, { returnUtc: true })
expect(result).toBe('Jan 1, 12:00am')
})

it('formats seconds timestamp correctly', () => {
const timestamp = 1640995200 // 2022-01-01 00:00:00 UTC in seconds
const result = formatTimestamp(timestamp, { returnUtc: true })
expect(result).toBe('Jan 1, 12:00am')
})

it('handles string timestamp input', () => {
const timestamp = '1640995200000'
const result = formatTimestamp(timestamp, { returnUtc: true })
expect(result).toBe('Jan 1, 12:00am')
})

it('handles invalid string timestamp', () => {
const timestamp = 'invalid-timestamp'
const result = formatTimestamp(timestamp, { returnUtc: true })
expect(result).toBe('Invalid Date')
})

it('handles zero timestamp', () => {
const timestamp = 0
const result = formatTimestamp(timestamp, { returnUtc: true })
expect(result).toBe('Jan 1, 12:00am')
})
})
42 changes: 40 additions & 2 deletions apps/studio/components/interfaces/Reports/Reports.utils.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import dayjs from 'dayjs'

import useDbQuery, { DbQueryHook } from 'hooks/analytics/useDbQuery'
import useLogsQuery, { LogsQueryHook } from 'hooks/analytics/useLogsQuery'
import type { BaseQueries, PresetConfig, ReportQuery } from './Reports.types'
Expand All @@ -9,10 +11,13 @@ export const queryParamsToObject = (params: string) => {
return Object.fromEntries(new URLSearchParams(params))
}

// generate hooks based on preset config
export type PresetHookResult = LogsQueryHook | DbQueryHook
type PresetHooks = Record<keyof PresetConfig['queries'], () => PresetHookResult>

/**
* @deprecated
* Queries are hooks, avoid generating hooks dynamically
* Generate fetch functions instead, and pass it to a hook inside the component
*/
export const queriesFactory = <T extends string>(
queries: BaseQueries<T>,
projectRef: string
Expand All @@ -35,3 +40,36 @@ export const queriesFactory = <T extends string>(
)
return hooks
}

/**
* Formats a timestamp to a human readable format in UTC
*
* @param timestamp - The timestamp to format
* @param returnUtc - Whether to return the timestamp in UTC
* @param format - The format to use for the timestamp
* @returns The formatted timestamp string
*/
export const formatTimestamp = (
timestamp: number | string,
{ returnUtc = false, format = 'MMM D, h:mma' }: { returnUtc?: boolean; format?: string } = {}
) => {
try {
const isSeconds = String(timestamp).length === 10
const isMicroseconds = String(timestamp).length === 16

const timestampInMs = isSeconds
? Number(timestamp) * 1000
: isMicroseconds
? Number(timestamp) / 1000
: Number(timestamp)

if (returnUtc) {
return dayjs.utc(timestampInMs).format(format)
} else {
return dayjs(timestampInMs).format(format)
}
} catch (error) {
console.error(error)
return 'Invalid Date'
}
}
81 changes: 81 additions & 0 deletions apps/studio/components/interfaces/Reports/v2/ReportChartUpsell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Link from 'next/link'
import { useRef, useState } from 'react'

import { LogChartHandler } from 'components/ui/Charts/LogChartHandler'
import { ReportConfig } from 'data/reports/v2/reports.types'
import { Button, Card, cn } from 'ui'

export function ReportChartUpsell({ report, orgSlug }: { report: ReportConfig; orgSlug: string }) {
const [isHoveringUpgrade, setIsHoveringUpgrade] = useState(false)

const startDate = '2025-01-01'
const endDate = '2025-01-02'

const getExpDemoChartData = () =>
new Array(20).fill(0).map((_, index) => ({
period_start: new Date(startDate).getTime() + index * 1000,
demo: Math.floor(Math.pow(1.25, index) * 10),
max_demo: 1000,
}))

const getDemoChartData = () =>
new Array(20).fill(0).map((_, index) => ({
period_start: new Date(startDate).getTime() + index * 1000,
demo: Math.floor(Math.random() * 10) + 1,
max_demo: 1000,
}))

const demoChartData = useRef(getDemoChartData())
const exponentialChartData = useRef(getExpDemoChartData())

const demoData = isHoveringUpgrade ? exponentialChartData.current : demoChartData.current

return (
<Card className={cn('h-[260px] relative')}>
<div className="z-10 flex flex-col items-center justify-center space-y-2 h-full absolute top-0 left-0 w-full bg-surface-100/70 backdrop-blur-md">
<h2 className="text-sm">{report.label}</h2>
<p className="text-sm text-foreground-light">
This chart is available from{' '}
<span className="capitalize">
{!!report.availableIn?.length ? report.availableIn[0] : 'Pro'}
</span>{' '}
plan and above
</p>
<Button
asChild
type="primary"
onMouseEnter={() => setIsHoveringUpgrade(true)}
onMouseLeave={() => setIsHoveringUpgrade(false)}
className="mt-4"
>
<Link href={`/org/${orgSlug}/billing?panel=subscriptionPlan&source=reports`}>
Upgrade to{' '}
<span className="capitalize">
{!!report.availableIn?.length ? report.availableIn[0] : 'Pro'}
</span>
</Link>
</Button>
</div>
<div className="absolute top-0 left-0 w-full h-full z-0">
<LogChartHandler
attributes={[
{
attribute: 'demo',
enabled: true,
label: 'Demo',
provider: 'logs',
},
]}
label={''}
startDate={startDate}
endDate={endDate}
interval={'1d'}
data={demoData as any}
isLoading={false}
highlightedValue={0}
updateDateRange={() => {}}
/>
</div>
</Card>
)
}
Loading
Loading