mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 02:45:07 +03:00
feat: move filters to be in a line, fix filtering matching bug
This commit is contained in:
1 parent
df160e8597
commit
97fee229d7
4 files changed
+143
-102
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { keyBy } from 'lodash'
|
||||
import { IconBox, IconCode, IconDatabase, IconKey, IconZap, IconZapOff } from '@supabase/ui'
|
||||
import React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Dropdown, Popover, Button, IconPlus, IconChevronDown, Select, Input, IconX } from 'ui'
|
||||
@@ -10,11 +10,76 @@ interface Props {
|
||||
filters: ReportFilterItem[]
|
||||
onAddFilter: (filter: ReportFilterItem) => void
|
||||
onRemoveFilter: (filter: ReportFilterItem) => void
|
||||
onDatepickerChange: React.ComponentProps<typeof DatePickers>['onChange']
|
||||
datepickerTo?: string
|
||||
datepickerFrom?: string
|
||||
}
|
||||
|
||||
const ReportFilterBar: React.FC<Props> = ({ filters, onAddFilter, onRemoveFilter }) => {
|
||||
const filterKeys = ['request.path', 'request.host' , 'response.status_code']
|
||||
const PRODUCT_FILTERS = [
|
||||
{
|
||||
key: 'rest',
|
||||
filterKey: 'request.path',
|
||||
filterValue: '/rest',
|
||||
label: 'REST',
|
||||
description: 'Requests made to PostgREST',
|
||||
icon: IconDatabase,
|
||||
},
|
||||
{
|
||||
key: 'auth',
|
||||
filterKey: 'request.path',
|
||||
filterValue: '/auth',
|
||||
label: 'Auth',
|
||||
description: 'Authentication and authorization requests',
|
||||
icon: IconKey,
|
||||
},
|
||||
{
|
||||
key: 'storage',
|
||||
filterKey: 'request.path',
|
||||
filterValue: '/storage',
|
||||
label: 'Storage',
|
||||
description: 'Storage asset requests',
|
||||
icon: IconBox,
|
||||
},
|
||||
{
|
||||
key: 'realtime',
|
||||
filterKey: 'request.path',
|
||||
filterValue: '/realtime',
|
||||
label: 'Realtime',
|
||||
description: 'Realtime connection requests',
|
||||
icon: IconZapOff,
|
||||
},
|
||||
// TODO: support functions once union parsing is fixed
|
||||
// {
|
||||
// key: 'functions',
|
||||
// filterKey: 'request.host',
|
||||
// filterValue: '.functions.',
|
||||
// label: 'Edge Functions',
|
||||
// description: 'Edge function calls',
|
||||
// icon: IconCode,
|
||||
// },
|
||||
{
|
||||
key: 'graphql',
|
||||
filterKey: 'request.path',
|
||||
filterValue: '/graphql',
|
||||
label: 'GraphQL',
|
||||
description: 'Requests made to pg_graphql',
|
||||
icon: IconCode,
|
||||
},
|
||||
]
|
||||
|
||||
const ReportFilterBar: React.FC<Props> = ({
|
||||
filters,
|
||||
onAddFilter,
|
||||
onRemoveFilter,
|
||||
onDatepickerChange,
|
||||
datepickerTo = '',
|
||||
datepickerFrom = '',
|
||||
}) => {
|
||||
const filterKeys = ['request.path', 'request.host', 'response.status_code']
|
||||
const [showAdder, setShowAdder] = useState(false)
|
||||
const [currentProductFilter, setCurrentProductFilter] = useState<
|
||||
null | typeof PRODUCT_FILTERS[number]
|
||||
>(null)
|
||||
const [addFilterValues, setAddFilterValues] = useState<ReportFilterItem>({
|
||||
key: filterKeys[0],
|
||||
compare: 'is',
|
||||
@@ -30,67 +95,68 @@ const ReportFilterBar: React.FC<Props> = ({ filters, onAddFilter, onRemoveFilter
|
||||
}
|
||||
|
||||
const handleProductFilterChange = async (
|
||||
filterToRemove: ReportFilterItem | null,
|
||||
key: string
|
||||
productFilter: null | typeof PRODUCT_FILTERS[number]
|
||||
) => {
|
||||
if (filterToRemove) {
|
||||
await onRemoveFilter(filterToRemove)
|
||||
}
|
||||
if (key !== 'all') {
|
||||
await onAddFilter({
|
||||
key: 'request.path',
|
||||
setCurrentProductFilter(productFilter)
|
||||
if (productFilter) {
|
||||
await onRemoveFilter({
|
||||
key: productFilter.filterKey,
|
||||
compare: 'matches',
|
||||
value: `/${key}/`,
|
||||
value: productFilter.filterValue,
|
||||
})
|
||||
}
|
||||
if (productFilter !== null) {
|
||||
await onAddFilter({
|
||||
key: productFilter.filterKey,
|
||||
compare: 'matches',
|
||||
value: productFilter.filterValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
const getPathFilterMatchValue = (filter: ReportFilterItem) => {
|
||||
return [...String(filter.value).matchAll(/\/(storage|realtime|auth|functions)\//g)]
|
||||
}
|
||||
const currentProductFilter =
|
||||
filters.find((filter) => {
|
||||
const matches = getPathFilterMatchValue(filter)
|
||||
if (filter.key == 'request.path' && matches.length > 0) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) || null
|
||||
|
||||
const currentProductFilterKey = currentProductFilter
|
||||
? getPathFilterMatchValue(currentProductFilter)[0][1]
|
||||
: 'all'
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-row justify-start items-center flex-wrap gap-2">
|
||||
<DatePickers
|
||||
onChange={onDatepickerChange}
|
||||
to={datepickerTo}
|
||||
from={datepickerFrom}
|
||||
helpers={REPORTS_DATEPICKER_HELPERS}
|
||||
/>
|
||||
<Dropdown
|
||||
size="small"
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={
|
||||
<>
|
||||
<Dropdown.Item onClick={() => handleProductFilterChange(currentProductFilter, 'all')}>
|
||||
All
|
||||
<Dropdown.Item onClick={() => handleProductFilterChange(null)}>
|
||||
All Requests
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Separator />
|
||||
{[
|
||||
{ key: 'rest', label: 'REST' },
|
||||
{ key: 'auth', label: 'Auth' },
|
||||
{ key: 'storage', label: 'Storage' },
|
||||
{ key: 'realtime', label: 'Realtime' },
|
||||
{ key: 'functions', label: 'Functions' },
|
||||
{ key: 'graphql', label: 'GraphQL' },
|
||||
].map(({ key, label }) => (
|
||||
<Dropdown.Item
|
||||
key={key}
|
||||
disabled={key === currentProductFilterKey}
|
||||
onClick={() => handleProductFilterChange(currentProductFilter, key)}
|
||||
>
|
||||
<span className={[key === currentProductFilterKey ? 'font-bold' : ''].join(' ')}>
|
||||
{label}
|
||||
</span>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
{PRODUCT_FILTERS.map((productFilter) => {
|
||||
const Icon = productFilter.icon
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key={productFilter.key}
|
||||
disabled={productFilter.key === currentProductFilter?.key}
|
||||
onClick={() => handleProductFilterChange(productFilter)}
|
||||
className="hover:bg-scale-600"
|
||||
icon={<Icon size={24} />}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
productFilter.key === currentProductFilter?.key ? 'font-bold' : '',
|
||||
'inline-block',
|
||||
].join(' ')}
|
||||
>
|
||||
{productFilter.label}
|
||||
</span>
|
||||
<span className=" text-left text-scale-1000 inline-block">
|
||||
{productFilter.description}
|
||||
</span>
|
||||
</Dropdown.Item>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -100,18 +166,15 @@ const ReportFilterBar: React.FC<Props> = ({ filters, onAddFilter, onRemoveFilter
|
||||
className="inline-flex flex-row gap-2"
|
||||
iconRight={<IconChevronDown size={14} />}
|
||||
>
|
||||
{currentProductFilterKey === 'all'
|
||||
? 'All'
|
||||
: `${currentProductFilterKey
|
||||
.slice(0, 1)
|
||||
.toUpperCase()}${currentProductFilterKey.slice(
|
||||
1,
|
||||
currentProductFilterKey.length
|
||||
)}`}
|
||||
{currentProductFilter === null ? 'All Requests' : currentProductFilter.label}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{filters
|
||||
.filter((filter) => filter !== currentProductFilter)
|
||||
.filter(
|
||||
(filter) =>
|
||||
filter.value !== currentProductFilter?.filterValue &&
|
||||
filter.key !== currentProductFilter?.filterKey
|
||||
)
|
||||
.map((filter) => (
|
||||
<div className="text-xs rounded border border-scale-1100 bg-scale-500 px-1 h-7 flex flex-row justify-center gap-1 items-center">
|
||||
{filter.key} {filter.compare} {filter.value}
|
||||
|
||||
@@ -1,48 +1,28 @@
|
||||
import { Button, IconRefreshCw } from 'ui'
|
||||
import DatePickers from '../Settings/Logs/Logs.DatePickers'
|
||||
import { REPORTS_DATEPICKER_HELPERS } from './Reports.constants'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
onDatepickerChange: React.ComponentProps<typeof DatePickers>['onChange']
|
||||
datepickerTo?: string
|
||||
datepickerFrom?: string
|
||||
onRefresh: () => void
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const ReportHeader: React.FC<Props> = ({
|
||||
title,
|
||||
onDatepickerChange,
|
||||
datepickerTo = '',
|
||||
datepickerFrom = '',
|
||||
onRefresh,
|
||||
isLoading,
|
||||
}) => (
|
||||
<div className="flex flex-col gap-4">
|
||||
const ReportHeader: React.FC<Props> = ({ title, onRefresh, isLoading }) => (
|
||||
<div className="flex flex-row justify-between gap-4">
|
||||
<h1 className="text-2xl text-scale-1200">{title}</h1>
|
||||
<div className="flex flex-row justify-start gap-4">
|
||||
<DatePickers
|
||||
onChange={onDatepickerChange}
|
||||
to={datepickerTo}
|
||||
from={datepickerFrom}
|
||||
helpers={REPORTS_DATEPICKER_HELPERS}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading ? true : false}
|
||||
icon={
|
||||
<IconRefreshCw
|
||||
size="tiny"
|
||||
className={`text-scale-1100 ${isLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading ? true : false}
|
||||
icon={
|
||||
<IconRefreshCw
|
||||
size="tiny"
|
||||
className={`text-scale-1100 ${isLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
export default ReportHeader
|
||||
@@ -62,8 +62,7 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
select
|
||||
cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
|
||||
count(t.id) as count
|
||||
FROM
|
||||
edge_logs t
|
||||
FROM edge_logs t
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as response
|
||||
cross join unnest(m.request) as request
|
||||
@@ -79,13 +78,12 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
select
|
||||
cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
|
||||
count(t.id) as count
|
||||
FROM
|
||||
edge_logs t
|
||||
FROM edge_logs t
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as response
|
||||
cross join unnest(m.request) as request
|
||||
WHERE
|
||||
f3.status_code >= 400
|
||||
response.status_code >= 400
|
||||
${generateRexepWhere(filters, false)}
|
||||
GROUP BY
|
||||
timestamp
|
||||
|
||||
@@ -14,24 +14,24 @@ import {
|
||||
} from 'components/interfaces/Reports/renderers/ApiRenderers'
|
||||
import { useState, useEffect } from 'react'
|
||||
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
|
||||
import { LogsEndpointParams } from 'components/interfaces/Settings/Logs'
|
||||
import { DatePickerToFrom, LogsEndpointParams } from 'components/interfaces/Settings/Logs'
|
||||
import ReportFilterBar from 'components/interfaces/Reports/ReportFilterBar'
|
||||
|
||||
export const ApiReport: NextPageWithLayout = () => {
|
||||
const report = useApiReport()
|
||||
const handleDatepickerChange = (value: DatePickerToFrom) => {}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 xl:px-24 2xl:px-32">
|
||||
<ReportHeader
|
||||
title="API Performance"
|
||||
onDatepickerChange={(value) => console.log(value)}
|
||||
datepickerFrom={report.params.totalRequests.iso_timestamp_start}
|
||||
datepickerTo={report.params.totalRequests.iso_timestamp_end}
|
||||
isLoading={report.isLoading}
|
||||
onRefresh={report.refresh}
|
||||
/>
|
||||
|
||||
<ReportFilterBar
|
||||
onDatepickerChange={handleDatepickerChange}
|
||||
datepickerFrom={report.params.totalRequests.iso_timestamp_start}
|
||||
datepickerTo={report.params.totalRequests.iso_timestamp_end}
|
||||
onAddFilter={report.addFilter}
|
||||
onRemoveFilter={report.removeFilter}
|
||||
filters={report.filters}
|
||||
|
||||
Reference in new issue
Block a user