diff --git a/apps/studio/components/interfaces/Reports/CreateReportModal.tsx b/apps/studio/components/interfaces/Reports/CreateReportModal.tsx index adbe1cd5150..22b4ec79af8 100644 --- a/apps/studio/components/interfaces/Reports/CreateReportModal.tsx +++ b/apps/studio/components/interfaces/Reports/CreateReportModal.tsx @@ -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) => { diff --git a/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx b/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx index c4ec76fd5ec..6d4b3dc2a16 100644 --- a/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx +++ b/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx @@ -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({ - 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(getInitialDatePickerValue()) return (
diff --git a/apps/studio/components/interfaces/Reports/ReportStickyNav.tsx b/apps/studio/components/interfaces/Reports/ReportStickyNav.tsx new file mode 100644 index 00000000000..81e8ecf1505 --- /dev/null +++ b/apps/studio/components/interfaces/Reports/ReportStickyNav.tsx @@ -0,0 +1,20 @@ +import { PropsWithChildren } from 'react' +import { cn } from 'ui' + +const ReportStickyNav = ({ + content, + children, + className, +}: PropsWithChildren<{ className?: string; content: React.ReactNode }>) => { + return ( +
+
+
+ {content} +
+
+ {children} +
+ ) +} +export default ReportStickyNav diff --git a/apps/studio/components/interfaces/Reports/Reports.constants.ts b/apps/studio/components/interfaces/Reports/Reports.constants.ts index 32df6f8d496..a46746ec604 100644 --- a/apps/studio/components/interfaces/Reports/Reports.constants.ts +++ b/apps/studio/components/interfaces/Reports/Reports.constants.ts @@ -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 diff --git a/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx b/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx index 6c801e6617e..1ebb482970c 100644 --- a/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx +++ b/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx @@ -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}`, }, ] : []), diff --git a/apps/studio/components/ui/Charts/ComposedChartHandler.tsx b/apps/studio/components/ui/Charts/ComposedChartHandler.tsx index ea20384af40..833730cf659 100644 --- a/apps/studio/components/ui/Charts/ComposedChartHandler.tsx +++ b/apps/studio/components/ui/Charts/ComposedChartHandler.tsx @@ -264,7 +264,7 @@ const ComposedChartHandler = ({ diff --git a/apps/studio/components/ui/Charts/LogChartHandler.tsx b/apps/studio/components/ui/Charts/LogChartHandler.tsx index 101b28c485c..ee21b67be55 100644 --- a/apps/studio/components/ui/Charts/LogChartHandler.tsx +++ b/apps/studio/components/ui/Charts/LogChartHandler.tsx @@ -143,7 +143,7 @@ const LogChartHandler = ({ diff --git a/apps/studio/hooks/misc/useReportDateRange.ts b/apps/studio/hooks/misc/useReportDateRange.ts new file mode 100644 index 00000000000..216448751bc --- /dev/null +++ b/apps/studio/hooks/misc/useReportDateRange.ts @@ -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, + } +} diff --git a/apps/studio/pages/project/[ref]/reports/api-overview.tsx b/apps/studio/pages/project/[ref]/reports/api-overview.tsx index 3061f85beb3..ac4fbf258d2 100644 --- a/apps/studio/pages/project/[ref]/reports/api-overview.tsx +++ b/apps/studio/pages/project/[ref]/reports/api-overview.tsx @@ -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 ( -
- + } + > + + + -
- -
-
- - - - - + +
) } diff --git a/apps/studio/pages/project/[ref]/reports/auth.tsx b/apps/studio/pages/project/[ref]/reports/auth.tsx index f3175791982..edf622e70f1 100644 --- a/apps/studio/pages/project/[ref]/reports/auth.tsx +++ b/apps/studio/pages/project/[ref]/reports/auth.tsx @@ -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({ - 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 ( <> -
-
-
+ { onClick={onRefreshReport} />
- { - if (values.interval === '1d') { - setDateRange({ ...values, interval: '1h' }) - } else { - setDateRange(values) - } - }} + - {dateRange && ( + {selectedDateRange && (

- {dayjs(dateRange.period_start.date).format('MMM D, h:mma')} + {dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')}

- {dayjs(dateRange.period_end.date).format('MMM D, h:mma')} + {dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')}

)}
-
-
- -
- {dateRange && - AUTH_REPORT_ATTRIBUTES.filter((attr) => !attr.hide).map((attr, i) => ( - - ))} -
-
+ + } + > + {selectedDateRange && + AUTH_REPORT_ATTRIBUTES.filter((attr) => !attr.hide).map((attr, i) => ( + + ))} + ) } diff --git a/apps/studio/pages/project/[ref]/reports/database.tsx b/apps/studio/pages/project/[ref]/reports/database.tsx index 7e883005489..5aac7249277 100644 --- a/apps/studio/pages/project/[ref]/reports/database.tsx +++ b/apps/studio/pages/project/[ref]/reports/database.tsx @@ -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({ - 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 ( <> -
-
- -
-
-
-
-
+ { onClick={onRefreshReport} />
- { - if (values.interval === '1d') { - setDateRange({ ...values, interval: '1h' }) - } else { - setDateRange(values) - } - }} + - {dateRange && ( + {selectedDateRange && (

- {dayjs(dateRange.period_start.date).format('MMM D, h:mma')} + {dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')}

- {dayjs(dateRange.period_end.date).format('MMM D, h:mma')} + {dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')}

)}
-
-
+ + } + > {showChartsV2 ? ( -
- {dateRange && - REPORT_ATTRIBUTES_V2.filter((chart) => !chart.hide).map((chart) => ( - - ))} -
+ selectedDateRange && + REPORT_ATTRIBUTES_V2.filter((chart) => !chart.hide).map((chart) => ( + + )) ) : ( Database health}> - {dateRange && + {selectedDateRange && REPORT_ATTRIBUTES.filter((attr) => !attr.hide).map((attr) => ( { 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} /> ))} )} - {dateRange && isReplicaSelected && ( + {selectedDateRange && isReplicaSelected && (
)} -
+
{ return ( @@ -55,56 +54,29 @@ const EdgeFunctionsUsage = () => { } }, [isOpen, functionIds]) - const defaultStart = dayjs().subtract(1, 'day').toISOString() - const defaultEnd = dayjs().toISOString() - const [dateRange, setDateRange] = useState({ - 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 ( <> -
-
- -
-
-
-
-
+ { tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }} onClick={onRefreshReport} /> -
- { - if (values.interval === '1d') { - setDateRange({ ...values, interval: '1h' }) - } else { - setDateRange(values) - } - }} +
+ @@ -227,38 +186,36 @@ const EdgeFunctionsUsage = () => {
- {dateRange && ( + {selectedDateRange && (

- {dayjs(dateRange.period_start.date).format('MMM D, h:mma')} + {dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')}

- {dayjs(dateRange.period_end.date).format('MMM D, h:mma')} + {dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')}

)}
-
-
- -
- {dateRange && - EDGEFN_CHARTS.filter((attr) => !attr.hide).map((attr, i) => ( - - ))} -
-
+ + } + > + {selectedDateRange && + EDGEFN_CHARTS.filter((attr) => !attr.hide).map((attr, i) => ( + + ))} + ) } diff --git a/apps/studio/pages/project/[ref]/reports/realtime.tsx b/apps/studio/pages/project/[ref]/reports/realtime.tsx index 00ea93b4ded..17de0d3a1b4 100644 --- a/apps/studio/pages/project/[ref]/reports/realtime.tsx +++ b/apps/studio/pages/project/[ref]/reports/realtime.tsx @@ -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({ - 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 ( <> -
-
-
+ { onClick={onRefreshReport} />
- { - if (values.interval === '1d') { - setDateRange({ ...values, interval: '1h' }) - } else { - setDateRange(values) - } - }} + - {dateRange && ( + {selectedDateRange && (

- {dayjs(dateRange.period_start.date).format('MMM D, h:mma')} + {dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')}

- {dayjs(dateRange.period_end.date).format('MMM D, h:mma')} + {dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')}

)}
-
-
- -
- {dateRange && - REALTIME_REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart) => ( - - ))} -
-
-
-
- -
-
-
- + } + > + {selectedDateRange && + REALTIME_REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart) => ( + - -
-
-
+ } + > + + + + ) } diff --git a/apps/studio/pages/project/[ref]/reports/storage.tsx b/apps/studio/pages/project/[ref]/reports/storage.tsx index 79610a1c570..2497f568645 100644 --- a/apps/studio/pages/project/[ref]/reports/storage.tsx +++ b/apps/studio/pages/project/[ref]/reports/storage.tsx @@ -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 ( -
-
-
+ { isLoading={isLoading} filters={filters} selectedProduct="storage" - datepickerHelpers={createFilteredDatePickerHelpers(plan?.id || 'free')} + datepickerHelpers={datePickerHelpers} + initialDatePickerValue={datePickerValue} className="w-full" showDatabaseSelector={false} /> -
- -
-
-
- + + } + > { append={TopCacheMissesRenderer} appendProps={{ data: data.topCacheMisses || [] }} /> -
+
) } diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 036160859d9..e986052e82c 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -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}`,