Chore/uniform reports daterange picker (#36989)

* sync date range picker across reports
* sync report daterange pickers across reports using nuqs
This commit is contained in:
Francesco Sansalvadore authored and GitHub committed 2025-07-10 12:35:20 +02:00
1 parent 620805a1ac
commit a78026dc73
15 files changed
+680 -517

No files matched your search

@@ -1,4 +1,5 @@
import { useRouter } from 'next/router'
import { useMemo } from 'react'
import { toast } from 'sonner'
import { useContentUpsertMutation } from 'data/content/content-upsert-mutation'
@@ -20,11 +21,25 @@ export const CreateReportModal = ({ visible, onCancel, afterSubmit }: CreateRepo
const project = useSelectedProject()
const ref = project?.ref ?? 'default'
// Preserve date range query parameters when navigating to new report
const preservedQueryParams = useMemo(() => {
const { its, ite, isHelper, helperText } = router.query
const params = new URLSearchParams()
if (its && typeof its === 'string') params.set('its', its)
if (ite && typeof ite === 'string') params.set('ite', ite)
if (isHelper && typeof isHelper === 'string') params.set('isHelper', isHelper)
if (helperText && typeof helperText === 'string') params.set('helperText', helperText)
const queryString = params.toString()
return queryString ? `?${queryString}` : ''
}, [router.query])
const { mutate: upsertContent, isLoading: isCreating } = useContentUpsertMutation({
onSuccess: (_, vars) => {
toast.success('Successfully created new report')
const newReportId = vars.payload.id
router.push(`/project/${ref}/reports/${newReportId}`)
router.push(`/project/${ref}/reports/${newReportId}${preservedQueryParams}`)
afterSubmit()
},
onError: (error) => {
@@ -34,6 +34,7 @@ interface ReportFilterBarProps {
datepickerTo?: string
datepickerFrom?: string
datepickerHelpers: typeof REPORTS_DATEPICKER_HELPERS
initialDatePickerValue?: DatePickerValue
className?: string
selectedProduct?: string
showDatabaseSelector?: boolean
@@ -92,6 +93,7 @@ const ReportFilterBar = ({
onRemoveFilters,
onRefresh,
datepickerHelpers,
initialDatePickerValue,
className,
selectedProduct,
showDatabaseSelector = true,
@@ -158,13 +160,20 @@ const ReportFilterBar = ({
}
}, [])
const defaultHelper = datepickerHelpers[0]
const [selectedRange, setSelectedRange] = useState<DatePickerValue>({
to: defaultHelper.calcTo(),
from: defaultHelper.calcFrom(),
isHelper: true,
text: defaultHelper.text,
})
const getInitialDatePickerValue = () => {
if (initialDatePickerValue) {
return initialDatePickerValue
}
const defaultHelper = datepickerHelpers.find((h) => h.default) || datepickerHelpers[0]
return {
to: defaultHelper.calcTo(),
from: defaultHelper.calcFrom(),
isHelper: true,
text: defaultHelper.text,
}
}
const [selectedRange, setSelectedRange] = useState<DatePickerValue>(getInitialDatePickerValue())
return (
<div className={cn('flex items-center justify-between', className)}>
@@ -0,0 +1,20 @@
import { PropsWithChildren } from 'react'
import { cn } from 'ui'
const ReportStickyNav = ({
content,
children,
className,
}: PropsWithChildren<{ className?: string; content: React.ReactNode }>) => {
return (
<section className={cn('relative flex flex-col gap-4 pt-16 -mt-2', className)}>
<div className="absolute inset-0 z-40 pointer-events-none flex flex-col gap-4">
<div className="sticky top-0 py-4 mb-4 flex items-center gap-2 pointer-events-auto dark:bg-background-200 bg-background">
{content}
</div>
</div>
{children}
</section>
)
}
export default ReportStickyNav
@@ -6,56 +6,67 @@ import { PresetConfig, Presets, ReportFilterItem } from './Reports.types'
export const LAYOUT_COLUMN_COUNT = 2
interface ReportsDatetimeHelper extends DatetimeHelper {
export interface ReportsDatetimeHelper extends DatetimeHelper {
availableIn: PlanId[]
}
export enum REPORT_DATERANGE_HELPER_LABELS {
LAST_10_MINUTES = 'Last 10 minutes',
LAST_30_MINUTES = 'Last 30 minutes',
LAST_60_MINUTES = 'Last 60 minutes',
LAST_3_HOURS = 'Last 3 hours',
LAST_24_HOURS = 'Last 24 hours',
LAST_7_DAYS = 'Last 7 days',
LAST_14_DAYS = 'Last 14 days',
LAST_28_DAYS = 'Last 28 days',
}
export const REPORTS_DATEPICKER_HELPERS: ReportsDatetimeHelper[] = [
{
text: 'Last 10 minutes',
text: REPORT_DATERANGE_HELPER_LABELS.LAST_10_MINUTES,
calcFrom: () => dayjs().subtract(10, 'minute').toISOString(),
calcTo: () => dayjs().toISOString(),
availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
text: 'Last 30 minutes',
text: REPORT_DATERANGE_HELPER_LABELS.LAST_30_MINUTES,
calcFrom: () => dayjs().subtract(30, 'minute').toISOString(),
calcTo: () => dayjs().toISOString(),
availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
text: 'Last 60 minutes',
text: REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES,
calcFrom: () => dayjs().subtract(1, 'hour').startOf('day').toISOString(),
calcTo: () => dayjs().toISOString(),
default: true,
availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
text: 'Last 3 hours',
text: REPORT_DATERANGE_HELPER_LABELS.LAST_3_HOURS,
calcFrom: () => dayjs().subtract(3, 'hour').startOf('day').toISOString(),
calcTo: () => dayjs().toISOString(),
availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
text: 'Last 24 hours',
text: REPORT_DATERANGE_HELPER_LABELS.LAST_24_HOURS,
calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(),
calcTo: () => dayjs().toISOString(),
availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
text: 'Last 7 days',
text: REPORT_DATERANGE_HELPER_LABELS.LAST_7_DAYS,
calcFrom: () => dayjs().subtract(7, 'day').startOf('day').toISOString(),
calcTo: () => '',
availableIn: ['pro', 'team', 'enterprise'],
},
{
text: 'Last 14 days',
text: REPORT_DATERANGE_HELPER_LABELS.LAST_14_DAYS,
calcFrom: () => dayjs().subtract(14, 'day').startOf('day').toISOString(),
calcTo: () => '',
availableIn: ['team', 'enterprise'],
},
{
text: 'Last 28 days',
text: REPORT_DATERANGE_HELPER_LABELS.LAST_28_DAYS,
calcFrom: () => dayjs().subtract(28, 'day').startOf('day').toISOString(),
calcTo: () => '',
availableIn: ['team', 'enterprise'],
@@ -334,7 +345,7 @@ from edge_logs f
where starts_with(r.path, '/storage/v1/object')
and r.method = 'GET'
and h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC')
${generateRegexpWhere(filters, false)}
${generateRegexpWhere(filters, false)}
group by path, search
order by count desc
limit 12
@@ -368,7 +379,7 @@ select
statements.rows / statements.calls as avg_rows${
runIndexAdvisor
? `,
case
case
when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
then (
select json_build_object(
@@ -405,7 +416,7 @@ select
to_char(((statements.total_exec_time + statements.total_plan_time)/sum(statements.total_exec_time + statements.total_plan_time) OVER()) * 100, 'FM90D0') || '%' AS prop_total_time${
runIndexAdvisor
? `,
case
case
when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
then (
select json_build_object(
@@ -451,7 +462,7 @@ select
statements.rows / statements.calls as avg_rows${
runIndexAdvisor
? `,
case
case
when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
then (
select json_build_object(
@@ -495,12 +506,12 @@ select
largeObjects: {
queryType: 'db',
sql: (_) => `-- reports-database-large-objects
SELECT
SELECT
SCHEMA_NAME,
relname,
table_size
FROM
(SELECT
(SELECT
pg_catalog.pg_namespace.nspname AS SCHEMA_NAME,
relname,
pg_total_relation_size(pg_catalog.pg_class.oid) AS table_size
@@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Plus } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useState, useMemo } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
@@ -39,6 +39,20 @@ const ReportsMenu = () => {
subject: { id: profile?.id },
})
// Preserve date range query parameters when navigating
const preservedQueryParams = useMemo(() => {
const { its, ite, isHelper, helperText } = router.query
const params = new URLSearchParams()
if (its && typeof its === 'string') params.set('its', its)
if (ite && typeof ite === 'string') params.set('ite', ite)
if (isHelper && typeof isHelper === 'string') params.set('isHelper', isHelper)
if (helperText && typeof helperText === 'string') params.set('helperText', helperText)
const queryString = params.toString()
return queryString ? `?${queryString}` : ''
}, [router.query])
const { data: content, isLoading } = useContentQuery({
projectRef: ref,
type: 'report',
@@ -85,7 +99,7 @@ const ReportsMenu = () => {
name: r.name,
description: r.description || '',
key: r.id || idx + '-report',
url: `/project/${ref}/reports/${r.id}`,
url: `/project/${ref}/reports/${r.id}${preservedQueryParams}`,
hasDropdownActions: true,
report: r,
}))
@@ -103,28 +117,28 @@ const ReportsMenu = () => {
{
name: 'API Gateway',
key: 'api-overview',
url: `/project/${ref}/reports/api-overview`,
url: `/project/${ref}/reports/api-overview${preservedQueryParams}`,
},
...(authEnabled
? [
{
name: 'Auth',
key: 'auth',
url: `/project/${ref}/reports/auth`,
url: `/project/${ref}/reports/auth${preservedQueryParams}`,
},
]
: []),
{
name: 'Database',
key: 'database',
url: `/project/${ref}/reports/database`,
url: `/project/${ref}/reports/database${preservedQueryParams}`,
},
...(realtimeEnabled
? [
{
name: 'Realtime',
key: 'realtime',
url: `/project/${ref}/reports/realtime`,
url: `/project/${ref}/reports/realtime${preservedQueryParams}`,
},
]
: []),
@@ -133,7 +147,7 @@ const ReportsMenu = () => {
{
name: 'Edge Functions',
key: 'edge-functions',
url: `/project/${ref}/reports/edge-functions`,
url: `/project/${ref}/reports/edge-functions${preservedQueryParams}`,
},
]
: []),
@@ -142,7 +156,7 @@ const ReportsMenu = () => {
{
name: 'Storage',
key: 'storage',
url: `/project/${ref}/reports/storage`,
url: `/project/${ref}/reports/storage${preservedQueryParams}`,
},
]
: []),
@@ -264,7 +264,7 @@ const ComposedChartHandler = ({
<Panel
noMargin
noHideOverflow
className={cn('relative py-2 w-full scroll-mt-16', className)}
className={cn('relative w-full scroll-mt-16', className)}
wrapWithLoading={false}
id={id ?? label.toLowerCase().replaceAll(' ', '-')}
>
@@ -143,7 +143,7 @@ const LogChartHandler = ({
<Panel
noMargin
noHideOverflow
className={cn('relative py-2 w-full scroll-mt-16', className)}
className={cn('relative w-full scroll-mt-16', className)}
wrapWithLoading={false}
id={id ?? label.toLowerCase().replaceAll(' ', '-')}
>
@@ -0,0 +1,247 @@
import { useCallback, useMemo } from 'react'
import dayjs from 'dayjs'
import { createParser, useQueryState } from 'nuqs'
import { DatePickerValue } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
import {
createFilteredDatePickerHelpers,
REPORTS_DATEPICKER_HELPERS,
ReportsDatetimeHelper,
REPORT_DATERANGE_HELPER_LABELS,
} from 'components/interfaces/Reports/Reports.constants'
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
export interface ReportDateRange {
period_start: { date: string; time_period: string }
period_end: { date: string; time_period: string }
interval: string
}
// Create parsers for individual URL parameters
const stringWithDefault = (defaultValue: string) =>
createParser({
parse: (v) => v ?? defaultValue,
serialize: (v) => v || '',
})
const booleanWithDefault = (defaultValue: boolean) =>
createParser({
parse: (v) => {
if (v === 'true') return true
if (v === 'false') return false
return defaultValue
},
serialize: (v) => String(v),
})
export const useReportDateRange = (
defaultHelper:
| REPORT_DATERANGE_HELPER_LABELS
| string
| ReportsDatetimeHelper = REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES
) => {
const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
// Get filtered date picker helpers based on organization plan
const datePickerHelpers = createFilteredDatePickerHelpers(orgPlan?.id || 'free')
// Use nuqs for URL state management with individual parameters (empty defaults)
const [timestampStartValue, setTimestampStart] = useQueryState('its', stringWithDefault(''))
const [timestampEndValue, setTimestampEnd] = useQueryState('ite', stringWithDefault(''))
const [isHelperValue, setIsHelper] = useQueryState('isHelper', booleanWithDefault(false))
const [helperTextValue, setHelperText] = useQueryState('helperText', stringWithDefault(''))
const getDefaultHelper = useCallback(() => {
let targetHelper: ReportsDatetimeHelper | undefined
if (typeof defaultHelper === 'string') {
// Find helper by text (supports both enum values and direct strings)
targetHelper = REPORTS_DATEPICKER_HELPERS.find((helper) => helper.text === defaultHelper)
} else if (defaultHelper && typeof defaultHelper === 'object' && 'text' in defaultHelper) {
// Use the provided helper object directly
targetHelper = defaultHelper
}
// Check if the target helper is available for the current plan
if (targetHelper && targetHelper.availableIn?.includes(orgPlan?.id || 'free')) {
return {
start: targetHelper.calcFrom(),
end: targetHelper.calcTo(),
helper: { isHelper: true, text: targetHelper.text },
}
}
// Fallback: look for default helper marked in REPORTS_DATEPICKER_HELPERS
const fallbackHelper = REPORTS_DATEPICKER_HELPERS.find(
(helper) => helper.default && helper.availableIn?.includes(orgPlan?.id || 'free')
)
if (fallbackHelper) {
return {
start: fallbackHelper.calcFrom(),
end: fallbackHelper.calcTo(),
helper: { isHelper: true, text: fallbackHelper.text },
}
}
// Final fallback: use first available helper
const firstAvailable = REPORTS_DATEPICKER_HELPERS.find((helper) =>
helper.availableIn?.includes(orgPlan?.id || 'free')
)
if (firstAvailable) {
return {
start: firstAvailable.calcFrom(),
end: firstAvailable.calcTo(),
helper: { isHelper: true, text: firstAvailable.text },
}
}
// Ultimate fallback to manual date calculation (should rarely happen)
const defaultStart = dayjs().subtract(1, 'hour').toISOString()
const defaultEnd = dayjs().toISOString()
return {
start: defaultStart,
end: defaultEnd,
helper: { isHelper: false },
}
}, [defaultHelper, orgPlan?.id])
// Get current effective values (from URL or defaults, but don't set URL)
const timestampStart = useMemo(() => {
if (timestampStartValue) {
// Validate that stored date is reasonable
const storedFrom = dayjs(timestampStartValue)
const now = dayjs()
if (
storedFrom.isValid() &&
storedFrom.isAfter(now.subtract(90, 'day')) &&
storedFrom.isBefore(now.add(1, 'day'))
) {
return timestampStartValue
}
}
// Return default without setting URL
return getDefaultHelper().start
}, [timestampStartValue, getDefaultHelper])
const timestampEnd = useMemo(() => {
if (timestampEndValue) {
// Validate that stored date is reasonable
const storedTo = dayjs(timestampEndValue)
const now = dayjs()
if (
storedTo.isValid() &&
storedTo.isAfter(now.subtract(90, 'day')) &&
storedTo.isBefore(now.add(1, 'day'))
) {
return timestampEndValue
}
}
// Return default without setting URL
return getDefaultHelper().end
}, [timestampEndValue, getDefaultHelper])
// For helper state, use URL values or defaults from helper when no URL params exist
const isHelper = useMemo(() => {
if (timestampStartValue && timestampEndValue) {
return Boolean(isHelperValue)
}
// If no URL params, get default from helper
return getDefaultHelper().helper.isHelper
}, [timestampStartValue, timestampEndValue, isHelperValue, getDefaultHelper])
const helperText = useMemo(() => {
if (timestampStartValue && timestampEndValue) {
return helperTextValue || undefined
}
// If no URL params, get default from helper
return getDefaultHelper().helper.text
}, [timestampStartValue, timestampEndValue, helperTextValue, getDefaultHelper])
const handleIntervalGranularity = useCallback((from: string, to: string) => {
const conditions = {
'1m': dayjs(to).diff(from, 'hour') < 3, // less than 3 hours
'10m': dayjs(to).diff(from, 'hour') < 6, // less than 6 hours
'30m': dayjs(to).diff(from, 'hour') < 18, // less than 18 hours
'1h': dayjs(to).diff(from, 'day') < 10, // less than 10 days
'1d': dayjs(to).diff(from, 'day') >= 10, // more than 10 days
}
switch (true) {
case conditions['1m']:
return '1m'
case conditions['10m']:
return '10m'
case conditions['30m']:
return '30m'
default:
return '1h'
}
}, [])
// Derive selectedDateRange from current values
const selectedDateRange: ReportDateRange = useMemo(
() => ({
period_start: { date: timestampStart, time_period: '1d' },
period_end: { date: timestampEnd, time_period: 'today' },
interval: handleIntervalGranularity(timestampStart, timestampEnd),
}),
[timestampStart, timestampEnd, handleIntervalGranularity]
)
const updateDateRange = useCallback(
(from: string, to: string) => {
setTimestampStart(from)
setTimestampEnd(to)
setIsHelper(false)
setHelperText('')
},
[setTimestampStart, setTimestampEnd, setIsHelper, setHelperText]
)
const handleDatePickerChange = useCallback(
(values: DatePickerValue) => {
if (values.from && values.to) {
setTimestampStart(values.from)
setTimestampEnd(values.to)
setIsHelper(values.isHelper || false)
setHelperText(values.text || '')
}
},
[setTimestampStart, setTimestampEnd, setIsHelper, setHelperText]
)
// For backward compatibility, provide a setter that updates the derived state
const setSelectedDateRange = useCallback(
(newRange: ReportDateRange) => {
setTimestampStart(newRange.period_start.date)
setTimestampEnd(newRange.period_end.date)
// When setting directly, assume it's not from a helper
setIsHelper(false)
setHelperText('')
},
[setTimestampStart, setTimestampEnd, setIsHelper, setHelperText]
)
// Convert to DatePickerValue format for LogsDatePicker
const datePickerValue: DatePickerValue = useMemo(
() => ({
from: timestampStart,
to: timestampEnd,
isHelper,
text: helperText,
}),
[timestampStart, timestampEnd, isHelper, helperText]
)
return {
selectedDateRange,
setSelectedDateRange,
updateDateRange,
handleDatePickerChange,
datePickerValue,
datePickerHelpers,
isOrgPlanLoading,
orgPlan,
}
}
@@ -2,7 +2,6 @@ import ReportFilterBar from 'components/interfaces/Reports/ReportFilterBar'
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
import { createFilteredDatePickerHelpers } from 'components/interfaces/Reports/Reports.constants'
import {
ErrorCountsChartRenderer,
NetworkTrafficRenderer,
@@ -10,17 +9,18 @@ import {
TopApiRoutesRenderer,
TotalRequestsChartRenderer,
} from 'components/interfaces/Reports/renderers/ApiRenderers'
import type { DatePickerToFrom } from 'components/interfaces/Settings/Logs/Logs.types'
import { DatePickerValue } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
import DefaultLayout from 'components/layouts/DefaultLayout'
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
import ShimmerLine from 'components/ui/ShimmerLine'
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants'
import { useApiReport } from 'data/reports/api-report-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
import { NextPageWithLayout } from 'types'
export const ApiReport: NextPageWithLayout = () => {
const report = useApiReport()
const organization = useSelectedOrganization()
const {
data,
@@ -34,81 +34,87 @@ export const ApiReport: NextPageWithLayout = () => {
refresh,
} = report
const plan = organization?.plan
const { datePickerHelpers, datePickerValue, handleDatePickerChange } = useReportDateRange(
REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES
)
const handleDatepickerChange = ({ from, to }: DatePickerToFrom) => {
const handleDatepickerChange = (vals: DatePickerValue) => {
// Update localStorage and hook state
handleDatePickerChange(vals)
// Update query params for the report
mergeParams({
iso_timestamp_start: from || '',
iso_timestamp_end: to || '',
iso_timestamp_start: vals.from || '',
iso_timestamp_end: vals.to || '',
})
}
return (
<ReportPadding>
<ReportHeader title="API Gateway" showDatabaseSelector={false} />
<div className="w-full flex flex-col gap-1">
<ReportFilterBar
onRemoveFilters={removeFilters}
onDatepickerChange={handleDatepickerChange}
datepickerFrom={params.totalRequests.iso_timestamp_start}
datepickerTo={params.totalRequests.iso_timestamp_end}
onAddFilter={addFilter}
onRefresh={refresh}
<ReportStickyNav
content={
<ReportFilterBar
onRemoveFilters={removeFilters}
onDatepickerChange={handleDatepickerChange}
datepickerFrom={params.totalRequests.iso_timestamp_start}
datepickerTo={params.totalRequests.iso_timestamp_end}
onAddFilter={addFilter}
onRefresh={refresh}
isLoading={isLoading}
filters={filters}
datepickerHelpers={datePickerHelpers}
initialDatePickerValue={datePickerValue}
className="w-full"
showDatabaseSelector={false}
/>
}
>
<ReportWidget
isLoading={isLoading}
filters={filters}
datepickerHelpers={createFilteredDatePickerHelpers(plan?.id || 'free')}
showDatabaseSelector={false}
params={params.totalRequests}
title="Total Requests"
data={data.totalRequests || []}
error={error.totalRequest}
renderer={TotalRequestsChartRenderer}
append={TopApiRoutesRenderer}
appendProps={{ data: data.topRoutes || [], params: params.topRoutes }}
/>
<ReportWidget
isLoading={isLoading}
params={params.errorCounts}
title="Response Errors"
tooltip="Error responses with 4XX or 5XX status codes"
data={data.errorCounts || []}
error={error.errorCounts}
renderer={ErrorCountsChartRenderer}
appendProps={{
data: data.topErrorRoutes || [],
params: params.topErrorRoutes,
}}
append={TopApiRoutesRenderer}
/>
<ReportWidget
isLoading={isLoading}
params={params.responseSpeed}
title="Response Speed"
tooltip="Average response speed of a request (in ms)"
data={data.responseSpeed || []}
error={error.responseSpeed}
renderer={ResponseSpeedChartRenderer}
appendProps={{ data: data.topSlowRoutes || [], params: params.topSlowRoutes }}
append={TopApiRoutesRenderer}
/>
<div className="h-2 w-full">
<ShimmerLine active={isLoading} />
</div>
</div>
<ReportWidget
isLoading={isLoading}
params={params.totalRequests}
title="Total Requests"
data={data.totalRequests || []}
error={error.totalRequest}
renderer={TotalRequestsChartRenderer}
append={TopApiRoutesRenderer}
appendProps={{ data: data.topRoutes || [], params: params.topRoutes }}
/>
<ReportWidget
isLoading={isLoading}
params={params.errorCounts}
title="Response Errors"
tooltip="Error responses with 4XX or 5XX status codes"
data={data.errorCounts || []}
error={error.errorCounts}
renderer={ErrorCountsChartRenderer}
appendProps={{
data: data.topErrorRoutes || [],
params: params.topErrorRoutes,
}}
append={TopApiRoutesRenderer}
/>
<ReportWidget
isLoading={isLoading}
params={params.responseSpeed}
title="Response Speed"
tooltip="Average response speed of a request (in ms)"
data={data.responseSpeed || []}
error={error.responseSpeed}
renderer={ResponseSpeedChartRenderer}
appendProps={{ data: data.topSlowRoutes || [], params: params.topSlowRoutes }}
append={TopApiRoutesRenderer}
/>
<ReportWidget
isLoading={isLoading}
params={params.networkTraffic}
error={error.networkTraffic}
title="Network Traffic"
tooltip="Ingress and egress of requests and responses respectively"
data={data.networkTraffic || []}
renderer={NetworkTrafficRenderer}
/>
<ReportWidget
isLoading={isLoading}
params={params.networkTraffic}
error={error.networkTraffic}
title="Network Traffic"
tooltip="Ingress and egress of requests and responses respectively"
data={data.networkTraffic || []}
renderer={NetworkTrafficRenderer}
/>
</ReportStickyNav>
</ReportPadding>
)
}
@@ -9,13 +9,14 @@ import ReportPadding from 'components/interfaces/Reports/ReportPadding'
import DefaultLayout from 'components/layouts/DefaultLayout'
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DateRangePicker } from 'components/ui/DateRangePicker'
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
import { TIME_PERIODS_INFRA } from 'lib/constants/metrics'
import { getAuthReportAttributes } from 'data/reports/auth-charts'
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
import ReportChart from 'components/interfaces/Reports/ReportChart'
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
import { getAuthReportAttributes } from 'data/reports/auth-charts'
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants'
import type { NextPageWithLayout } from 'types'
const AuthReport: NextPageWithLayout = () => {
@@ -38,24 +39,24 @@ export default AuthReport
const AuthUsage = () => {
const { ref } = useParams()
const defaultStart = dayjs().subtract(1, 'day').toISOString()
const defaultEnd = dayjs().toISOString()
const [dateRange, setDateRange] = useState<any>({
period_start: { date: defaultStart, time_period: '1d' },
period_end: { date: defaultEnd, time_period: 'today' },
interval: '1h',
})
const {
selectedDateRange,
updateDateRange: updateDateRangeFromHook,
handleDatePickerChange,
datePickerValue,
datePickerHelpers,
isOrgPlanLoading,
orgPlan,
} = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES)
const queryClient = useQueryClient()
const [isRefreshing, setIsRefreshing] = useState(false)
const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
const isFreePlan = !isOrgPlanLoading && orgPlan?.id === 'free'
const AUTH_REPORT_ATTRIBUTES = getAuthReportAttributes(isFreePlan)
const onRefreshReport = async () => {
if (!dateRange) return
if (!selectedDateRange) return
setIsRefreshing(true)
AUTH_REPORT_ATTRIBUTES.forEach((attr) => {
@@ -64,50 +65,25 @@ const AuthUsage = () => {
'auth-metrics',
ref,
subAttr.attribute,
dateRange.period_start.date,
dateRange.period_end.date,
dateRange.interval,
selectedDateRange.period_start.date,
selectedDateRange.period_end.date,
selectedDateRange.interval,
])
})
})
setTimeout(() => setIsRefreshing(false), 1000)
}
const handleIntervalGranularity = (from: string, to: string) => {
const conditions = {
'1m': dayjs(to).diff(from, 'hour') < 3, // less than 3 hours
'10m': dayjs(to).diff(from, 'hour') < 6, // less than 6 hours
'30m': dayjs(to).diff(from, 'hour') < 18, // less than 18 hours
'1h': dayjs(to).diff(from, 'day') < 10, // less than 10 days
'1d': dayjs(to).diff(from, 'day') >= 10, // more than 10 days
}
switch (true) {
case conditions['1m']:
return '1m'
case conditions['10m']:
return '10m'
case conditions['30m']:
return '30m'
default:
return '1h'
}
}
const updateDateRange: UpdateDateRange = (from: string, to: string) => {
setDateRange({
period_start: { date: from, time_period: '1d' },
period_end: { date: to, time_period: 'today' },
interval: handleIntervalGranularity(from, to),
})
updateDateRangeFromHook(from, to)
}
return (
<>
<ReportHeader title="Auth" showDatabaseSelector={false} />
<section className="relative pt-16 -mt-2">
<div className="absolute inset-0 z-40 pointer-events-none flex flex-col gap-4">
<div className="sticky top-0 py-4 mb-4 flex items-center space-x-3 pointer-events-auto dark:bg-background-200 bg-background">
<ReportStickyNav
content={
<>
<ButtonTooltip
type="default"
disabled={isRefreshing}
@@ -117,50 +93,40 @@ const AuthUsage = () => {
onClick={onRefreshReport}
/>
<div className="flex items-center gap-3">
<DateRangePicker
loading={false}
value={'1d'}
options={TIME_PERIODS_INFRA}
currentBillingPeriodStart={undefined}
onChange={(values) => {
if (values.interval === '1d') {
setDateRange({ ...values, interval: '1h' })
} else {
setDateRange(values)
}
}}
<LogsDatePicker
onSubmit={handleDatePickerChange}
value={datePickerValue}
helpers={datePickerHelpers}
/>
{dateRange && (
{selectedDateRange && (
<div className="flex items-center gap-x-2 text-xs">
<p className="text-foreground-light">
{dayjs(dateRange.period_start.date).format('MMM D, h:mma')}
{dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')}
</p>
<p className="text-foreground-light">
<ArrowRight size={12} />
</p>
<p className="text-foreground-light">
{dayjs(dateRange.period_end.date).format('MMM D, h:mma')}
{dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')}
</p>
</div>
)}
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-4">
{dateRange &&
AUTH_REPORT_ATTRIBUTES.filter((attr) => !attr.hide).map((attr, i) => (
<ReportChart
key={`${attr.id}-${i}`}
chart={attr}
interval={dateRange.interval}
startDate={dateRange?.period_start?.date}
endDate={dateRange?.period_end?.date}
updateDateRange={updateDateRange}
/>
))}
</div>
</section>
</>
}
>
{selectedDateRange &&
AUTH_REPORT_ATTRIBUTES.filter((attr) => !attr.hide).map((attr, i) => (
<ReportChart
key={`${attr.id}-${i}`}
chart={attr}
interval={selectedDateRange.interval}
startDate={selectedDateRange?.period_start?.date}
endDate={selectedDateRange?.period_end?.date}
updateDateRange={updateDateRange}
/>
))}
</ReportStickyNav>
</>
)
}
@@ -19,10 +19,10 @@ import Table from 'components/to-be-cleaned/Table'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import ChartHandler from 'components/ui/Charts/ChartHandler'
import Panel from 'components/ui/Panel'
import ShimmerLine from 'components/ui/ShimmerLine'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import ComposedChartHandler from 'components/ui/Charts/ComposedChartHandler'
import { DateRangePicker } from 'components/ui/DateRangePicker'
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
import GrafanaPromoBanner from 'components/ui/GrafanaPromoBanner'
import { analyticsKeys } from 'data/analytics/keys'
@@ -31,10 +31,10 @@ import { useDatabaseSizeQuery } from 'data/database/database-size-query'
import { useDatabaseReport } from 'data/reports/database-report-query'
import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
import { useFlag } from 'hooks/ui/useFlag'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { TIME_PERIODS_INFRA } from 'lib/constants/metrics'
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants'
import { formatBytes } from 'lib/helpers'
import type { NextPageWithLayout } from 'types'
@@ -62,21 +62,23 @@ const DatabaseUsage = () => {
const { project } = useProjectContext()
const isReportsV2 = useFlag('reportsDatabaseV2')
const org = useSelectedOrganization()
const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
const {
selectedDateRange,
updateDateRange: updateDateRangeFromHook,
handleDatePickerChange,
datePickerValue,
datePickerHelpers,
isOrgPlanLoading,
orgPlan,
} = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES)
const isFreePlan = !isOrgPlanLoading && orgPlan?.id === 'free'
const isTeamsOrEnterprisePlan =
!isOrgPlanLoading && (orgPlan?.id === 'team' || orgPlan?.id === 'enterprise')
const showChartsV2 = isReportsV2 || isTeamsOrEnterprisePlan
const state = useDatabaseSelectorStateSnapshot()
const defaultStart = dayjs().subtract(1, 'day').toISOString()
const defaultEnd = dayjs().toISOString()
const [dateRange, setDateRange] = useState<any>({
period_start: { date: defaultStart, time_period: '1d' },
period_end: { date: defaultEnd, time_period: 'today' },
interval: '1h',
})
const queryClient = useQueryClient()
const [isRefreshing, setIsRefreshing] = useState(false)
@@ -111,19 +113,19 @@ const DatabaseUsage = () => {
})
const onRefreshReport = async () => {
if (!dateRange) return
if (!selectedDateRange) return
// [Joshen] Since we can't track individual loading states for each chart
// so for now we mock a loading state that only lasts for a second
setIsRefreshing(true)
refresh()
const { period_start, interval } = dateRange
const { period_start, period_end, interval } = selectedDateRange
REPORT_ATTRIBUTES.forEach((attr) => {
queryClient.invalidateQueries(
analyticsKeys.infraMonitoring(ref, {
attribute: attr?.id,
startDate: period_start.date,
endDate: period_start.end,
endDate: period_end.date,
interval,
databaseIdentifier: state.selectedDatabaseId,
})
@@ -136,7 +138,7 @@ const DatabaseUsage = () => {
analyticsKeys.infraMonitoring(ref, {
attribute: attr.attribute,
startDate: period_start.date,
endDate: period_start.end,
endDate: period_end.date,
interval,
databaseIdentifier: state.selectedDatabaseId,
})
@@ -149,7 +151,7 @@ const DatabaseUsage = () => {
analyticsKeys.infraMonitoring(ref, {
attribute: 'physical_replication_lag_physical_replica_lag_seconds',
startDate: period_start.date,
endDate: period_start.end,
endDate: period_end.date,
interval,
databaseIdentifier: state.selectedDatabaseId,
})
@@ -176,47 +178,17 @@ const DatabaseUsage = () => {
}
}, [db, chart])
const handleIntervalGranularity = (from: string, to: string) => {
const conditions = {
'1m': dayjs(to).diff(from, 'hour') < 3, // less than 3 hours
'10m': dayjs(to).diff(from, 'hour') < 6, // less than 6 hours
'30m': dayjs(to).diff(from, 'hour') < 18, // less than 18 hours
'1h': dayjs(to).diff(from, 'day') < 10, // less than 10 days
'1d': dayjs(to).diff(from, 'day') >= 10, // more than 10 days
}
switch (true) {
case conditions['1m']:
return '1m'
case conditions['10m']:
return '10m'
case conditions['30m']:
return '30m'
default:
return '1h'
}
}
const updateDateRange: UpdateDateRange = (from: string, to: string) => {
setDateRange({
period_start: { date: from, time_period: '1d' },
period_end: { date: to, time_period: 'today' },
interval: handleIntervalGranularity(from, to),
})
updateDateRangeFromHook(from, to)
}
return (
<>
<ReportHeader showDatabaseSelector title="Database" />
<div className="w-full flex flex-col gap-1">
<div className="h-2 w-full">
<ShimmerLine active={report.isLoading} />
</div>
</div>
<GrafanaPromoBanner />
<section className="relative pt-16 -mt-2">
<div className="absolute inset-0 z-40 pointer-events-none flex flex-col gap-4">
<div className="sticky top-0 bg-200 py-4 mb-4 flex items-center space-x-3 pointer-events-auto">
<ReportStickyNav
content={
<>
<ButtonTooltip
type="default"
disabled={isRefreshing}
@@ -226,60 +198,51 @@ const DatabaseUsage = () => {
onClick={onRefreshReport}
/>
<div className="flex items-center gap-3">
<DateRangePicker
loading={false}
value={'1d'}
options={TIME_PERIODS_INFRA}
currentBillingPeriodStart={undefined}
onChange={(values) => {
if (values.interval === '1d') {
setDateRange({ ...values, interval: '1h' })
} else {
setDateRange(values)
}
}}
<LogsDatePicker
onSubmit={handleDatePickerChange}
value={datePickerValue}
helpers={datePickerHelpers}
/>
{dateRange && (
{selectedDateRange && (
<div className="flex items-center gap-x-2 text-xs">
<p className="text-foreground-light">
{dayjs(dateRange.period_start.date).format('MMM D, h:mma')}
{dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')}
</p>
<p className="text-foreground-light">
<ArrowRight size={12} />
</p>
<p className="text-foreground-light">
{dayjs(dateRange.period_end.date).format('MMM D, h:mma')}
{dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')}
</p>
</div>
)}
</div>
</div>
</div>
</>
}
>
{showChartsV2 ? (
<div className="grid grid-cols-1 gap-4">
{dateRange &&
REPORT_ATTRIBUTES_V2.filter((chart) => !chart.hide).map((chart) => (
<ComposedChartHandler
key={chart.id}
{...chart}
attributes={chart.attributes as MultiAttribute[]}
interval={dateRange.interval}
startDate={dateRange?.period_start?.date}
endDate={dateRange?.period_end?.date}
updateDateRange={updateDateRange}
defaultChartStyle={chart.defaultChartStyle as 'line' | 'bar' | 'stackedAreaLine'}
showMaxValue={
chart.id === 'client-connections' || chart.id === 'pgbouncer-connections'
? true
: chart.showMaxValue
}
/>
))}
</div>
selectedDateRange &&
REPORT_ATTRIBUTES_V2.filter((chart) => !chart.hide).map((chart) => (
<ComposedChartHandler
key={chart.id}
{...chart}
attributes={chart.attributes as MultiAttribute[]}
interval={selectedDateRange.interval}
startDate={selectedDateRange?.period_start?.date}
endDate={selectedDateRange?.period_end?.date}
updateDateRange={updateDateRange}
defaultChartStyle={chart.defaultChartStyle as 'line' | 'bar' | 'stackedAreaLine'}
showMaxValue={
chart.id === 'client-connections' || chart.id === 'pgbouncer-connections'
? true
: chart.showMaxValue
}
/>
))
) : (
<Panel title={<h2>Database health</h2>}>
<Panel.Content className="grid grid-cols-1 gap-4">
{dateRange &&
{selectedDateRange &&
REPORT_ATTRIBUTES.filter((attr) => !attr.hide).map((attr) => (
<ChartHandler
key={attr.id}
@@ -287,31 +250,31 @@ const DatabaseUsage = () => {
provider="infra-monitoring"
attribute={attr.id}
label={attr.label}
interval={dateRange.interval}
startDate={dateRange?.period_start?.date}
endDate={dateRange?.period_end?.date}
interval={selectedDateRange.interval}
startDate={selectedDateRange?.period_start?.date}
endDate={selectedDateRange?.period_end?.date}
/>
))}
</Panel.Content>
</Panel>
)}
{dateRange && isReplicaSelected && (
{selectedDateRange && isReplicaSelected && (
<Panel title="Replica Information">
<Panel.Content>
<div id="replication-lag">
<ChartHandler
startDate={dateRange?.period_start?.date}
endDate={dateRange?.period_end?.date}
startDate={selectedDateRange?.period_start?.date}
endDate={selectedDateRange?.period_end?.date}
attribute="physical_replication_lag_physical_replica_lag_seconds"
label="Replication lag"
interval={dateRange.interval}
interval={selectedDateRange.interval}
provider="infra-monitoring"
/>
</div>
</Panel.Content>
</Panel>
)}
</section>
</ReportStickyNav>
<section id="database-size-report">
<ReportWidget
isLoading={isLoading}
@@ -8,20 +8,19 @@ import ReportHeader from 'components/interfaces/Reports/ReportHeader'
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
import DefaultLayout from 'components/layouts/DefaultLayout'
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import ShimmerLine from 'components/ui/ShimmerLine'
import { DateRangePicker } from 'components/ui/DateRangePicker'
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
import { TIME_PERIODS_INFRA } from 'lib/constants/metrics'
import type { NextPageWithLayout } from 'types'
import { Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui'
import ReportChart from 'components/interfaces/Reports/ReportChart'
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
import { Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui'
import { Label } from '@ui/components/shadcn/ui/label'
import { getEdgeFunctionReportAttributes } from 'data/reports/edgefn-charts'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { Label } from '@ui/components/shadcn/ui/label'
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants'
import type { NextPageWithLayout } from 'types'
const EdgeFunctionsReport: NextPageWithLayout = () => {
return (
@@ -55,56 +54,29 @@ const EdgeFunctionsUsage = () => {
}
}, [isOpen, functionIds])
const defaultStart = dayjs().subtract(1, 'day').toISOString()
const defaultEnd = dayjs().toISOString()
const [dateRange, setDateRange] = useState<any>({
period_start: { date: defaultStart, time_period: '1d' },
period_end: { date: defaultEnd, time_period: 'today' },
interval: '1h',
})
const {
selectedDateRange,
updateDateRange: updateDateRangeFromHook,
handleDatePickerChange,
datePickerValue,
datePickerHelpers,
} = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES)
const queryClient = useQueryClient()
const [isRefreshing, setIsRefreshing] = useState(false)
useCurrentOrgPlan()
const EDGEFN_CHARTS = getEdgeFunctionReportAttributes()
const onRefreshReport = async () => {
if (!dateRange) return
if (!selectedDateRange) return
setIsRefreshing(true)
queryClient.invalidateQueries(['edge-function-report', ref])
setTimeout(() => setIsRefreshing(false), 1000)
}
const handleIntervalGranularity = (from: string, to: string) => {
const conditions = {
'1m': dayjs(to).diff(from, 'hour') < 3, // less than 3 hours
'10m': dayjs(to).diff(from, 'hour') < 6, // less than 6 hours
'30m': dayjs(to).diff(from, 'hour') < 18, // less than 18 hours
'1h': dayjs(to).diff(from, 'day') < 10, // less than 10 days
'1d': dayjs(to).diff(from, 'day') >= 10, // more than 10 days
}
switch (true) {
case conditions['1m']:
return '1m'
case conditions['10m']:
return '10m'
case conditions['30m']:
return '30m'
default:
return '1h'
}
}
const updateDateRange: UpdateDateRange = (from: string, to: string) => {
setDateRange({
period_start: { date: from, time_period: '1d' },
period_end: { date: to, time_period: 'today' },
interval: handleIntervalGranularity(from, to),
})
updateDateRangeFromHook(from, to)
}
if (!ref) {
@@ -115,14 +87,9 @@ const EdgeFunctionsUsage = () => {
return (
<>
<ReportHeader title="Edge Functions" showDatabaseSelector={false} />
<div className="w-full flex flex-col gap-1">
<div className="h-2 w-full">
<ShimmerLine active={isRefreshing || isLoadingFunctions} />
</div>
</div>
<section className="relative pt-16 -mt-2">
<div className="absolute inset-0 z-40 pointer-events-none flex flex-col gap-4">
<div className="sticky top-0 bg-200 py-4 mb-4 flex items-center space-x-3 pointer-events-auto">
<ReportStickyNav
content={
<>
<ButtonTooltip
type="default"
disabled={isRefreshing}
@@ -131,19 +98,11 @@ const EdgeFunctionsUsage = () => {
tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }}
onClick={onRefreshReport}
/>
<div className="flex items-center gap-3">
<DateRangePicker
loading={false}
value={'1d'}
options={TIME_PERIODS_INFRA}
currentBillingPeriodStart={undefined}
onChange={(values: any) => {
if (values.interval === '1d') {
setDateRange({ ...values, interval: '1h' })
} else {
setDateRange(values)
}
}}
<div className="flex items-center gap-2">
<LogsDatePicker
onSubmit={handleDatePickerChange}
value={datePickerValue}
helpers={datePickerHelpers}
/>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
@@ -227,38 +186,36 @@ const EdgeFunctionsUsage = () => {
</div>
</DropdownMenuContent>
</DropdownMenu>
{dateRange && (
{selectedDateRange && (
<div className="flex items-center gap-x-2 text-xs">
<p className="text-foreground-light">
{dayjs(dateRange.period_start.date).format('MMM D, h:mma')}
{dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')}
</p>
<p className="text-foreground-light">
<ArrowRight size={12} />
</p>
<p className="text-foreground-light">
{dayjs(dateRange.period_end.date).format('MMM D, h:mma')}
{dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')}
</p>
</div>
)}
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-4">
{dateRange &&
EDGEFN_CHARTS.filter((attr) => !attr.hide).map((attr, i) => (
<ReportChart
key={`${attr.id}-${i}`}
chart={attr}
interval={dateRange.interval}
startDate={dateRange?.period_start?.date}
endDate={dateRange?.period_end?.date}
updateDateRange={updateDateRange}
functionIds={functionIds}
/>
))}
</div>
</section>
</>
}
>
{selectedDateRange &&
EDGEFN_CHARTS.filter((attr) => !attr.hide).map((attr, i) => (
<ReportChart
key={`${attr.id}-${i}`}
chart={attr}
interval={selectedDateRange.interval}
startDate={selectedDateRange?.period_start?.date}
endDate={selectedDateRange?.period_end?.date}
updateDateRange={updateDateRange}
functionIds={functionIds}
/>
))}
</ReportStickyNav>
</>
)
}
@@ -10,8 +10,7 @@ import ReportPadding from 'components/interfaces/Reports/ReportPadding'
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { DateRangePicker } from 'components/ui/DateRangePicker'
import { createFilteredDatePickerHelpers } from 'components/interfaces/Reports/Reports.constants'
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
import {
ResponseSpeedChartRenderer,
TopApiRoutesRenderer,
@@ -20,13 +19,13 @@ import {
import ComposedChartHandler from 'components/ui/Charts/ComposedChartHandler'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
import ReportFilterBar from 'components/interfaces/Reports/ReportFilterBar'
import { DatePickerValue } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
import { analyticsKeys } from 'data/analytics/keys'
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
import { TIME_PERIODS_INFRA } from 'lib/constants/metrics'
import { getRealtimeReportAttributes } from 'data/reports/realtime-charts'
import { useApiReport } from 'data/reports/api-report-query'
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants'
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
import type { NextPageWithLayout } from 'types'
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
@@ -66,35 +65,35 @@ const RealtimeUsage = () => {
} = report
const state = useDatabaseSelectorStateSnapshot()
const defaultStart = dayjs().subtract(7, 'day').toISOString()
const defaultEnd = dayjs().toISOString()
const [dateRange, setDateRange] = useState<any>({
period_start: { date: defaultStart, time_period: '1d' },
period_end: { date: defaultEnd, time_period: 'today' },
interval: '1h',
})
const {
selectedDateRange,
updateDateRange: updateDateRangeFromHook,
handleDatePickerChange,
datePickerValue,
datePickerHelpers,
isOrgPlanLoading,
orgPlan,
} = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES)
const queryClient = useQueryClient()
const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
const isFreePlan = !isOrgPlanLoading && orgPlan?.id === 'free'
const REALTIME_REPORT_ATTRIBUTES = getRealtimeReportAttributes(isFreePlan)
const onRefreshReport = async () => {
if (!dateRange) return
if (!selectedDateRange) return
// [Joshen] Since we can't track individual loading states for each chart
// so for now we mock a loading state that only lasts for a second
setIsRefreshing(true)
const { period_start, interval } = dateRange
const { period_start, period_end, interval } = selectedDateRange
REALTIME_REPORT_ATTRIBUTES.forEach((attr) => {
queryClient.invalidateQueries(
analyticsKeys.infraMonitoring(ref, {
attribute: attr?.id,
startDate: period_start.date,
endDate: period_start.end,
endDate: period_end.date,
interval,
databaseIdentifier: state.selectedDatabaseId,
})
@@ -124,56 +123,20 @@ const RealtimeUsage = () => {
}
}, [])
const handleIntervalGranularity = (from: string, to: string) => {
const conditions = {
'1m': dayjs(to).diff(from, 'hour') < 3, // less than 3 hours
'10m': dayjs(to).diff(from, 'hour') < 6, // less than 6 hours
'30m': dayjs(to).diff(from, 'hour') < 18, // less than 18 hours
'1h': dayjs(to).diff(from, 'day') < 10, // less than 10 days
'1d': dayjs(to).diff(from, 'day') >= 10, // more than 10 days
}
switch (true) {
case conditions['1m']:
return '1m'
case conditions['10m']:
return '10m'
case conditions['30m']:
return '30m'
default:
return '1h'
}
}
const updateDateRange: UpdateDateRange = (from: string, to: string) => {
setDateRange({
period_start: { date: from, time_period: '1d' },
period_end: { date: to, time_period: 'today' },
interval: handleIntervalGranularity(from, to),
})
updateDateRangeFromHook(from, to)
report.mergeParams({
iso_timestamp_start: from,
iso_timestamp_end: to,
})
}
// const handleDatepickerChange = (vals: DatePickerValue) => {
// report.mergeParams({
// iso_timestamp_start: vals.from || '',
// iso_timestamp_end: vals.to || '',
// })
// setDateRange({
// period_start: { date: vals.from || '', time_period: '1d' },
// period_end: { date: vals.to || '', time_period: 'today' },
// })
// }
return (
<>
<ReportHeader showDatabaseSelector={false} title="Realtime" />
<section className="relative pt-16 -mt-2">
<div className="absolute inset-0 z-40 pointer-events-none flex flex-col gap-4">
<div className="sticky top-0 bg-200 py-4 mb-4 flex items-center space-x-3 pointer-events-auto">
<ReportStickyNav
content={
<>
<ButtonTooltip
type="default"
disabled={isRefreshing}
@@ -183,96 +146,82 @@ const RealtimeUsage = () => {
onClick={onRefreshReport}
/>
<div className="flex items-center gap-3">
<DateRangePicker
loading={false}
value="1d"
options={TIME_PERIODS_INFRA}
currentBillingPeriodStart={undefined}
onChange={(values) => {
if (values.interval === '1d') {
setDateRange({ ...values, interval: '1h' })
} else {
setDateRange(values)
}
}}
<LogsDatePicker
onSubmit={handleDatePickerChange}
value={datePickerValue}
helpers={datePickerHelpers}
/>
{dateRange && (
{selectedDateRange && (
<div className="flex items-center gap-x-2 text-xs">
<p className="text-foreground-light">
{dayjs(dateRange.period_start.date).format('MMM D, h:mma')}
{dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')}
</p>
<p className="text-foreground-light">
<ArrowRight size={12} />
</p>
<p className="text-foreground-light">
{dayjs(dateRange.period_end.date).format('MMM D, h:mma')}
{dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')}
</p>
</div>
)}
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-4">
{dateRange &&
REALTIME_REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart) => (
<ComposedChartHandler
key={chart.id}
{...chart}
attributes={chart.attributes as MultiAttribute[]}
interval={dateRange.interval}
startDate={dateRange?.period_start?.date}
endDate={dateRange?.period_end?.date}
updateDateRange={updateDateRange}
defaultChartStyle={chart.defaultChartStyle as 'line' | 'bar' | 'stackedAreaLine'}
/>
))}
</div>
<div className="relative pt-16 mt-4 border-t">
<div className="absolute inset-0 z-40 pointer-events-none flex flex-col gap-4">
<div className="sticky top-0 bg-200 py-4 mb-4 flex flex-col items-center pointer-events-auto gap-4">
<ReportFilterBar
onRemoveFilters={removeFilters}
hideDatepicker={true}
// onDatepickerChange={handleDatepickerChange}
datepickerFrom={dateRange.from}
datepickerTo={dateRange.to}
onAddFilter={addFilter}
// onRefresh={refresh}
isLoading={isLoading}
filters={filters}
selectedProduct="realtime"
datepickerHelpers={createFilteredDatePickerHelpers(orgPlan?.id || 'free')}
className="w-full"
showDatabaseSelector={false}
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4">
<ReportWidget
isLoading={isLoading}
params={params.totalRequests}
title="Total Requests"
data={data.totalRequests || []}
error={error.totalRequest}
renderer={TotalRequestsChartRenderer}
append={TopApiRoutesRenderer}
appendProps={{ data: data.topRoutes || [], params: params.topRoutes }}
</>
}
>
{selectedDateRange &&
REALTIME_REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart) => (
<ComposedChartHandler
key={chart.id}
{...chart}
attributes={chart.attributes as MultiAttribute[]}
interval={selectedDateRange.interval}
startDate={selectedDateRange?.period_start?.date}
endDate={selectedDateRange?.period_end?.date}
updateDateRange={updateDateRange}
defaultChartStyle={chart.defaultChartStyle as 'line' | 'bar' | 'stackedAreaLine'}
/>
<ReportWidget
))}
<ReportStickyNav
className="mt-0 border-t"
content={
<ReportFilterBar
onRemoveFilters={removeFilters}
hideDatepicker={true}
datepickerFrom={selectedDateRange.period_start.date}
datepickerTo={selectedDateRange.period_end.date}
onAddFilter={addFilter}
isLoading={isLoading}
params={params.responseSpeed}
title="Response Speed"
tooltip="Average response speed (in miliseconds) of a request"
data={data.responseSpeed || []}
error={error.responseSpeed}
renderer={ResponseSpeedChartRenderer}
appendProps={{ data: data.topSlowRoutes || [], params: params.topSlowRoutes }}
append={TopApiRoutesRenderer}
filters={filters}
selectedProduct="realtime"
datepickerHelpers={datePickerHelpers}
className="w-full"
showDatabaseSelector={false}
/>
</div>
</div>
</section>
}
>
<ReportWidget
isLoading={isLoading}
params={params.totalRequests}
title="Total Requests"
data={data.totalRequests || []}
error={error.totalRequest}
renderer={TotalRequestsChartRenderer}
append={TopApiRoutesRenderer}
appendProps={{ data: data.topRoutes || [], params: params.topRoutes }}
/>
<ReportWidget
isLoading={isLoading}
params={params.responseSpeed}
title="Response Speed"
tooltip="Average response speed (in miliseconds) of a request"
data={data.responseSpeed || []}
error={error.responseSpeed}
renderer={ResponseSpeedChartRenderer}
appendProps={{ data: data.topSlowRoutes || [], params: params.topSlowRoutes }}
append={TopApiRoutesRenderer}
/>
</ReportStickyNav>
</ReportStickyNav>
</>
)
}
@@ -1,8 +1,9 @@
import Link from 'next/link'
import { ExternalLinkIcon } from 'lucide-react'
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
import ReportFilterBar from 'components/interfaces/Reports/ReportFilterBar'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
import { createFilteredDatePickerHelpers } from 'components/interfaces/Reports/Reports.constants'
import {
CacheHitRateChartRenderer,
TopCacheMissesRenderer,
@@ -16,13 +17,14 @@ import {
TotalRequestsChartRenderer,
} from 'components/interfaces/Reports/renderers/ApiRenderers'
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
import ShimmerLine from 'components/ui/ShimmerLine'
import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants'
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
import { useStorageReport } from 'data/reports/storage-report-query'
import type { NextPageWithLayout } from 'types'
import Link from 'next/link'
import { ExternalLinkIcon } from 'lucide-react'
export const StorageReport: NextPageWithLayout = () => {
const report = useStorageReport()
@@ -40,9 +42,14 @@ export const StorageReport: NextPageWithLayout = () => {
refresh,
} = report
const plan = organization?.plan
const { datePickerHelpers, datePickerValue, handleDatePickerChange } = useReportDateRange(
REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES
)
const handleDatepickerChange = (vals: DatePickerValue) => {
// Update localStorage and hook state
handleDatePickerChange(vals)
// Update query params for the report
mergeParams({
iso_timestamp_start: vals.from || '',
iso_timestamp_end: vals.to || '',
@@ -52,9 +59,9 @@ export const StorageReport: NextPageWithLayout = () => {
return (
<ReportPadding>
<ReportHeader title="Storage" showDatabaseSelector={false} />
<section className="relative pt-20 -mt-2 flex flex-col gap-3">
<div className="absolute inset-0 z-40 pointer-events-none flex flex-col gap-4">
<div className="sticky top-0 bg dark:bg-200 pt-4 mb-4 flex flex-col items-center pointer-events-auto gap-4">
<ReportStickyNav
content={
<>
<ReportFilterBar
onRemoveFilters={removeFilters}
onDatepickerChange={handleDatepickerChange}
@@ -65,16 +72,14 @@ export const StorageReport: NextPageWithLayout = () => {
isLoading={isLoading}
filters={filters}
selectedProduct="storage"
datepickerHelpers={createFilteredDatePickerHelpers(plan?.id || 'free')}
datepickerHelpers={datePickerHelpers}
initialDatePickerValue={datePickerValue}
className="w-full"
showDatabaseSelector={false}
/>
<div className="h-px w-full">
<ShimmerLine active={report.isLoading} />
</div>
</div>
</div>
</>
}
>
<ReportWidget
isLoading={isLoading}
params={params.totalRequests}
@@ -130,7 +135,7 @@ export const StorageReport: NextPageWithLayout = () => {
append={TopCacheMissesRenderer}
appendProps={{ data: data.topCacheMisses || [] }}
/>
</section>
</ReportStickyNav>
</ReportPadding>
)
}
@@ -53,6 +53,7 @@ export const LOCAL_STORAGE_KEYS = {
API_KEYS_FEEDBACK_DISMISSED: (ref: string) => `supabase-api-keys-feedback-dismissed-${ref}`,
MIDDLEWARE_OUTAGE_BANNER: 'middleware-outage-banner-2025-05-16',
AUTH_USERS_COLUMNS_CONFIGURATION: (ref: string) => `supabase-auth-users-columns-${ref}`,
REPORT_DATERANGE: 'supabase-report-daterange',
// api keys view switcher for new and legacy api keys
API_KEYS_VIEW: (ref: string) => `supabase-api-keys-view-${ref}`,