feat: initial overview preset report

This commit is contained in:
TzeYiing authored and Ziinc committed 2022-08-25 02:56:15 +08:00
1 parent 8ac9294368
commit f115a6e53a
12 files changed
+868 -156

No files matched your search

@@ -40,7 +40,6 @@ interface Props {
}
const ProjectUsage: FC<Props> = ({ project }) => {
const logsUsageCodesPaths = useFlag('logsUsageCodesPaths')
const [interval, setInterval] = useState<string>('hourly')
const router = useRouter()
const { ref } = router.query
@@ -50,20 +49,6 @@ const ProjectUsage: FC<Props> = ({ project }) => {
get
)
const { data: codesData, error: codesFetchError } = useSWR<EndpointResponse<StatusCodesDatum>>(
logsUsageCodesPaths
? `${API_URL}/projects/${ref}/analytics/endpoints/usage.api-codes?interval=${interval}`
: null,
get
)
const { data: pathsData, error: _pathsFetchError }: any = useSWR<EndpointResponse<PathsDatum>>(
logsUsageCodesPaths
? `${API_URL}/projects/${ref}/analytics/endpoints/usage.api-paths?interval=${interval}`
: null,
get
)
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
const startDate = dayjs()
.subtract(selectedInterval.startValue, selectedInterval.startUnit)
@@ -223,70 +208,6 @@ const ProjectUsage: FC<Props> = ({ project }) => {
</div>
</>
)}
{logsUsageCodesPaths && (
<div className="grid md:gap-4 lg:grid-cols-4 lg:gap-8">
<Panel
key="api-status-codes"
className="col-span-2 col-start-1"
wrapWithLoading={false}
>
<Panel.Content className="space-y-4">
<PanelHeader title="API Status Codes" />
<StackedAreaChart
dateFormat={datetimeFormat}
data={codesData?.result}
stackKey="status_code"
xAxisKey="timestamp"
yAxisKey="count"
isLoading={!codesData && !codesFetchError ? true : false}
xAxisFormatAsDate
size="large"
styleMap={{
200: { stroke: USAGE_COLORS['200'], fill: USAGE_COLORS['200'] },
201: { stroke: USAGE_COLORS['201'], fill: USAGE_COLORS['201'] },
400: { stroke: USAGE_COLORS['400'], fill: USAGE_COLORS['400'] },
401: { stroke: USAGE_COLORS['401'], fill: USAGE_COLORS['401'] },
404: { stroke: USAGE_COLORS['404'], fill: USAGE_COLORS['404'] },
500: { stroke: USAGE_COLORS['500'], fill: USAGE_COLORS['500'] },
}}
/>
</Panel.Content>
</Panel>
<Panel
key="top-routes"
className="col-span-2 col-start-3 pb-0"
bodyClassName="h-full"
wrapWithLoading={false}
>
<Panel.Content className="space-y-4">
<PanelHeader title="Top Routes" />
<Table
head={
<>
<Table.th>Path</Table.th>
<Table.th>Count</Table.th>
<Table.th>Avg. Latency (ms)</Table.th>
</>
}
body={
<>
{(pathsData?.result ?? []).map((row: PathsDatum) => (
<Table.tr>
<Table.td className="flex items-center space-x-2">
<p className="text-scale-1200 font-mono text-sm">{row.method}</p>
<p className="font-mono text-sm">{row.path}</p>
</Table.td>
<Table.td>{row.count}</Table.td>
<Table.td>{Number(row.avg_origin_time).toFixed(2)}</Table.td>
</Table.tr>
))}
</>
}
/>
</Panel.Content>
</Panel>
</div>
)}
</div>
</div>
)
@@ -0,0 +1,228 @@
import {
Button,
Collapsible,
IconChevronLeft,
IconChevronRight,
IconChevronUp,
IconRefreshCw,
} from '@supabase/ui'
import Table from 'components/to-be-cleaned/Table'
import { USAGE_COLORS } from 'components/ui/Charts/Charts.constants'
import StackedAreaChart from 'components/ui/Charts/StackedAreaChart'
import Panel from 'components/ui/Panel'
import useLogsQuery from 'hooks/analytics/useLogsQuery'
import { useState } from 'react'
import { DatePickerToFrom } from '../Settings/Logs'
import DatePickers from '../Settings/Logs/Logs.DatePickers'
import { jsonSyntaxHighlight } from '../Settings/Logs/LogsFormatters'
import { DATETIME_FORMAT, PRESET_CONFIG, REPORTS_DATEPICKER_HELPERS } from './Reports.constants'
import { Presets } from './Reports.types'
interface Props {
preset: Presets
projectRef: string
}
const DEFAULT_PARAMS = {
iso_timestamp_start: REPORTS_DATEPICKER_HELPERS[0].calcFrom(),
iso_timestamp_end: REPORTS_DATEPICKER_HELPERS[0].calcTo(),
}
const PresetReport: React.FC<Props> = ({ projectRef, preset }) => {
const config = PRESET_CONFIG[preset]
const [statusCodes, statusCodesHandlers] = useLogsQuery(projectRef, {
sql: config.sql.statusCodes(),
...DEFAULT_PARAMS,
})
const [requestPaths, requestPathsHandlers] = useLogsQuery(projectRef, {
sql: config.sql.requestPaths(12),
...DEFAULT_PARAMS,
})
const handleRefresh = () => {
statusCodesHandlers.runQuery()
requestPathsHandlers.runQuery()
}
const handleDatepickerChange = ({ to, from }: DatePickerToFrom) => {
const newParams = { iso_timestamp_start: from || '', iso_timestamp_end: to || '' }
statusCodesHandlers.setParams((prev) => ({ ...prev, ...newParams }))
requestPathsHandlers.setParams((prev) => ({ ...prev, ...newParams }))
}
const isLoading = statusCodes.isLoading || requestPaths.isLoading
const requestPathsSums = requestPaths.logData.map((v) => v.sum) as number[]
const requestPathsSumMax = Math.max(...requestPathsSums)
const requestPathsSumMin = Math.min(...requestPathsSums)
return (
<div className="mx-auto flex flex-col gap-4 px-5 lg:px-16 xl:px-24 1xl:px-28 2xl:px-32 py-6">
<h1 className="text-2xl text-scale-1200">{config.title}</h1>
<div className="flex flex-row justify-between">
<DatePickers
onChange={handleDatepickerChange}
to={statusCodes.params.iso_timestamp_end || ''}
from={statusCodes.params.iso_timestamp_end || ''}
helpers={REPORTS_DATEPICKER_HELPERS}
/>
<Button
type="default"
size="tiny"
onClick={handleRefresh}
disabled={isLoading ? true : false}
icon={
<IconRefreshCw
size="tiny"
className={`text-scale-1100 ${isLoading ? 'animate-spin' : ''}`}
/>
}
>
Refresh
</Button>
</div>
<div className="grid md:gap-4 lg:grid-cols-4 lg:gap-8">
<Panel key="api-status-codes" className="col-span-4 col-start-1" wrapWithLoading={false}>
<Panel.Content className="space-y-4">
<h3>API Status Codes</h3>
<StackedAreaChart
dateFormat={DATETIME_FORMAT}
data={statusCodes.logData}
stackKey="status_code"
xAxisKey="timestamp"
yAxisKey="count"
isLoading={statusCodes.isLoading}
xAxisFormatAsDate
size="large"
styleMap={{
200: { stroke: USAGE_COLORS['200'], fill: USAGE_COLORS['200'] },
201: { stroke: USAGE_COLORS['201'], fill: USAGE_COLORS['201'] },
400: { stroke: USAGE_COLORS['400'], fill: USAGE_COLORS['400'] },
401: { stroke: USAGE_COLORS['401'], fill: USAGE_COLORS['401'] },
404: { stroke: USAGE_COLORS['404'], fill: USAGE_COLORS['404'] },
500: { stroke: USAGE_COLORS['500'], fill: USAGE_COLORS['500'] },
}}
/>
</Panel.Content>
</Panel>
<Panel
key="top-routes"
className="col-span-4 col-start-1 pb-0"
bodyClassName="h-full"
wrapWithLoading={false}
>
<Panel.Content className="space-y-4">
<h3>Slow API Requests</h3>
<Table
className="border border-scale-600 rounded"
head={
<>
<Table.th>Path</Table.th>
<Table.th>Count</Table.th>
<Table.th>Avg. Time (ms)</Table.th>
<Table.th>Total Query Time</Table.th>
</>
}
body={
<>
{requestPaths.logData.map((row: any, index) => {
const totalQueryTimePercentage =
((row.sum - requestPathsSumMin) / requestPathsSumMax) * 100
return (
<Table.tr key={index}>
<Table.td className="max-w-sm lg:max-w-lg" style={{ padding: '0.3rem' }}>
<Collapsible className="w-full flex flex-col gap-2">
<Collapsible.Trigger asChild>
<button
className="w-full text-scale-1200 flex justify-start space-x-1"
type="button"
>
<IconChevronRight
size="tiny"
className="transition data-open-parent:rotate-90 data-closed-parent:rotate-0"
/>
<div className="flex space-x-2 items-center overflow-x-none">
<p className="text-scale-1200 font-mono text-xs">{row.method}</p>
<p className="font-mono text-scale-1200 text-xs truncate max-w-xs ">
{row.path}
<span className="text-scale-1000">{row.query_params}</span>
</p>
</div>
</button>
</Collapsible.Trigger>
<Collapsible.Content className="bg-scale-300 p-2 rounded">
<pre className="text-xs syntax-highlight overflow-x-auto">
<div
className="text-wrap"
dangerouslySetInnerHTML={{
__html: jsonSyntaxHighlight(
queryParamsToObject(row.query_params)
),
}}
/>
</pre>
</Collapsible.Content>
</Collapsible>
</Table.td>
<Table.td style={{ padding: '0.5rem' }} className="text-xs align-top">
{row.count}
</Table.td>
<Table.td style={{ padding: '0.5rem' }} className="text-xs align-top">
{Number(row.avg_origin_time).toFixed(2)}
</Table.td>
<Table.td className="align-top py-1">
<div
className={`mt-1 h-2 rounded w-full bg-green-1100`}
style={{
width: `${totalQueryTimePercentage}%`,
}}
/>
</Table.td>
</Table.tr>
)
})}
</>
}
/>
</Panel.Content>
</Panel>
</div>
</div>
)
}
const queryParamsToObject = (params: string) => {
return Object.fromEntries(new URLSearchParams(params))
}
// const usePresetReport = (projectRef: string, preset: Presets) => {
// const params: LogsEndpointParams = { ...paramsToMerge, project: projectRef, sql }
// const endpointUrl = `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${genQueryParams(
// params as any
// )}`
// const {
// data,
// error: swrError,
// isValidating,
// mutate,
// } = useSWR<any>(endpointUrl, get, {
// revalidateOnFocus: false,
// revalidateIfStale: false,
// revalidateOnReconnect: false,
// dedupingInterval: 5000,
// })
// let error: null | string | object = swrError ? swrError.message : null
// return [
// {
// logData: data?.result ? data.result[0] : undefined,
// isLoading: isValidating,
// error,
// },
// {
// refresh: () => mutate(),
// },
// ]
// }
// const useWidgetQuery = (sql: string) => {}
export default PresetReport
@@ -0,0 +1,83 @@
import dayjs from 'dayjs'
import { DatetimeHelper } from '../Settings/Logs'
import { Presets } from './Reports.types'
export const REPORTS_DATEPICKER_HELPERS: DatetimeHelper[] = [
// {
// text: 'Last 1 day',
// calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(),
// calcTo: () => '',
// },
{
text: 'Last 7 days',
calcFrom: () => dayjs().subtract(7, 'day').startOf('day').toISOString(),
calcTo: () => '',
default: true,
},
{
text: 'Last 14 days',
calcFrom: () => dayjs().subtract(7, 'day').startOf('day').toISOString(),
calcTo: () => '',
},
{
text: 'Last 30 days',
calcFrom: () => dayjs().subtract(30, 'day').startOf('day').toISOString(),
calcTo: () => '',
},
{
text: 'Last 90 days',
calcFrom: () => dayjs().subtract(90, 'day').startOf('day').toISOString(),
calcTo: () => '',
},
]
export const PRESET_CONFIG = {
[Presets.OVERVIEW]: {
title: 'Overview',
sql: {
statusCodes: () => `
select
timestamp_trunc(timestamp, hour) as timestamp,
r.status_code as status_code,
count(status_code) as count
FROM
edge_logs
CROSS JOIN UNNEST(metadata) AS m
CROSS JOIN UNNEST(m.response) AS r
GROUP BY
timestamp,
status_code
ORDER BY
timestamp ASC`,
requestPaths: (limit: number) => `
select
f2.path as path,
f2.search as query_params,
f2.method as method,
f3.status_code as status_code,
avg(f3.origin_time) as avg_origin_time,
sum(f3.origin_time) as sum,
--APPROX_QUANTILES(f3.origin_time, 100) as p95_array,
--APPROX_QUANTILES(f3.origin_time, 100) as p99_array,
count(timestamp) as count
FROM
edge_logs
LEFT JOIN UNNEST(metadata) AS f1 ON TRUE
LEFT JOIN UNNEST(f1.request) AS f2 ON TRUE
LEFT JOIN UNNEST(f1.response) AS f3 ON TRUE
GROUP BY
path,
query_params,
method,
status_code
ORDER BY
sum DESC,
count desc,
avg_origin_time DESC
LIMIT ${limit}
`
},
},
}
export const DATETIME_FORMAT = 'MMM D, ha'
@@ -0,0 +1,3 @@
export enum Presets {
OVERVIEW = "overview"
}
@@ -0,0 +1,56 @@
import { FC, ReactNode } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore, withAuth } from 'hooks'
import { generateSettingsMenu } from './SettingsMenu.utils'
import BaseLayout from '..'
import ProductMenu from 'components/ui/ProductMenu'
interface Props {
title: string
children: ReactNode
}
const ReportsLayout: FC<Props> = ({ title, children }) => {
const { ui } = useStore()
const projectRef = ui.selectedProjectRef as string
const router = useRouter()
const page = router.pathname.split('/')[4] || ''
const menuRoutes = [
{
items: [
{
name: 'Overview',
key: '',
url: `/project/${projectRef}/reports`,
items: [],
},
],
},
{
items: [
{
name: 'Dashboard',
key: 'dashboard',
url: `/project/${projectRef}/reports/dashboard`,
items: [],
},
],
},
]
return (
<BaseLayout
title={title}
product="Reports"
productMenu={<ProductMenu page={page} menu={menuRoutes} />}
>
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
{children}
</main>
</BaseLayout>
)
}
export default withAuth(observer(ReportsLayout))
@@ -19,10 +19,12 @@ const ProductMenu: FC<Props> = ({ page, menu }) => {
<Menu.Group
//@ts-ignore
title={
<div className="flex flex-col space-y-2">
<span>{group.title}</span>
{group.isPreview && <Badge color="amber">Not production ready</Badge>}
</div>
group.title ? (
<div className="flex flex-col space-y-2">
<span>{group.title}</span>
{group.isPreview && <Badge color="amber">Not production ready</Badge>}
</div>
) : null
}
/>
<div>
@@ -1,7 +1,7 @@
import { ReactNode } from 'react'
export interface ProductMenuGroup {
title: string
title?: string
isPreview?: boolean
items: ProductMenuGroupItem[]
}