Skip to content

Commit

Permalink
Chore/reports read replicas filter (supabase#23711)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people committed May 7, 2024
1 parent 02c0d75 commit 1afaae6
Show file tree
Hide file tree
Showing 26 changed files with 650 additions and 737 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { InformationCircleIcon } from '@heroicons/react/16/solid'
import { X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import toast from 'react-hot-toast'
import { X } from 'lucide-react'

import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { executeSql } from 'data/sql/execute-sql-query'
import { useLocalStorageQuery } from 'hooks'
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
Button,
TabsList_Shadcn_,
Expand All @@ -28,7 +29,6 @@ import { PresetHookResult } from '../Reports/Reports.utils'
import { QUERY_PERFORMANCE_REPORT_TYPES } from './QueryPerformance.constants'
import { QueryPerformanceFilterBar } from './QueryPerformanceFilterBar'
import { QueryPerformanceGrid } from './QueryPerformanceGrid'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'

interface QueryPerformanceProps {
queryHitRate: PresetHookResult
Expand Down
49 changes: 43 additions & 6 deletions apps/studio/components/interfaces/Reports/GridResize.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import { X } from 'lucide-react'
import RGL, { WidthProvider } from 'react-grid-layout'
import { LAYOUT_COLUMN_COUNT } from './Reports.constants'

import { useParams } from 'common'
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { Button, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui'
import { LAYOUT_COLUMN_COUNT } from './Reports.constants'

const ReactGridLayout = WidthProvider(RGL)

const GridResize = ({ startDate, endDate, interval, editableReport, setEditableReport }: any) => {
if (!editableReport) return null
interface GridResizeProps {
startDate: string
endDate: string
interval: string
editableReport: any
onRemoveChart: ({ metric }: { metric: { key: string } }) => void
setEditableReport: (payload: any) => void
}

const GridResize = ({
startDate,
endDate,
interval,
editableReport,
onRemoveChart,
setEditableReport,
}: GridResizeProps) => {
const { ref } = useParams()
const state = useDatabaseSelectorStateSnapshot()

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

if (!editableReport) return null

return (
<>
<ReactGridLayout
Expand All @@ -47,13 +71,26 @@ const GridResize = ({ startDate, endDate, interval, editableReport, setEditableR
interval={interval}
attribute={x.attribute}
provider={x.provider}
label={x.label}
label={`${x.label}${ref !== state.selectedDatabaseId ? (x.provider === 'infra-monitoring' ? ' of replica' : ' on project') : ''}`}
customDateFormat={'MMM D, YYYY'}
/>
>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<Button
type="text"
icon={<X />}
className="ml-2 px-1"
onClick={() => onRemoveChart({ metric: { key: x.attribute } })}
/>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom">Remove chart</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
</ChartHandler>

<div className="absolute inset-x-0 top-3 ">
<div className="flex justify-around">
<div className="flex h-3 w-24 cursor-move flex-col space-y-2">
<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>
<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>
</div>
</div>
Expand Down
57 changes: 57 additions & 0 deletions apps/studio/components/interfaces/Reports/MetricOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Home } from 'lucide-react'

import { useIsFeatureEnabled } from 'hooks'
import { METRICS, METRIC_CATEGORIES } from 'lib/constants/metrics'
import {
DropdownMenuCheckboxItem,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from 'ui'

interface MetricOptionsProps {
config: any
handleChartSelection: any
}

export const MetricOptions = ({ config, handleChartSelection }: MetricOptionsProps) => {
const { projectAuthAll: authEnabled, projectStorageAll: storageEnabled } = useIsFeatureEnabled([
'project_auth:all',
'project_storage:all',
])

const metricCategories = Object.values(METRIC_CATEGORIES).filter(({ key }) => {
if (key === 'api_auth') return authEnabled
if (key === 'api_storage') return storageEnabled
return true
})

return metricCategories.map((cat) => {
return (
<DropdownMenuSub key={cat.key}>
<DropdownMenuSubTrigger className="space-x-2">
{cat.icon ? cat.icon : <Home size={14} />}
<p>{cat.label}</p>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{METRICS.filter((metric) => metric?.category?.key === cat.key).map((metric) => {
return (
<DropdownMenuCheckboxItem
key={metric.key}
checked={config.layout?.some((x: any) => x.attribute === metric.key)}
onCheckedChange={(e) => handleChartSelection({ metric, value: e })}
>
<div className="flex flex-col space-y-0">
<span>{metric.label}</span>
</div>
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)
})
}
54 changes: 34 additions & 20 deletions apps/studio/components/interfaces/Reports/ReportFilterBar.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { ChevronDown, Database, Plus, X } from 'lucide-react'
import { ComponentProps, useState } from 'react'
import SVG from 'react-inlinesvg'

import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import { Auth, Realtime, Storage } from 'icons'
import { BASE_PATH } from 'lib/constants'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
IconBox,
IconChevronDown,
IconCode,
IconDatabase,
IconKey,
IconPlus,
IconX,
IconZap,
Input,
Popover,
Select,
cn,
} from 'ui'

import DatePickers from '../Settings/Logs/Logs.DatePickers'
import { REPORTS_DATEPICKER_HELPERS } from './Reports.constants'
import type { ReportFilterItem } from './Reports.types'
Expand All @@ -41,39 +39,39 @@ const PRODUCT_FILTERS = [
filterValue: '/rest',
label: 'REST',
description: 'Requests made to PostgREST',
icon: IconDatabase,
icon: Database,
},
{
key: 'auth',
filterKey: 'request.path',
filterValue: '/auth',
label: 'Auth',
description: 'Auth and authorization requests',
icon: IconKey,
icon: Auth,
},
{
key: 'storage',
filterKey: 'request.path',
filterValue: '/storage',
label: 'Storage',
description: 'Storage asset requests',
icon: IconBox,
icon: Storage,
},
{
key: 'realtime',
filterKey: 'request.path',
filterValue: '/realtime',
label: 'Realtime',
description: 'Realtime connection requests',
icon: IconZap,
icon: Realtime,
},
{
key: 'graphql',
filterKey: 'request.path',
filterValue: '/graphql',
label: 'GraphQL',
description: 'Requests made to pg_graphql',
icon: IconCode,
icon: null,
},
]

Expand All @@ -86,6 +84,10 @@ const ReportFilterBar = ({
onRemoveFilters,
datepickerHelpers,
}: ReportFilterBarProps) => {
const { project } = useProjectContext()
// [Joshen] TODO: Once API support is out
const showReadReplicasUI = false // project?.is_read_replicas_enabled

const filterKeys = [
'request.path',
'request.method',
Expand Down Expand Up @@ -135,7 +137,7 @@ const ReportFilterBar = ({
}

return (
<div>
<div className="flex items-center justify-between">
<div className="flex flex-row justify-start items-center flex-wrap gap-2">
<DatePickers
onChange={onDatepickerChange}
Expand All @@ -148,7 +150,7 @@ const ReportFilterBar = ({
<Button
type="default"
className="inline-flex flex-row gap-2"
iconRight={<IconChevronDown size={14} />}
iconRight={<ChevronDown size={14} />}
>
<span>
{currentProductFilter === null ? 'All Requests' : currentProductFilter.label}
Expand All @@ -162,14 +164,25 @@ const ReportFilterBar = ({
<DropdownMenuSeparator />
{PRODUCT_FILTERS.map((productFilter) => {
const Icon = productFilter.icon

return (
<DropdownMenuItem
key={productFilter.key}
className="space-x-2"
disabled={productFilter.key === currentProductFilter?.key}
onClick={() => handleProductFilterChange(productFilter)}
>
<Icon size={20} className="mr-2" />
{productFilter.key === 'graphql' ? (
<SVG
src={`${BASE_PATH}/img/graphql.svg`}
className="w-[20px] h-[20px] mr-2"
preProcessor={(code) =>
code.replace(/svg/, 'svg class="m-auto text-color-inherit"')
}
/>
) : Icon !== null ? (
<Icon size={20} strokeWidth={1.5} className="mr-2" />
) : null}
<div className="flex flex-col">
<p
className={cn(
Expand Down Expand Up @@ -205,7 +218,7 @@ const ReportFilterBar = ({
size="tiny"
className="!p-0 !space-x-0"
onClick={() => onRemoveFilters([filter])}
icon={<IconX size="tiny" className="text-foreground-light" />}
icon={<X size={14} className="text-foreground-light" />}
>
<span className="sr-only">Remove</span>
</Button>
Expand Down Expand Up @@ -285,12 +298,13 @@ const ReportFilterBar = ({
asChild
type="default"
size="tiny"
icon={<IconPlus size="tiny" className={`text-foreground-light `} />}
icon={<Plus size={14} className={`text-foreground-light `} />}
>
<span>Add filter</span>
</Button>
</Popover>
</div>
{showReadReplicasUI && <DatabaseSelector />}
</div>
)
}
Expand Down
27 changes: 12 additions & 15 deletions apps/studio/components/interfaces/Reports/ReportWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import Panel from 'components/ui/Panel'
import { ExternalLink, HelpCircle } from 'lucide-react'
import { NextRouter, useRouter } from 'next/router'
import { Button, IconExternalLink, IconHelpCircle, Loading } from 'ui'

import { useParams } from 'common'
import Panel from 'components/ui/Panel'
import { Button, Loading, cn } from 'ui'
import { LogsEndpointParams } from '../Settings/Logs'
import type { BaseReportParams, ReportQueryType } from './Reports.types'

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

const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
const ReportWidget = (props: ReportWidgetProps) => {
const router = useRouter()
const { ref } = router.query
const { ref } = useParams()
const projectRef = ref as string

return (
<Panel
noMargin
noHideOverflow
className={'pb-0 ' + props.className}
className={cn('pb-0', props.className)}
bodyClassName="h-full"
wrapWithLoading={false}
>
Expand All @@ -47,11 +51,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
{props?.tooltip && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconHelpCircle
className="text-foreground-light"
size="tiny"
strokeWidth={1.5}
/>
<HelpCircle className="text-foreground-light" size={14} />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
Expand All @@ -76,7 +76,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
<Tooltip.Trigger asChild>
<Button
type="default"
icon={<IconExternalLink strokeWidth={1.5} />}
icon={<ExternalLink />}
className="px-1"
onClick={() => {
const isDbQueryType = props.queryType === 'db'
Expand All @@ -95,10 +95,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
query.ite = props.params!.iso_timestamp_end
}

router.push({
pathname,
query,
})
router.push({ pathname, query })
}}
/>
</Tooltip.Trigger>
Expand Down
Loading

0 comments on commit 1afaae6

Please sign in to comment.