Skip to content

Commit 1afaae6

Browse files
joshenlimalaister
andauthored
Chore/reports read replicas filter (supabase#23711)
* Scaffold for API reports * Add support for filtering database reports by replicas * midway adding read replica support for custom reports * Refactor reports page to use react query and deprecate project content mobxstore * Reports add chart labels if metric is read replica specific * Give an easier way to remove charts from reports * make reports layout non-blocking * Update apps/studio/components/interfaces/Reports/Reports.tsx Co-authored-by: Alaister Young <[email protected]> --------- Co-authored-by: Alaister Young <[email protected]> Co-authored-by: Alaister Young <[email protected]>
1 parent 02c0d75 commit 1afaae6

File tree

26 files changed

+650
-737
lines changed

26 files changed

+650
-737
lines changed

apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { InformationCircleIcon } from '@heroicons/react/16/solid'
2+
import { X } from 'lucide-react'
23
import { useRouter } from 'next/router'
34
import { useEffect, useMemo, useState } from 'react'
45
import toast from 'react-hot-toast'
5-
import { X } from 'lucide-react'
66

77
import { useParams } from 'common'
88
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
99
import { executeSql } from 'data/sql/execute-sql-query'
1010
import { useLocalStorageQuery } from 'hooks'
1111
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
1212
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
13+
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
1314
import {
1415
Button,
1516
TabsList_Shadcn_,
@@ -28,7 +29,6 @@ import { PresetHookResult } from '../Reports/Reports.utils'
2829
import { QUERY_PERFORMANCE_REPORT_TYPES } from './QueryPerformance.constants'
2930
import { QueryPerformanceFilterBar } from './QueryPerformanceFilterBar'
3031
import { QueryPerformanceGrid } from './QueryPerformanceGrid'
31-
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
3232

3333
interface QueryPerformanceProps {
3434
queryHitRate: PresetHookResult

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

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
1+
import { X } from 'lucide-react'
12
import RGL, { WidthProvider } from 'react-grid-layout'
2-
import { LAYOUT_COLUMN_COUNT } from './Reports.constants'
3+
4+
import { useParams } from 'common'
35
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
6+
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
7+
import { Button, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui'
8+
import { LAYOUT_COLUMN_COUNT } from './Reports.constants'
49

510
const ReactGridLayout = WidthProvider(RGL)
611

7-
const GridResize = ({ startDate, endDate, interval, editableReport, setEditableReport }: any) => {
8-
if (!editableReport) return null
12+
interface GridResizeProps {
13+
startDate: string
14+
endDate: string
15+
interval: string
16+
editableReport: any
17+
onRemoveChart: ({ metric }: { metric: { key: string } }) => void
18+
setEditableReport: (payload: any) => void
19+
}
20+
21+
const GridResize = ({
22+
startDate,
23+
endDate,
24+
interval,
25+
editableReport,
26+
onRemoveChart,
27+
setEditableReport,
28+
}: GridResizeProps) => {
29+
const { ref } = useParams()
30+
const state = useDatabaseSelectorStateSnapshot()
931

1032
function onLayoutChange(layout: any) {
1133
let updatedLayout = editableReport.layout
@@ -23,6 +45,8 @@ const GridResize = ({ startDate, endDate, interval, editableReport, setEditableR
2345
setEditableReport(payload)
2446
}
2547

48+
if (!editableReport) return null
49+
2650
return (
2751
<>
2852
<ReactGridLayout
@@ -47,13 +71,26 @@ const GridResize = ({ startDate, endDate, interval, editableReport, setEditableR
4771
interval={interval}
4872
attribute={x.attribute}
4973
provider={x.provider}
50-
label={x.label}
74+
label={`${x.label}${ref !== state.selectedDatabaseId ? (x.provider === 'infra-monitoring' ? ' of replica' : ' on project') : ''}`}
5175
customDateFormat={'MMM D, YYYY'}
52-
/>
76+
>
77+
<Tooltip_Shadcn_>
78+
<TooltipTrigger_Shadcn_ asChild>
79+
<Button
80+
type="text"
81+
icon={<X />}
82+
className="ml-2 px-1"
83+
onClick={() => onRemoveChart({ metric: { key: x.attribute } })}
84+
/>
85+
</TooltipTrigger_Shadcn_>
86+
<TooltipContent_Shadcn_ side="bottom">Remove chart</TooltipContent_Shadcn_>
87+
</Tooltip_Shadcn_>
88+
</ChartHandler>
89+
5390
<div className="absolute inset-x-0 top-3 ">
5491
<div className="flex justify-around">
5592
<div className="flex h-3 w-24 cursor-move flex-col space-y-2">
56-
<div className="hidden h-3 w-full border-4 border-dotted border-green-900 opacity-50 transition-all hover:opacity-100 group-hover:block"></div>
93+
<div className="hidden h-3 w-full border-4 border-dotted border-green-900 opacity-50 transition-all hover:opacity-100 group-hover:block" />
5794
</div>
5895
</div>
5996
</div>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Home } from 'lucide-react'
2+
3+
import { useIsFeatureEnabled } from 'hooks'
4+
import { METRICS, METRIC_CATEGORIES } from 'lib/constants/metrics'
5+
import {
6+
DropdownMenuCheckboxItem,
7+
DropdownMenuPortal,
8+
DropdownMenuSub,
9+
DropdownMenuSubContent,
10+
DropdownMenuSubTrigger,
11+
} from 'ui'
12+
13+
interface MetricOptionsProps {
14+
config: any
15+
handleChartSelection: any
16+
}
17+
18+
export const MetricOptions = ({ config, handleChartSelection }: MetricOptionsProps) => {
19+
const { projectAuthAll: authEnabled, projectStorageAll: storageEnabled } = useIsFeatureEnabled([
20+
'project_auth:all',
21+
'project_storage:all',
22+
])
23+
24+
const metricCategories = Object.values(METRIC_CATEGORIES).filter(({ key }) => {
25+
if (key === 'api_auth') return authEnabled
26+
if (key === 'api_storage') return storageEnabled
27+
return true
28+
})
29+
30+
return metricCategories.map((cat) => {
31+
return (
32+
<DropdownMenuSub key={cat.key}>
33+
<DropdownMenuSubTrigger className="space-x-2">
34+
{cat.icon ? cat.icon : <Home size={14} />}
35+
<p>{cat.label}</p>
36+
</DropdownMenuSubTrigger>
37+
<DropdownMenuPortal>
38+
<DropdownMenuSubContent>
39+
{METRICS.filter((metric) => metric?.category?.key === cat.key).map((metric) => {
40+
return (
41+
<DropdownMenuCheckboxItem
42+
key={metric.key}
43+
checked={config.layout?.some((x: any) => x.attribute === metric.key)}
44+
onCheckedChange={(e) => handleChartSelection({ metric, value: e })}
45+
>
46+
<div className="flex flex-col space-y-0">
47+
<span>{metric.label}</span>
48+
</div>
49+
</DropdownMenuCheckboxItem>
50+
)
51+
})}
52+
</DropdownMenuSubContent>
53+
</DropdownMenuPortal>
54+
</DropdownMenuSub>
55+
)
56+
})
57+
}

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

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
1+
import { ChevronDown, Database, Plus, X } from 'lucide-react'
12
import { ComponentProps, useState } from 'react'
3+
import SVG from 'react-inlinesvg'
4+
5+
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
6+
import DatabaseSelector from 'components/ui/DatabaseSelector'
7+
import { Auth, Realtime, Storage } from 'icons'
8+
import { BASE_PATH } from 'lib/constants'
29
import {
310
Button,
4-
cn,
511
DropdownMenu,
612
DropdownMenuContent,
713
DropdownMenuItem,
814
DropdownMenuSeparator,
915
DropdownMenuTrigger,
10-
IconBox,
11-
IconChevronDown,
12-
IconCode,
13-
IconDatabase,
14-
IconKey,
15-
IconPlus,
16-
IconX,
17-
IconZap,
1816
Input,
1917
Popover,
2018
Select,
19+
cn,
2120
} from 'ui'
22-
2321
import DatePickers from '../Settings/Logs/Logs.DatePickers'
2422
import { REPORTS_DATEPICKER_HELPERS } from './Reports.constants'
2523
import type { ReportFilterItem } from './Reports.types'
@@ -41,39 +39,39 @@ const PRODUCT_FILTERS = [
4139
filterValue: '/rest',
4240
label: 'REST',
4341
description: 'Requests made to PostgREST',
44-
icon: IconDatabase,
42+
icon: Database,
4543
},
4644
{
4745
key: 'auth',
4846
filterKey: 'request.path',
4947
filterValue: '/auth',
5048
label: 'Auth',
5149
description: 'Auth and authorization requests',
52-
icon: IconKey,
50+
icon: Auth,
5351
},
5452
{
5553
key: 'storage',
5654
filterKey: 'request.path',
5755
filterValue: '/storage',
5856
label: 'Storage',
5957
description: 'Storage asset requests',
60-
icon: IconBox,
58+
icon: Storage,
6159
},
6260
{
6361
key: 'realtime',
6462
filterKey: 'request.path',
6563
filterValue: '/realtime',
6664
label: 'Realtime',
6765
description: 'Realtime connection requests',
68-
icon: IconZap,
66+
icon: Realtime,
6967
},
7068
{
7169
key: 'graphql',
7270
filterKey: 'request.path',
7371
filterValue: '/graphql',
7472
label: 'GraphQL',
7573
description: 'Requests made to pg_graphql',
76-
icon: IconCode,
74+
icon: null,
7775
},
7876
]
7977

@@ -86,6 +84,10 @@ const ReportFilterBar = ({
8684
onRemoveFilters,
8785
datepickerHelpers,
8886
}: ReportFilterBarProps) => {
87+
const { project } = useProjectContext()
88+
// [Joshen] TODO: Once API support is out
89+
const showReadReplicasUI = false // project?.is_read_replicas_enabled
90+
8991
const filterKeys = [
9092
'request.path',
9193
'request.method',
@@ -135,7 +137,7 @@ const ReportFilterBar = ({
135137
}
136138

137139
return (
138-
<div>
140+
<div className="flex items-center justify-between">
139141
<div className="flex flex-row justify-start items-center flex-wrap gap-2">
140142
<DatePickers
141143
onChange={onDatepickerChange}
@@ -148,7 +150,7 @@ const ReportFilterBar = ({
148150
<Button
149151
type="default"
150152
className="inline-flex flex-row gap-2"
151-
iconRight={<IconChevronDown size={14} />}
153+
iconRight={<ChevronDown size={14} />}
152154
>
153155
<span>
154156
{currentProductFilter === null ? 'All Requests' : currentProductFilter.label}
@@ -162,14 +164,25 @@ const ReportFilterBar = ({
162164
<DropdownMenuSeparator />
163165
{PRODUCT_FILTERS.map((productFilter) => {
164166
const Icon = productFilter.icon
167+
165168
return (
166169
<DropdownMenuItem
167170
key={productFilter.key}
168171
className="space-x-2"
169172
disabled={productFilter.key === currentProductFilter?.key}
170173
onClick={() => handleProductFilterChange(productFilter)}
171174
>
172-
<Icon size={20} className="mr-2" />
175+
{productFilter.key === 'graphql' ? (
176+
<SVG
177+
src={`${BASE_PATH}/img/graphql.svg`}
178+
className="w-[20px] h-[20px] mr-2"
179+
preProcessor={(code) =>
180+
code.replace(/svg/, 'svg class="m-auto text-color-inherit"')
181+
}
182+
/>
183+
) : Icon !== null ? (
184+
<Icon size={20} strokeWidth={1.5} className="mr-2" />
185+
) : null}
173186
<div className="flex flex-col">
174187
<p
175188
className={cn(
@@ -205,7 +218,7 @@ const ReportFilterBar = ({
205218
size="tiny"
206219
className="!p-0 !space-x-0"
207220
onClick={() => onRemoveFilters([filter])}
208-
icon={<IconX size="tiny" className="text-foreground-light" />}
221+
icon={<X size={14} className="text-foreground-light" />}
209222
>
210223
<span className="sr-only">Remove</span>
211224
</Button>
@@ -285,12 +298,13 @@ const ReportFilterBar = ({
285298
asChild
286299
type="default"
287300
size="tiny"
288-
icon={<IconPlus size="tiny" className={`text-foreground-light `} />}
301+
icon={<Plus size={14} className={`text-foreground-light `} />}
289302
>
290303
<span>Add filter</span>
291304
</Button>
292305
</Popover>
293306
</div>
307+
{showReadReplicasUI && <DatabaseSelector />}
294308
</div>
295309
)
296310
}

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

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import * as Tooltip from '@radix-ui/react-tooltip'
2-
import Panel from 'components/ui/Panel'
2+
import { ExternalLink, HelpCircle } from 'lucide-react'
33
import { NextRouter, useRouter } from 'next/router'
4-
import { Button, IconExternalLink, IconHelpCircle, Loading } from 'ui'
4+
5+
import { useParams } from 'common'
6+
import Panel from 'components/ui/Panel'
7+
import { Button, Loading, cn } from 'ui'
58
import { LogsEndpointParams } from '../Settings/Logs'
69
import type { BaseReportParams, ReportQueryType } from './Reports.types'
710

@@ -27,15 +30,16 @@ export interface ReportWidgetRendererProps<T = any> extends ReportWidgetProps<T>
2730
projectRef: string
2831
}
2932

30-
const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
33+
const ReportWidget = (props: ReportWidgetProps) => {
3134
const router = useRouter()
32-
const { ref } = router.query
35+
const { ref } = useParams()
3336
const projectRef = ref as string
37+
3438
return (
3539
<Panel
3640
noMargin
3741
noHideOverflow
38-
className={'pb-0 ' + props.className}
42+
className={cn('pb-0', props.className)}
3943
bodyClassName="h-full"
4044
wrapWithLoading={false}
4145
>
@@ -47,11 +51,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
4751
{props?.tooltip && (
4852
<Tooltip.Root delayDuration={0}>
4953
<Tooltip.Trigger>
50-
<IconHelpCircle
51-
className="text-foreground-light"
52-
size="tiny"
53-
strokeWidth={1.5}
54-
/>
54+
<HelpCircle className="text-foreground-light" size={14} />
5555
</Tooltip.Trigger>
5656
<Tooltip.Portal>
5757
<Tooltip.Content side="bottom">
@@ -76,7 +76,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
7676
<Tooltip.Trigger asChild>
7777
<Button
7878
type="default"
79-
icon={<IconExternalLink strokeWidth={1.5} />}
79+
icon={<ExternalLink />}
8080
className="px-1"
8181
onClick={() => {
8282
const isDbQueryType = props.queryType === 'db'
@@ -95,10 +95,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
9595
query.ite = props.params!.iso_timestamp_end
9696
}
9797

98-
router.push({
99-
pathname,
100-
query,
101-
})
98+
router.push({ pathname, query })
10299
}}
103100
/>
104101
</Tooltip.Trigger>

0 commit comments

Comments
 (0)