edge-functions.tsx268 lines · main
| 1 | import { useQueryClient } from '@tanstack/react-query' |
| 2 | import { useParams } from 'common' |
| 3 | import dayjs from 'dayjs' |
| 4 | import { ArrowRight, RefreshCw } from 'lucide-react' |
| 5 | import { parseAsJson, useQueryState } from 'nuqs' |
| 6 | import { useMemo, useState } from 'react' |
| 7 | |
| 8 | import ReportHeader from '@/components/interfaces/Reports/ReportHeader' |
| 9 | import ReportPadding from '@/components/interfaces/Reports/ReportPadding' |
| 10 | import { |
| 11 | EDGE_FUNCTION_REGIONS, |
| 12 | REPORT_DATERANGE_HELPER_LABELS, |
| 13 | } from '@/components/interfaces/Reports/Reports.constants' |
| 14 | import ReportStickyNav from '@/components/interfaces/Reports/ReportStickyNav' |
| 15 | import { ReportChartV2 } from '@/components/interfaces/Reports/v2/ReportChartV2' |
| 16 | import { |
| 17 | numericFilterSchema, |
| 18 | ReportsNumericFilter, |
| 19 | } from '@/components/interfaces/Reports/v2/ReportsNumericFilter' |
| 20 | import { |
| 21 | ReportsSelectFilter, |
| 22 | selectFilterSchema, |
| 23 | } from '@/components/interfaces/Reports/v2/ReportsSelectFilter' |
| 24 | import { LogsDatePicker } from '@/components/interfaces/Settings/Logs/Logs.DatePickers' |
| 25 | import UpgradePrompt from '@/components/interfaces/Settings/Logs/UpgradePrompt' |
| 26 | import DefaultLayout from '@/components/layouts/DefaultLayout' |
| 27 | import ObservabilityLayout from '@/components/layouts/ObservabilityLayout/ObservabilityLayout' |
| 28 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 29 | import { ReportSettings } from '@/components/ui/Charts/ReportSettings' |
| 30 | import { useChartHoverState } from '@/components/ui/Charts/useChartHoverState' |
| 31 | import { ObservabilityLink } from '@/components/ui/ObservabilityLink' |
| 32 | import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query' |
| 33 | import { edgeFunctionReports } from '@/data/reports/v2/edge-functions.config' |
| 34 | import { useRefreshHandler, useReportDateRange } from '@/hooks/misc/useReportDateRange' |
| 35 | import { BASE_PATH } from '@/lib/constants' |
| 36 | import type { NextPageWithLayout } from '@/types' |
| 37 | |
| 38 | const EdgeFunctionsReportV2: NextPageWithLayout = () => { |
| 39 | return ( |
| 40 | <ReportPadding> |
| 41 | <EdgeFunctionsUsage /> |
| 42 | </ReportPadding> |
| 43 | ) |
| 44 | } |
| 45 | |
| 46 | EdgeFunctionsReportV2.getLayout = (page) => ( |
| 47 | <DefaultLayout> |
| 48 | <ObservabilityLayout title="Edge Functions">{page}</ObservabilityLayout> |
| 49 | </DefaultLayout> |
| 50 | ) |
| 51 | |
| 52 | export default EdgeFunctionsReportV2 |
| 53 | |
| 54 | const EdgeFunctionsUsage = () => { |
| 55 | const { ref } = useParams() |
| 56 | const { data: functions } = useEdgeFunctionsQuery({ |
| 57 | projectRef: ref, |
| 58 | }) |
| 59 | |
| 60 | const chartSyncId = `edge-functions-${ref}` |
| 61 | useChartHoverState(chartSyncId) |
| 62 | |
| 63 | // Filters |
| 64 | const [statusCodeFilter, setStatusCodeFilter] = useQueryState( |
| 65 | 'status_code', |
| 66 | parseAsJson(numericFilterSchema.parse) |
| 67 | ) |
| 68 | |
| 69 | const [regionFilter, setRegionFilter] = useQueryState( |
| 70 | 'region', |
| 71 | parseAsJson(selectFilterSchema.parse) |
| 72 | ) |
| 73 | const [executionTimeFilter, setExecutionTimeFilter] = useQueryState( |
| 74 | 'execution_time', |
| 75 | parseAsJson(numericFilterSchema.parse) |
| 76 | ) |
| 77 | |
| 78 | const [functionFilter, setFunctionFilter] = useQueryState( |
| 79 | 'functions', |
| 80 | parseAsJson(selectFilterSchema.parse) |
| 81 | ) |
| 82 | |
| 83 | const { |
| 84 | selectedDateRange, |
| 85 | updateDateRange, |
| 86 | datePickerValue, |
| 87 | datePickerHelpers, |
| 88 | showUpgradePrompt, |
| 89 | setShowUpgradePrompt, |
| 90 | handleDatePickerChange, |
| 91 | } = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES) |
| 92 | |
| 93 | const queryClient = useQueryClient() |
| 94 | const [isRefreshing, setIsRefreshing] = useState(false) |
| 95 | |
| 96 | const reportConfig = useMemo(() => { |
| 97 | return edgeFunctionReports({ |
| 98 | projectRef: ref!, |
| 99 | functions: functions ?? [], |
| 100 | startDate: selectedDateRange?.period_start?.date ?? '', |
| 101 | endDate: selectedDateRange?.period_end?.date ?? '', |
| 102 | interval: selectedDateRange?.interval ?? 'minute', |
| 103 | filters: { |
| 104 | functions: functionFilter ?? [], |
| 105 | status_code: statusCodeFilter, |
| 106 | region: regionFilter ?? [], |
| 107 | execution_time: executionTimeFilter, |
| 108 | }, |
| 109 | }) |
| 110 | }, [ |
| 111 | ref, |
| 112 | functions, |
| 113 | selectedDateRange, |
| 114 | functionFilter, |
| 115 | statusCodeFilter, |
| 116 | regionFilter, |
| 117 | executionTimeFilter, |
| 118 | ]) |
| 119 | |
| 120 | const onRefreshReport = useRefreshHandler( |
| 121 | datePickerValue, |
| 122 | datePickerHelpers, |
| 123 | handleDatePickerChange, |
| 124 | async () => { |
| 125 | if (!selectedDateRange) return |
| 126 | |
| 127 | setIsRefreshing(true) |
| 128 | queryClient.invalidateQueries({ queryKey: ['projects', ref, 'report-v2'] }) |
| 129 | setTimeout(() => setIsRefreshing(false), 1000) |
| 130 | } |
| 131 | ) |
| 132 | |
| 133 | return ( |
| 134 | <> |
| 135 | <ReportHeader title="Edge Functions" showDatabaseSelector={false} /> |
| 136 | <ReportStickyNav |
| 137 | content={ |
| 138 | <div className="flex flex-col gap-2"> |
| 139 | <div className="flex items-center gap-2"> |
| 140 | <ButtonTooltip |
| 141 | type="default" |
| 142 | disabled={isRefreshing} |
| 143 | icon={<RefreshCw className={isRefreshing ? 'animate-spin' : ''} />} |
| 144 | className="w-7" |
| 145 | tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }} |
| 146 | onClick={onRefreshReport} |
| 147 | /> |
| 148 | |
| 149 | <ReportSettings chartId="edge-functions-charts" /> |
| 150 | |
| 151 | <LogsDatePicker |
| 152 | align="start" |
| 153 | value={datePickerValue} |
| 154 | helpers={datePickerHelpers} |
| 155 | onSubmit={handleDatePickerChange} |
| 156 | /> |
| 157 | <UpgradePrompt |
| 158 | show={showUpgradePrompt} |
| 159 | setShowUpgradePrompt={setShowUpgradePrompt} |
| 160 | title="Report date range" |
| 161 | description="Report data can be stored for a maximum of 3 months depending on the plan that your project is on." |
| 162 | source="edgeFunctionsReportDateRange" |
| 163 | /> |
| 164 | |
| 165 | {selectedDateRange && ( |
| 166 | <div className="flex items-center gap-x-2 text-xs"> |
| 167 | <p className="text-foreground-light"> |
| 168 | {dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')} |
| 169 | </p> |
| 170 | <p className="text-foreground-light"> |
| 171 | <ArrowRight size={12} /> |
| 172 | </p> |
| 173 | <p className="text-foreground-light"> |
| 174 | {dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')} |
| 175 | </p> |
| 176 | </div> |
| 177 | )} |
| 178 | </div> |
| 179 | <div className="w-full flex items-center gap-2 flex-wrap"> |
| 180 | <ReportsSelectFilter |
| 181 | label="Function" |
| 182 | options={ |
| 183 | functions?.map((fn: { name: string; id: string }) => ({ |
| 184 | label: fn.name, |
| 185 | value: fn.id, |
| 186 | })) ?? [] |
| 187 | } |
| 188 | value={functionFilter ?? []} |
| 189 | onChange={setFunctionFilter} |
| 190 | isLoading={isRefreshing} |
| 191 | showSearch |
| 192 | /> |
| 193 | |
| 194 | <ReportsNumericFilter |
| 195 | label="Status Code" |
| 196 | value={statusCodeFilter} |
| 197 | onChange={setStatusCodeFilter} |
| 198 | defaultOperator="=" |
| 199 | isLoading={isRefreshing} |
| 200 | /> |
| 201 | |
| 202 | <ReportsNumericFilter |
| 203 | label="Execution Time" |
| 204 | value={executionTimeFilter} |
| 205 | onChange={setExecutionTimeFilter} |
| 206 | placeholder="Enter time in ms" |
| 207 | min={0} |
| 208 | max={99999} |
| 209 | defaultOperator=">=" |
| 210 | isLoading={isRefreshing} |
| 211 | /> |
| 212 | |
| 213 | <ReportsSelectFilter |
| 214 | label="Region" |
| 215 | options={EDGE_FUNCTION_REGIONS.map((region) => ({ |
| 216 | value: region.key, |
| 217 | label: ( |
| 218 | <div className="flex items-center gap-x-2"> |
| 219 | <img |
| 220 | src={`${BASE_PATH}/img/regions/${region.key}.svg`} |
| 221 | alt={region.key} |
| 222 | className="w-4 h-4" |
| 223 | /> |
| 224 | <div className="flex flex-wrap gap-x-2 items-center"> |
| 225 | <span className="text-foreground text-xs">{region.label}</span> |
| 226 | <span className="text-foreground-lighter text-xs">{region.key}</span> |
| 227 | </div> |
| 228 | </div> |
| 229 | ), |
| 230 | }))} |
| 231 | value={regionFilter ?? []} |
| 232 | onChange={setRegionFilter} |
| 233 | showSearch |
| 234 | /> |
| 235 | </div> |
| 236 | </div> |
| 237 | } |
| 238 | > |
| 239 | <div className="mt-8 flex flex-col gap-4 pb-8"> |
| 240 | {selectedDateRange && |
| 241 | reportConfig |
| 242 | .filter((report) => !report.hide) |
| 243 | .map((report) => ( |
| 244 | <ReportChartV2 |
| 245 | key={`${report.id}`} |
| 246 | report={report} |
| 247 | projectRef={ref!} |
| 248 | interval={selectedDateRange.interval} |
| 249 | startDate={selectedDateRange?.period_start?.date} |
| 250 | endDate={selectedDateRange?.period_end?.date} |
| 251 | updateDateRange={updateDateRange} |
| 252 | syncId={chartSyncId} |
| 253 | filters={{ |
| 254 | functions: functionFilter, |
| 255 | status_code: statusCodeFilter, |
| 256 | region: regionFilter, |
| 257 | execution_time: executionTimeFilter, |
| 258 | }} |
| 259 | /> |
| 260 | ))} |
| 261 | </div> |
| 262 | </ReportStickyNav> |
| 263 | <div className="pb-8"> |
| 264 | <ObservabilityLink /> |
| 265 | </div> |
| 266 | </> |
| 267 | ) |
| 268 | } |