Skip to content

Commit 1a36794

Browse files
authored
Add postgREST Report + Filters (supabase#37178)
* add postgrest report * fix type * fix layout border top * fix refreshes * fix up loading state, title, telemetry src * add shared api report filters, make headers look the same * fix auth * consolidate shared api hooks * fix name
1 parent d586493 commit 1a36794

File tree

9 files changed

+541
-248
lines changed

9 files changed

+541
-248
lines changed

apps/studio/components/interfaces/Reports/ReportFilterBar.tsx

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ import {
1616
DropdownMenuSeparator,
1717
DropdownMenuTrigger,
1818
Input,
19-
Popover,
2019
Select,
2120
cn,
2221
} from 'ui'
2322
import { DatePickerValue, LogsDatePicker } from '../Settings/Logs/Logs.DatePickers'
2423
import { REPORTS_DATEPICKER_HELPERS } from './Reports.constants'
2524
import type { ReportFilterItem } from './Reports.types'
25+
import { Popover, PopoverContent, PopoverTrigger } from '@ui/components/shadcn/ui/popover'
2626

2727
interface ReportFilterBarProps {
2828
filters: ReportFilterItem[]
@@ -262,9 +262,11 @@ const ReportFilterBar = ({
262262
.map((filter) => (
263263
<div
264264
key={`${filter.key}-${filter.compare}-${filter.value}`}
265-
className="text-xs rounded border border-foreground-lighter bg-surface-300 px-2 h-7 flex flex-row justify-center gap-1 items-center"
265+
className="text-xs rounded-md font-mono bg-surface-300 px-2 h-[26px] flex flex-row justify-center gap-1 items-center"
266266
>
267-
{filter.key} {filter.compare} {filter.value}
267+
<span className="">{filter.key}</span>
268+
<span className="text-foreground-lighter">{filter.compare}</span>
269+
<span className="">{filter.value}</span>
268270
<Button
269271
type="text"
270272
size="tiny"
@@ -276,36 +278,31 @@ const ReportFilterBar = ({
276278
</Button>
277279
</div>
278280
))}
279-
<Popover
280-
align="end"
281-
header={
282-
<div className="flex justify-between items-center py-1">
283-
<h5 className="text-sm text-foreground">Add Filter</h5>
284-
285-
<Button
286-
type="primary"
287-
size="tiny"
288-
onClick={() => {
289-
onAddFilter(addFilterValues)
290-
setShowAdder(false)
291-
resetFilterValues()
292-
}}
293-
>
294-
Save
295-
</Button>
296-
</div>
297-
}
298-
open={showAdder}
299-
onOpenChange={(openValue) => setShowAdder(openValue)}
300-
overlay={
301-
<div className="px-3 py-3 flex flex-col gap-2">
281+
<Popover open={showAdder} onOpenChange={(openValue) => setShowAdder(openValue)}>
282+
<PopoverTrigger>
283+
<Button
284+
asChild
285+
type="default"
286+
size="tiny"
287+
icon={<Plus className={`text-foreground-light `} />}
288+
>
289+
<span>Add filter</span>
290+
</Button>
291+
</PopoverTrigger>
292+
<PopoverContent
293+
align={filters.length > 0 ? 'end' : 'start'}
294+
portal={true}
295+
className="p-0 w-60"
296+
>
297+
<div className="flex flex-col gap-3 p-3">
302298
<Select
303299
size="tiny"
304300
value={addFilterValues.key}
305301
onChange={(e) => {
306302
setAddFilterValues((prev) => ({ ...prev, key: e.target.value }))
307303
}}
308304
label="Attribute Filter"
305+
className="gap-[2px]"
309306
>
310307
{filterKeys.map((key) => (
311308
<Select.Option key={key} value={key}>
@@ -323,6 +320,7 @@ const ReportFilterBar = ({
323320
}))
324321
}}
325322
label="Comparison"
323+
className="gap-[2px]"
326324
>
327325
{['matches', 'is'].map((value) => (
328326
<Select.Option key={value} value={value}>
@@ -333,6 +331,7 @@ const ReportFilterBar = ({
333331
<Input
334332
size="tiny"
335333
label="Value"
334+
className="gap-[2px]"
336335
placeholder={
337336
addFilterValues.compare === 'matches'
338337
? 'Provide a regex expression'
@@ -343,17 +342,21 @@ const ReportFilterBar = ({
343342
}}
344343
/>
345344
</div>
346-
}
347-
showClose
348-
>
349-
<Button
350-
asChild
351-
type="default"
352-
size="tiny"
353-
icon={<Plus className={`text-foreground-light `} />}
354-
>
355-
<span>Add filter</span>
356-
</Button>
345+
346+
<div className="flex items-center justify-end gap-2 border-t border-default p-2">
347+
<Button
348+
type="primary"
349+
size="tiny"
350+
onClick={() => {
351+
onAddFilter(addFilterValues)
352+
setShowAdder(false)
353+
resetFilterValues()
354+
}}
355+
>
356+
Add filter
357+
</Button>
358+
</div>
359+
</PopoverContent>
357360
</Popover>
358361
</div>
359362

@@ -369,4 +372,5 @@ const ReportFilterBar = ({
369372
</div>
370373
)
371374
}
375+
372376
export default ReportFilterBar

apps/studio/components/interfaces/Reports/SharedAPIReport.constants.ts renamed to apps/studio/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants.ts

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { get } from 'data/fetchers'
2-
import { generateRegexpWhere } from './Reports.constants'
3-
import { ReportFilterItem } from './Reports.types'
4-
import { useQueries } from '@tanstack/react-query'
2+
import { generateRegexpWhere } from '../Reports.constants'
3+
import { ReportFilterItem } from '../Reports.types'
4+
import { useQueries, useQueryClient } from '@tanstack/react-query'
55
import * as Sentry from '@sentry/nextjs'
6+
import { useState } from 'react'
7+
import { useParams } from 'common'
8+
import { isEqual } from 'lodash'
69

710
export const SHARED_API_REPORT_SQL = {
811
totalRequests: {
@@ -214,30 +217,58 @@ const fetchLogs = async ({
214217
return data
215218
}
216219

220+
const DEFAULT_KEYS = ['shared-api-report']
221+
217222
type SharedAPIReportParams = {
218-
src: string
219-
filters: ReportFilterItem[]
223+
filterBy: 'auth' | 'realtime' | 'storage' | 'graphql' | 'functions' | 'postgrest'
220224
start: string
221225
end: string
222226
projectRef: string
223227
enabled?: boolean
224228
}
225229
export const useSharedAPIReport = ({
226-
src = 'edge_logs',
227-
filters,
230+
filterBy,
228231
start,
229232
end,
230-
projectRef,
231233
enabled = true,
232-
}: SharedAPIReportParams) => {
234+
}: Omit<SharedAPIReportParams, 'projectRef'>) => {
235+
const { ref } = useParams() as { ref: string }
236+
const [filters, setFilters] = useState<ReportFilterItem[]>([])
237+
const queryClient = useQueryClient()
238+
const filterByMapSource = {
239+
functions: 'function_edge_logs',
240+
realtime: 'edge_logs',
241+
storage: 'edge_logs',
242+
graphql: 'edge_logs',
243+
postgrest: 'edge_logs',
244+
auth: 'edge_logs',
245+
}
246+
247+
const filterByMapValue = {
248+
functions: '/functions',
249+
realtime: '/realtime',
250+
storage: '/storage',
251+
graphql: '/graphql',
252+
postgrest: '/rest',
253+
auth: '/auth',
254+
}
255+
256+
const baseFilter = {
257+
key: 'request.path',
258+
value: filterByMapValue[filterBy],
259+
compare: 'matches' as const,
260+
}
261+
262+
const allFilters = [baseFilter, ...filters]
263+
233264
const queries = useQueries({
234265
queries: Object.entries(SHARED_API_REPORT_SQL).map(([key, value]) => ({
235-
queryKey: ['shared-api-report', key, src, filters, start, end, projectRef],
236-
enabled,
266+
queryKey: [...DEFAULT_KEYS, key, filterByMapSource[filterBy], filters, start, end, ref],
267+
enabled: enabled && !!ref && !!filterBy,
237268
queryFn: () =>
238269
fetchLogs({
239-
projectRef,
240-
sql: value.sql(filters, src),
270+
projectRef: ref,
271+
sql: value.sql(allFilters, filterByMapSource[filterBy]),
241272
start,
242273
end,
243274
}),
@@ -269,10 +300,39 @@ export const useSharedAPIReport = ({
269300
},
270301
{} as { [K in keyof typeof SHARED_API_REPORT_SQL]: boolean }
271302
)
303+
const addFilter = (filter: ReportFilterItem) => {
304+
if (isEqual(filter, baseFilter)) return
305+
if (filters.some((f) => isEqual(f, filter))) return
306+
setFilters((prev) =>
307+
[...prev, filter].sort((a, b) => {
308+
const keyA = a.key.toLowerCase()
309+
const keyB = b.key.toLowerCase()
310+
if (keyA < keyB) {
311+
return -1
312+
}
313+
if (keyA > keyB) {
314+
return 1
315+
}
316+
return 0
317+
})
318+
)
319+
}
320+
321+
const removeFilters = (toRemove: ReportFilterItem[]) => {
322+
setFilters((prev) => prev.filter((f) => !toRemove.find((r) => isEqual(f, r))))
323+
}
324+
325+
const isLoadingData = Object.values(isLoading).some(Boolean)
272326

273327
return {
274328
data,
275329
error,
276330
isLoading,
331+
isLoadingData,
332+
isRefetching: queryClient.isFetching({ queryKey: DEFAULT_KEYS }) > 0 || false,
333+
refetch: () => queryClient.invalidateQueries({ queryKey: DEFAULT_KEYS }),
334+
filters,
335+
addFilter,
336+
removeFilters,
277337
}
278338
}

apps/studio/components/interfaces/Reports/SharedAPIReport.tsx renamed to apps/studio/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.tsx

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,33 @@
1-
import ReportWidget from './ReportWidget'
1+
import ReportWidget from '../ReportWidget'
22
import {
33
ErrorCountsChartRenderer,
44
NetworkTrafficRenderer,
55
ResponseSpeedChartRenderer,
66
TopApiRoutesRenderer,
77
TotalRequestsChartRenderer,
8-
} from './renderers/ApiRenderers'
9-
import { SharedAPIReportKey, useSharedAPIReport } from './SharedAPIReport.constants'
10-
import { useParams } from 'common'
8+
} from '../renderers/ApiRenderers'
9+
import { SharedAPIReportKey } from './SharedAPIReport.constants'
1110

12-
export function SharedAPIReport({
13-
filterBy,
14-
start,
15-
end,
16-
hiddenReports = [],
17-
}: {
18-
filterBy: 'auth' | 'realtime' | 'storage' | 'graphql' | 'functions'
19-
start: string
20-
end: string
11+
type SharedAPIReportWidgetsProps = {
12+
data: any
13+
error: any
14+
isLoading: any
15+
isRefetching: boolean
2116
hiddenReports?: SharedAPIReportKey[]
22-
}) {
23-
const { ref } = useParams() as { ref: string }
24-
25-
const { data, error, isLoading } = useSharedAPIReport({
26-
src: filterBy === 'functions' ? 'function_edge_logs' : 'edge_logs',
27-
filters: [
28-
{
29-
key: 'request.path',
30-
value: `/${filterBy}`,
31-
compare: 'matches',
32-
},
33-
],
34-
start,
35-
end,
36-
projectRef: ref,
37-
enabled: !!ref && !!filterBy,
38-
})
17+
}
3918

19+
export function SharedAPIReport({
20+
data,
21+
error,
22+
isLoading,
23+
isRefetching,
24+
hiddenReports = [],
25+
}: SharedAPIReportWidgetsProps) {
4026
return (
4127
<div className="grid grid-cols-1 gap-4">
4228
{!hiddenReports.includes('totalRequests') && (
4329
<ReportWidget
44-
isLoading={isLoading.totalRequests}
30+
isLoading={isLoading.totalRequests || isRefetching}
4531
title="Total Requests"
4632
data={data.totalRequests || []}
4733
error={error.totalRequests}
@@ -52,7 +38,7 @@ export function SharedAPIReport({
5238
)}
5339
{!hiddenReports.includes('errorCounts') && (
5440
<ReportWidget
55-
isLoading={isLoading.errorCounts}
41+
isLoading={isLoading.errorCounts || isRefetching}
5642
title="Response Errors"
5743
tooltip="Error responses with 4XX or 5XX status codes"
5844
data={data.errorCounts || []}
@@ -66,7 +52,7 @@ export function SharedAPIReport({
6652
)}
6753
{!hiddenReports.includes('responseSpeed') && (
6854
<ReportWidget
69-
isLoading={isLoading.responseSpeed}
55+
isLoading={isLoading.responseSpeed || isRefetching}
7056
title="Response Speed"
7157
tooltip="Average response speed of a request (in ms)"
7258
data={data.responseSpeed || []}
@@ -78,7 +64,7 @@ export function SharedAPIReport({
7864
)}
7965
{!hiddenReports.includes('networkTraffic') && (
8066
<ReportWidget
81-
isLoading={isLoading.networkTraffic}
67+
isLoading={isLoading.networkTraffic || isRefetching}
8268
error={error.networkTraffic}
8369
title="Network Traffic"
8470
tooltip="Ingress and egress of requests and responses respectively"

0 commit comments

Comments
 (0)