diff --git a/apps/studio/components/interfaces/Reports/FilterBarWithApply.tsx b/apps/studio/components/interfaces/Reports/FilterBarWithApply.tsx new file mode 100644 index 00000000000..9cf65e9f7eb --- /dev/null +++ b/apps/studio/components/interfaces/Reports/FilterBarWithApply.tsx @@ -0,0 +1,146 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { Button } from 'ui' +import { FilterBar, type FilterGroup, type FilterProperty } from 'ui-patterns' + +interface FilterBarWithApplyProps { + filterProperties: FilterProperty[] + filters: FilterGroup + onFilterChange: (filters: FilterGroup) => void + freeformText: string + onFreeformTextChange: (freeformText: string) => void + onApply?: () => void + aiApiUrl?: string + className?: string +} + +export function FilterBarWithApply({ + filterProperties, + filters, + onFilterChange, + freeformText, + onFreeformTextChange, + onApply, + aiApiUrl, + className, +}: FilterBarWithApplyProps) { + const [pendingFilters, setPendingFilters] = useState(filters) + const [pendingFreeformText, setPendingFreeformText] = useState(freeformText) + const [hasChanges, setHasChanges] = useState(false) + const filterBarRef = useRef(null) + + const hasActiveFilters = filters.conditions.length > 0 || freeformText.trim() !== '' + + useEffect(() => { + const filtersChanged = JSON.stringify(pendingFilters) !== JSON.stringify(filters) + const textChanged = pendingFreeformText !== freeformText + setHasChanges(filtersChanged || textChanged) + }, [pendingFilters, pendingFreeformText, filters, freeformText]) + + // Sync pending state when external filters change + useEffect(() => { + setPendingFilters(filters) + }, [filters]) + + useEffect(() => { + setPendingFreeformText(freeformText) + }, [freeformText]) + + const handleApply = useCallback(async () => { + onFilterChange(pendingFilters) + onFreeformTextChange(pendingFreeformText) + setHasChanges(false) + + // Use setTimeout to ensure state updates are flushed before calling onApply + setTimeout(() => { + onApply?.() + }, 0) + }, [pendingFilters, pendingFreeformText, onFilterChange, onFreeformTextChange, onApply]) + + const handleClear = useCallback(async () => { + const emptyFilters: FilterGroup = { logicalOperator: 'AND', conditions: [] } + setPendingFilters(emptyFilters) + setPendingFreeformText('') + onFilterChange(emptyFilters) + onFreeformTextChange('') + setHasChanges(false) + + // Use setTimeout to ensure state updates are flushed before calling onApply + setTimeout(() => { + onApply?.() + }, 0) + }, [onFilterChange, onFreeformTextChange, onApply]) + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { + e.preventDefault() + if (hasChanges) { + handleApply() + } + } + }, + [hasChanges, handleApply] + ) + + // Add global keyboard listener when FilterBar is focused + useEffect(() => { + const filterBarElement = filterBarRef.current + if (!filterBarElement) return + + const handleFocus = () => { + document.addEventListener('keydown', handleKeyDown) + } + + const handleBlur = (e: FocusEvent) => { + // Only remove listener if focus is moving outside the FilterBar + if (!filterBarElement.contains(e.relatedTarget as Node)) { + document.removeEventListener('keydown', handleKeyDown) + } + } + + filterBarElement.addEventListener('focusin', handleFocus) + filterBarElement.addEventListener('focusout', handleBlur) + + return () => { + filterBarElement.removeEventListener('focusin', handleFocus) + filterBarElement.removeEventListener('focusout', handleBlur) + document.removeEventListener('keydown', handleKeyDown) + } + }, [handleKeyDown]) + + return ( +
+
+ +
+
+ + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx index 36538a71681..69ed1d80918 100644 --- a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx +++ b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx @@ -10,6 +10,7 @@ import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Card, CardContent, cn } from 'ui' import { ReportChartUpsell } from './ReportChartUpsell' +import { FilterGroup } from 'ui-patterns' export interface ReportChartV2Props { report: ReportConfig @@ -18,8 +19,9 @@ export interface ReportChartV2Props { endDate: string interval: AnalyticsInterval updateDateRange: (from: string, to: string) => void - functionIds?: string[] + filters?: FilterGroup edgeFnIdToName?: (id: string) => string | undefined + freeformText?: string className?: string syncId?: string } @@ -31,8 +33,9 @@ export const ReportChartV2 = ({ endDate, interval, updateDateRange, - functionIds, + filters, edgeFnIdToName, + freeformText, className, syncId, }: ReportChartV2Props) => { @@ -50,19 +53,26 @@ export const ReportChartV2 = ({ isLoading: isLoadingChart, error, } = useQuery( - ['projects', projectRef, 'report-v2', report.id, { startDate, endDate, interval, functionIds }], + [ + 'projects', + projectRef, + 'report-v2', + report.id, + { startDate, endDate, interval, filters, freeformText }, + ], async () => { return await report.dataProvider( projectRef, startDate, endDate, interval, - functionIds, - edgeFnIdToName + filters, + edgeFnIdToName, + freeformText ) }, { - enabled: Boolean(projectRef && canFetch && isAvailable), + enabled: Boolean(projectRef && canFetch && isAvailable && !report.hide), refetchOnWindowFocus: false, } ) @@ -91,17 +101,16 @@ export const ReportChartV2 = ({ } const isErrorState = error && !isLoadingChart - const showEmptyState = (!finalChartData || finalChartData.length === 0) && !isLoadingChart + + if (report.hide) { + return null + } return ( {isLoadingChart ? ( - ) : showEmptyState ? ( -

- No data available for the selected time range -

) : isErrorState ? (

Error loading chart data diff --git a/apps/studio/data/reports/v2/edge-functions-filters.ts b/apps/studio/data/reports/v2/edge-functions-filters.ts new file mode 100644 index 00000000000..ef0c60f2e1c --- /dev/null +++ b/apps/studio/data/reports/v2/edge-functions-filters.ts @@ -0,0 +1,52 @@ +import type { FilterProperty } from 'ui-patterns/FilterBar' + +export const edgeFunctionFilterProperties = ( + functions: { id: string; name: string; slug: string }[] +): FilterProperty[] => [ + { + label: 'Function', + name: 'function_id', + type: 'string' as const, + options: functions.map((fn) => ({ + label: fn.slug, + value: fn.id, + })), + operators: ['=', '!='], + }, + { + label: 'Status Code', + name: 'status_code', + type: 'number' as const, + options: [ + { label: '200 - Success', value: 200 }, + { label: '400 - Bad Request', value: 400 }, + { label: '401 - Unauthorized', value: 401 }, + { label: '403 - Forbidden', value: 403 }, + { label: '404 - Not Found', value: 404 }, + { label: '500 - Internal Server Error', value: 500 }, + { label: '502 - Bad Gateway', value: 502 }, + { label: '503 - Service Unavailable', value: 503 }, + { label: '504 - Gateway Timeout', value: 504 }, + ], + operators: ['=', '!=', '>', '<', '>=', '<='], + }, + { + label: 'Region', + name: 'region', + type: 'string' as const, + options: [ + { label: 'us-east-1', value: 'us-east-1' }, + { label: 'us-west-1', value: 'us-west-1' }, + { label: 'eu-west-1', value: 'eu-west-1' }, + { label: 'ap-southeast-1', value: 'ap-southeast-1' }, + { label: 'ap-northeast-1', value: 'ap-northeast-1' }, + ], + operators: ['=', '!='], + }, + { + label: 'Execution Time', + name: 'execution_time_ms', + type: 'number' as const, + operators: ['=', '!=', '>', '<', '>=', '<='], + }, +] diff --git a/apps/studio/data/reports/v2/edge-functions.config.ts b/apps/studio/data/reports/v2/edge-functions.config.ts index 2e65c925232..4b3ed6eecb5 100644 --- a/apps/studio/data/reports/v2/edge-functions.config.ts +++ b/apps/studio/data/reports/v2/edge-functions.config.ts @@ -11,12 +11,88 @@ import { REPORT_STATUS_CODE_COLORS, } from 'data/reports/report.utils' import { getHttpStatusCodeInfo } from 'lib/http-status-codes' +import type { FilterGroup, FilterCondition } from 'ui-patterns/FilterBar' import { ReportConfig } from './reports.types' -const METRIC_SQL: Record string> = - { - TotalInvocations: (interval, functionIds) => { - return ` +function buildFilterWhereClause(filters?: FilterGroup, freeformText?: string): string { + if (!filters && !freeformText) return '' + + const conditions: string[] = [] + + if (freeformText && freeformText.trim()) { + conditions.push(`( + function_id ILIKE '%${freeformText.trim()}%' OR + CAST(res.status_code AS TEXT) ILIKE '%${freeformText.trim()}%' OR + h.x_sb_edge_region ILIKE '%${freeformText.trim()}%' + )`) + } + + if (filters && filters.conditions.length > 0) { + const filterConditions = buildFilterConditions(filters) + if (filterConditions) { + conditions.push(filterConditions) + } + } + + if (conditions.length === 0) return '' + + return conditions.length === 1 ? `WHERE ${conditions[0]}` : `WHERE ${conditions.join(' AND ')}` +} + +function buildFilterConditions(group: FilterGroup): string { + if (group.conditions.length === 0) return '' + + const conditionStrings = group.conditions + .map((condition) => { + if ('logicalOperator' in condition) { + const subConditions = buildFilterConditions(condition as FilterGroup) + return subConditions ? `(${subConditions})` : '' + } else { + return buildSingleCondition(condition as FilterCondition) + } + }) + .filter(Boolean) + + if (conditionStrings.length === 0) return '' + + return conditionStrings.join(` ${group.logicalOperator} `) +} + +function buildSingleCondition(condition: FilterCondition): string { + const { propertyName, operator, value } = condition + + if (value === null || value === undefined) return '' + + const escapedValue = typeof value === 'string' ? `'${value.replace(/'/g, "''")}'` : value + + let sqlCondition = '' + switch (propertyName) { + case 'function_id': + sqlCondition = `function_id ${operator} ${escapedValue}` + break + case 'status_code': + sqlCondition = `res.status_code ${operator} ${escapedValue}` + break + case 'region': + sqlCondition = `h.x_sb_edge_region ${operator} ${escapedValue}` + break + case 'execution_time_ms': + sqlCondition = `m.execution_time_ms ${operator} ${escapedValue}` + break + default: + sqlCondition = '' + } + + return sqlCondition +} + +const METRIC_SQL: Record< + string, + (interval: AnalyticsInterval, filters?: FilterGroup, freeformText?: string) => string +> = { + TotalInvocations: (interval, filters, freeformText) => { + const whereClause = buildFilterWhereClause(filters, freeformText) + return ` --edgefn-report-invocations select timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp, @@ -25,46 +101,52 @@ select from function_edge_logs CROSS JOIN UNNEST(metadata) AS m - CROSS JOIN UNNEST(m.request) AS request - CROSS JOIN UNNEST(m.response) AS response - ${ - functionIds && functionIds.length > 0 - ? `WHERE function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})` - : '' - } + CROSS JOIN UNNEST(m.request) AS req + CROSS JOIN UNNEST(m.response) AS res + CROSS JOIN UNNEST(res.headers) AS h + ${whereClause} group by timestamp, function_id order by timestamp desc; ` - }, - ExecutionStatusCodes: (interval, functionIds) => { - return ` + }, + ExecutionStatusCodes: (interval, filters, freeformText) => { + const whereClause = buildFilterWhereClause(filters, freeformText) + return ` --edgefn-report-execution-status-codes select timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp, - response.status_code as status_code, - count(response.status_code) as count + res.status_code as status_code, + count(res.status_code) as count from function_edge_logs cross join unnest(metadata) as m - cross join unnest(m.response) as response - ${ - functionIds && functionIds.length > 0 - ? `where function_id in (${functionIds.map((id) => `'${id}'`).join(',')})` - : '' - } + cross join unnest(m.response) as res + cross join unnest(res.headers) as h + ${whereClause} group by timestamp, status_code order by timestamp desc ` - }, - InvocationsByRegion: (interval, functionIds) => { - const granularity = analyticsIntervalToGranularity(interval) - return ` + }, + InvocationsByRegion: (interval, filters, freeformText) => { + const granularity = analyticsIntervalToGranularity(interval) + const whereClause = buildFilterWhereClause(filters, freeformText) + + let finalWhereClause = '' + if (whereClause) { + // Wrap the filter conditions in parentheses and combine with region condition + const filterConditions = whereClause.replace('WHERE ', '') + finalWhereClause = `WHERE (${filterConditions}) AND h.x_sb_edge_region is not null` + } else { + finalWhereClause = 'WHERE h.x_sb_edge_region is not null' + } + + return ` --edgefn-report-invocations-by-region select timestamp_trunc(timestamp, ${granularity}) as timestamp, @@ -73,43 +155,44 @@ select from function_edge_logs cross join unnest(metadata) as m - cross join unnest(m.response) as r - cross join unnest(r.headers) as h - where h.x_sb_edge_region is not null - ${ - functionIds && functionIds.length > 0 - ? `and function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})` - : '' - } + cross join unnest(m.response) as res + cross join unnest(res.headers) as h + ${finalWhereClause} group by timestamp, region order by timestamp desc ` - }, - ExecutionTime: (interval, functionIds) => { - const granularity = analyticsIntervalToGranularity(interval) - const hasFunctions = functionIds && functionIds.length > 0 - return ` + }, + ExecutionTime: (interval, filters, freeformText) => { + const granularity = analyticsIntervalToGranularity(interval) + const whereClause = buildFilterWhereClause(filters, freeformText) + const hasFilters = filters?.conditions.some( + (condition) => 'propertyName' in condition && condition.propertyName === 'function_id' + ) + + return ` --edgefn-report-execution-time select timestamp_trunc(timestamp, ${granularity}) as timestamp, - ${hasFunctions ? 'function_id,' : ''} + ${hasFilters ? 'function_id,' : ''} avg(m.execution_time_ms) as avg_execution_time from function_edge_logs cross join unnest(metadata) as m - cross join unnest(m.request) as request - ${hasFunctions ? `where function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})` : ''} + cross join unnest(m.request) as req + cross join unnest(m.response) as res + cross join unnest(res.headers) as h + ${whereClause} group by timestamp - ${hasFunctions ? ', function_id' : ''} + ${hasFilters ? ', function_id' : ''} order by timestamp desc ` - }, - } + }, +} async function runQuery(projectRef: string, sql: string, startDate: string, endDate: string) { const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, { @@ -216,9 +299,7 @@ export const edgeFunctionReports = ({ startDate: string endDate: string interval: AnalyticsInterval - filters: { - functionIds?: string[] - } + filters?: FilterGroup }): ReportConfig[] => [ { id: 'total-invocations', @@ -232,8 +313,16 @@ export const edgeFunctionReports = ({ defaultChartStyle: 'line', titleTooltip: 'The total number of edge function invocations over time.', availableIn: ['free', 'pro', 'team', 'enterprise'], - dataProvider: async () => { - const sql = METRIC_SQL.TotalInvocations(interval, filters.functionIds) + dataProvider: async ( + projectRef, + startDate, + endDate, + interval, + filters, + edgeFnIdToName, + freeformText + ) => { + const sql = METRIC_SQL.TotalInvocations(interval, filters, freeformText) const response = await runQuery(projectRef, sql, startDate, endDate) if (!response?.result) return { data: [] } @@ -265,13 +354,15 @@ export const edgeFunctionReports = ({ titleTooltip: 'The total number of edge function executions by status code.', availableIn: ['free', 'pro', 'team', 'enterprise'], dataProvider: async ( - projectRef: string, - startDate: string, - endDate: string, - interval: AnalyticsInterval, - functionIds?: string[] + projectRef, + startDate, + endDate, + interval, + filters, + edgeFnIdToName, + freeformText ) => { - const sql = METRIC_SQL.ExecutionStatusCodes(interval, functionIds) + const sql = METRIC_SQL.ExecutionStatusCodes(interval, filters, freeformText) const rawData = await runQuery(projectRef, sql, startDate, endDate) if (!rawData?.result) return { data: [] } @@ -307,8 +398,16 @@ export const edgeFunctionReports = ({ tickFormatter: (value: number) => `${value}ms`, }, format: (value: unknown) => `${Number(value).toFixed(0)}ms`, - dataProvider: async () => { - const sql = METRIC_SQL.ExecutionTime(interval, filters.functionIds) + dataProvider: async ( + projectRef, + startDate, + endDate, + interval, + filters, + edgeFnIdToName, + freeformText + ) => { + const sql = METRIC_SQL.ExecutionTime(interval, filters, freeformText) const rawData = await runQuery(projectRef, sql, startDate, endDate) if (!rawData?.result) return { data: [] } @@ -364,8 +463,16 @@ export const edgeFunctionReports = ({ defaultChartStyle: 'line', titleTooltip: 'The total number of edge function invocations by region.', availableIn: ['pro', 'team', 'enterprise'], - dataProvider: async () => { - const sql = METRIC_SQL.InvocationsByRegion(interval, filters.functionIds) + dataProvider: async ( + projectRef, + startDate, + endDate, + interval, + filters, + edgeFnIdToName, + freeformText + ) => { + const sql = METRIC_SQL.InvocationsByRegion(interval, filters, freeformText) const rawData = await runQuery(projectRef, sql, startDate, endDate) const data = rawData.result?.map((point: any) => ({ ...point, diff --git a/apps/studio/data/reports/v2/reports.types.ts b/apps/studio/data/reports/v2/reports.types.ts index 5edd0b23791..7bde6457dca 100644 --- a/apps/studio/data/reports/v2/reports.types.ts +++ b/apps/studio/data/reports/v2/reports.types.ts @@ -1,9 +1,6 @@ import { AnalyticsInterval } from 'data/analytics/constants' import { YAxisProps } from 'recharts' - -type ReportDataProviderFilter = { - functionIds?: string[] -} +import { FilterGroup } from 'ui-patterns' export interface ReportDataProvider { ( @@ -11,9 +8,9 @@ export interface ReportDataProvider { startDate: string, endDate: string, interval: AnalyticsInterval, - functionIds?: string[], + filters?: FilterGroup, edgeFnIdToName?: (id: string) => string | undefined, - filters?: ReportDataProviderFilter[] + freeformText?: string ): Promise<{ data: any attributes?: { diff --git a/apps/studio/hooks/misc/useEdgeFunctionFilters.ts b/apps/studio/hooks/misc/useEdgeFunctionFilters.ts new file mode 100644 index 00000000000..d4c3a11b4c2 --- /dev/null +++ b/apps/studio/hooks/misc/useEdgeFunctionFilters.ts @@ -0,0 +1,39 @@ +import { useMemo } from 'react' +import { parseAsString, useQueryState } from 'nuqs' +import type { FilterGroup } from 'ui-patterns/FilterBar' + +const defaultFilters: FilterGroup = { + logicalOperator: 'AND', + conditions: [], +} + +export function useEdgeFunctionFilters() { + const [filtersParam, setFiltersParam] = useQueryState('filters', parseAsString.withDefault('')) + + const [freeformText, setFreeformText] = useQueryState('search', parseAsString.withDefault('')) + + const filters = useMemo(() => { + if (!filtersParam) return defaultFilters + + try { + return JSON.parse(filtersParam) as FilterGroup + } catch { + return defaultFilters + } + }, [filtersParam]) + + const setFilters = (newFilters: FilterGroup) => { + if (newFilters.conditions.length === 0) { + setFiltersParam('') + } else { + setFiltersParam(JSON.stringify(newFilters)) + } + } + + return { + filters, + setFilters, + freeformText, + setFreeformText, + } +} diff --git a/apps/studio/pages/project/[ref]/reports/edge-functions.tsx b/apps/studio/pages/project/[ref]/reports/edge-functions.tsx index 16b4705e043..67958e7c43e 100644 --- a/apps/studio/pages/project/[ref]/reports/edge-functions.tsx +++ b/apps/studio/pages/project/[ref]/reports/edge-functions.tsx @@ -1,10 +1,9 @@ import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import dayjs from 'dayjs' -import { ArrowRight, ChevronDown, RefreshCw } from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' +import { ArrowRight, RefreshCw } from 'lucide-react' +import { useMemo, useState } from 'react' -import { Label } from '@ui/components/shadcn/ui/label' import { ReportChartV2 } from 'components/interfaces/Reports/v2/ReportChartV2' import ReportHeader from 'components/interfaces/Reports/ReportHeader' import ReportPadding from 'components/interfaces/Reports/ReportPadding' @@ -13,18 +12,22 @@ import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePic import DefaultLayout from 'components/layouts/DefaultLayout' import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui' import { useChartHoverState } from 'components/ui/Charts/useChartHoverState' +import { FilterBarWithApply } from 'components/interfaces/Reports/FilterBarWithApply' import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' import { edgeFunctionReports } from 'data/reports/v2/edge-functions.config' +import { edgeFunctionFilterProperties } from 'data/reports/v2/edge-functions-filters' import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants' import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt' import { useReportDateRange } from 'hooks/misc/useReportDateRange' +import { useEdgeFunctionFilters } from 'hooks/misc/useEdgeFunctionFilters' import type { NextPageWithLayout } from 'types' import { ReportSettings } from 'components/ui/Charts/ReportSettings' +import { toast } from 'sonner' +import { ReportConfig } from 'data/reports/v2/reports.types' const EdgeFunctionsReportV2: NextPageWithLayout = () => { return ( @@ -51,15 +54,7 @@ const EdgeFunctionsUsage = () => { const chartSyncId = `edge-functions-${ref}` useChartHoverState(chartSyncId) - const [isOpen, setIsOpen] = useState(false) - const [functionIds, setFunctionIds] = useState([]) - const [tempFunctionIds, setTempFunctionIds] = useState(functionIds) - - useEffect(() => { - if (isOpen) { - setTempFunctionIds(functionIds) - } - }, [isOpen, functionIds]) + const { filters, setFilters, freeformText, setFreeformText } = useEdgeFunctionFilters() const { selectedDateRange, @@ -73,19 +68,20 @@ const EdgeFunctionsUsage = () => { const queryClient = useQueryClient() const [isRefreshing, setIsRefreshing] = useState(false) + const [refreshKey, setRefreshKey] = useState(0) - const reportConfig = useMemo(() => { - return edgeFunctionReports({ - projectRef: ref!, - functions: functions ?? [], - startDate: selectedDateRange?.period_start?.date ?? '', - endDate: selectedDateRange?.period_end?.date ?? '', - interval: selectedDateRange?.interval ?? 'minute', - filters: { - functionIds, - }, - }) - }, [ref, functions, selectedDateRange, functionIds]) + const filterProperties = useMemo(() => { + return edgeFunctionFilterProperties(functions ?? []) + }, [functions]) + + const reportConfig = edgeFunctionReports({ + projectRef: ref!, + functions: functions ?? [], + startDate: selectedDateRange?.period_start?.date ?? '', + endDate: selectedDateRange?.period_end?.date ?? '', + interval: selectedDateRange?.interval ?? 'minute', + filters, + }) const onRefreshReport = async () => { if (!selectedDateRange) return @@ -95,6 +91,11 @@ const EdgeFunctionsUsage = () => { setTimeout(() => setIsRefreshing(false), 1000) } + const handleFiltersApply = async () => { + setRefreshKey((prev) => prev + 1) + await onRefreshReport() + } + return ( <> @@ -138,108 +139,34 @@ const EdgeFunctionsUsage = () => { )} -

- - - - - -
- - {functions - ?.filter((fn: { slug: string }) => typeof fn.slug === 'string') - .map((fn) => ( - - ))} -
-
- - -
-
-
+
+
} >
- {selectedDateRange && - reportConfig - .filter((report) => !report.hide) - .map((report) => ( - - ))} + {reportConfig.map((report) => ( + + ))}