mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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}`,
|
||||
|
||||
Reference in new issue
Block a user