This commit is contained in:
Jordi Enric committed 2025-09-01 14:42:12 +02:00
1 parent 9917ff517f
commit fcbe1eae5f
7 files changed
+476 -199

No files matched your search

@@ -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<FilterGroup>(filters)
const [pendingFreeformText, setPendingFreeformText] = useState(freeformText)
const [hasChanges, setHasChanges] = useState(false)
const filterBarRef = useRef<HTMLDivElement>(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 (
<div className="flex items-center gap-2 w-full">
<div ref={filterBarRef} className="flex-1">
<FilterBar
filterProperties={filterProperties}
filters={pendingFilters}
onFilterChange={setPendingFilters}
freeformText={pendingFreeformText}
onFreeformTextChange={setPendingFreeformText}
aiApiUrl={aiApiUrl}
className={className}
/>
</div>
<div className="flex gap-2 shrink-0">
<Button
type="default"
size="tiny"
onClick={handleClear}
disabled={!hasActiveFilters}
title="Clear all filters"
>
Clear
</Button>
<Button
type="primary"
size="tiny"
onClick={handleApply}
disabled={!hasChanges}
title="Apply filters (⌘+Enter)"
>
Apply
</Button>
</div>
</div>
)
}
@@ -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 (
<Card id={report.id} className={cn('relative w-full overflow-hidden scroll-mt-16', className)}>
<CardContent className="flex flex-col gap-4 min-h-[280px] items-center justify-center">
{isLoadingChart ? (
<Loader2 className="size-5 animate-spin text-foreground-light" />
) : showEmptyState ? (
<p className="text-sm text-foreground-light text-center h-full flex items-center justify-center">
No data available for the selected time range
</p>
) : isErrorState ? (
<p className="text-sm text-foreground-light text-center h-full flex items-center justify-center">
Error loading chart data
@@ -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: ['=', '!=', '>', '<', '>=', '<='],
},
]
@@ -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, (interval: AnalyticsInterval, functionIds?: string[]) => 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,
+3 -6
View File
@@ -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?: {
@@ -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,
}
}
@@ -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<string[]>([])
const [tempFunctionIds, setTempFunctionIds] = useState<string[]>(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 (
<>
<ReportHeader title="Edge Functions" showDatabaseSelector={false} />
@@ -138,108 +139,34 @@ const EdgeFunctionsUsage = () => {
</div>
)}
</div>
<div>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button type="default" iconRight={<ChevronDown />}>
{functionIds.length === 0
? 'All Functions'
: `${functionIds.length} function${
functionIds.length > 1 ? 's' : ''
} selected`}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="start" className="w-72 p-0">
<div className="space-y-px max-h-60 overflow-y-auto p-1">
<Label
key="all-functions"
htmlFor="all-functions"
className="flex items-center hover:bg-overlay-hover overflow-hidden p-2 rounded-sm"
>
<Checkbox
id="all-functions"
checked={tempFunctionIds.length === 0}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
setTempFunctionIds([])
}
}}
/>
<div className="flex flex-col">
<span>All Functions</span>
</div>
</Label>
{functions
?.filter((fn: { slug: string }) => typeof fn.slug === 'string')
.map((fn) => (
<Label
key={fn.id}
htmlFor={fn.id}
className="flex items-center hover:bg-overlay-hover overflow-hidden p-2 rounded-sm"
>
<div>
<Checkbox
id={fn.id}
checked={tempFunctionIds.includes(fn.id)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
setTempFunctionIds([...tempFunctionIds, fn.id])
} else {
setTempFunctionIds(tempFunctionIds.filter((id) => id !== fn.id))
}
}}
/>
</div>
<div className="flex flex-col">
<span>{fn.slug}</span>
</div>
</Label>
))}
</div>
<div className="flex items-center justify-end gap-2 border-t border-default p-2">
<Button
size="tiny"
type="default"
onClick={() => setIsOpen(false)}
htmlType="button"
>
Cancel
</Button>
<Button
size="tiny"
type="primary"
htmlType="button"
onClick={() => {
setFunctionIds(tempFunctionIds)
setIsOpen(false)
}}
>
Apply
</Button>
</div>
</DropdownMenuContent>
</DropdownMenu>
<div className="w-full">
<FilterBarWithApply
filterProperties={filterProperties}
filters={filters}
onFilterChange={setFilters}
freeformText={freeformText}
onFreeformTextChange={setFreeformText}
onApply={handleFiltersApply}
/>
</div>
</div>
}
>
<div className="mt-8 flex flex-col gap-4">
{selectedDateRange &&
reportConfig
.filter((report) => !report.hide)
.map((report) => (
<ReportChartV2
key={`${report.id}`}
report={report}
projectRef={ref!}
interval={selectedDateRange.interval}
startDate={selectedDateRange?.period_start?.date}
endDate={selectedDateRange?.period_end?.date}
updateDateRange={updateDateRange}
functionIds={functionIds}
syncId={chartSyncId}
/>
))}
{reportConfig.map((report) => (
<ReportChartV2
key={`${report.id}-${refreshKey}`}
report={report}
projectRef={ref!}
interval={selectedDateRange.interval}
startDate={selectedDateRange?.period_start?.date}
endDate={selectedDateRange?.period_end?.date}
updateDateRange={updateDateRange}
filters={filters}
freeformText={freeformText}
syncId={chartSyncId}
/>
))}
</div>
</ReportStickyNav>
</>