mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
Merge pull request #14063 from supabase/feat/report-widget-expandable-api-requests
feat: add route information to API report
This commit is contained in:
9 files changed
+368
-63
No files matched your search
@@ -12,7 +12,7 @@ import {
|
||||
IconBox,
|
||||
IconCode,
|
||||
IconDatabase,
|
||||
IconZapOff,
|
||||
IconZap,
|
||||
} from 'ui'
|
||||
import DatePickers from '../Settings/Logs/Logs.DatePickers'
|
||||
import { REPORTS_DATEPICKER_HELPERS } from './Reports.constants'
|
||||
@@ -59,7 +59,7 @@ const PRODUCT_FILTERS = [
|
||||
filterValue: '/realtime',
|
||||
label: 'Realtime',
|
||||
description: 'Realtime connection requests',
|
||||
icon: IconZapOff,
|
||||
icon: IconZap,
|
||||
},
|
||||
// TODO: support functions once union parsing is fixed
|
||||
// {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Standardized padding and width layout for non-custom reports
|
||||
*/
|
||||
const ReportPadding: React.FC = ({ children }) => (
|
||||
<div className="flex flex-col gap-4 px-5 py-6 h-full mx-auto 1xl:px-28 lg:px-16 xl:px-24 2xl:px-32">
|
||||
<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">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextRouter, useRouter } from 'next/router'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, IconExternalLink, IconHelpCircle } from 'ui'
|
||||
|
||||
import { BaseReportParams } from './Reports.types'
|
||||
import { LogsEndpointParams } from '../Settings/Logs'
|
||||
import Panel from 'components/ui/Panel'
|
||||
@@ -14,12 +13,15 @@ export interface ReportWidgetProps<T = any> {
|
||||
tooltip?: string
|
||||
className?: string
|
||||
renderer: (props: ReportWidgetRendererProps) => React.ReactNode
|
||||
append?: (props: ReportWidgetRendererProps) => React.ReactNode
|
||||
// for overriding props, such as data
|
||||
appendProps?: Partial<ReportWidgetRendererProps>
|
||||
// omitting params will hide the "View in logs explorer" button
|
||||
params?: BaseReportParams | LogsEndpointParams
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export interface ReportWidgetRendererProps extends ReportWidgetProps {
|
||||
export interface ReportWidgetRendererProps<T = any> extends ReportWidgetProps<T> {
|
||||
router: NextRouter
|
||||
projectRef: string
|
||||
}
|
||||
@@ -28,7 +30,6 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const projectRef = ref as string
|
||||
|
||||
return (
|
||||
<Panel
|
||||
noMargin
|
||||
@@ -104,6 +105,9 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
|
||||
<LoadingOpacity className="w-full" active={props.isLoading}>
|
||||
{props.data === undefined ? null : props.renderer({ ...props, router, projectRef })}
|
||||
</LoadingOpacity>
|
||||
|
||||
{props.append &&
|
||||
props.append({ ...props, ...(props.appendProps || {}), router, projectRef })}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
)
|
||||
|
||||
@@ -74,6 +74,29 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
ORDER BY
|
||||
timestamp ASC`,
|
||||
},
|
||||
topRoutes: {
|
||||
queryType: "logs",
|
||||
sql: (filters)=> `
|
||||
select
|
||||
request.path as path,
|
||||
request.method as method,
|
||||
request.search as search,
|
||||
response.status_code as status_code,
|
||||
count(t.id) as count
|
||||
from edge_logs t
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as response
|
||||
cross join unnest(m.request) as request
|
||||
cross join unnest(request.headers) as headers
|
||||
${generateRexepWhere(filters)}
|
||||
group by
|
||||
request.path, request.method, request.search, response.status_code
|
||||
order by
|
||||
count desc
|
||||
limit
|
||||
3
|
||||
`
|
||||
},
|
||||
errorCounts: {
|
||||
queryType: 'logs',
|
||||
sql: (filters) => `
|
||||
@@ -94,13 +117,37 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
timestamp ASC
|
||||
`,
|
||||
},
|
||||
topErrorRoutes: {
|
||||
queryType: "logs",
|
||||
sql: (filters)=> `
|
||||
select
|
||||
request.path as path,
|
||||
request.method as method,
|
||||
request.search as search,
|
||||
response.status_code as status_code,
|
||||
count(t.id) as count
|
||||
from edge_logs t
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as response
|
||||
cross join unnest(m.request) as request
|
||||
cross join unnest(request.headers) as headers
|
||||
where
|
||||
response.status_code >= 400
|
||||
${generateRexepWhere(filters, false)}
|
||||
group by
|
||||
request.path, request.method, request.search, response.status_code
|
||||
order by
|
||||
count desc
|
||||
limit
|
||||
3
|
||||
`
|
||||
},
|
||||
responseSpeed: {
|
||||
queryType: 'logs',
|
||||
sql: (filters) => `
|
||||
select
|
||||
cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
|
||||
avg(response.origin_time) as avg,
|
||||
APPROX_QUANTILES(response.origin_time, 100) as quantiles
|
||||
avg(response.origin_time) as avg
|
||||
FROM
|
||||
edge_logs t
|
||||
cross join unnest(metadata) as m
|
||||
@@ -114,6 +161,30 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
timestamp ASC
|
||||
`,
|
||||
},
|
||||
topSlowRoutes: {
|
||||
queryType: "logs",
|
||||
sql: (filters)=> `
|
||||
select
|
||||
request.path as path,
|
||||
request.method as method,
|
||||
request.search as search,
|
||||
response.status_code as status_code,
|
||||
count(t.id) as count,
|
||||
avg(response.origin_time) as avg
|
||||
from edge_logs t
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as response
|
||||
cross join unnest(m.request) as request
|
||||
cross join unnest(request.headers) as headers
|
||||
${generateRexepWhere(filters)}
|
||||
group by
|
||||
request.path, request.method, request.search, response.status_code
|
||||
order by
|
||||
avg desc
|
||||
limit
|
||||
3
|
||||
`
|
||||
},
|
||||
},
|
||||
},
|
||||
[Presets.AUTH]: {
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { ReportWidgetProps } from '../ReportWidget'
|
||||
import { ReportWidgetProps, ReportWidgetRendererProps } from '../ReportWidget'
|
||||
import BarChart from 'components/ui/Charts/BarChart'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import {
|
||||
jsonSyntaxHighlight,
|
||||
TextFormatter,
|
||||
} from 'components/interfaces/Settings/Logs/LogsFormatters'
|
||||
import { Button, Collapsible, IconChevronRight } from 'ui'
|
||||
import { queryParamsToObject } from '../Reports.utils'
|
||||
import { Fragment } from 'react'
|
||||
import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted'
|
||||
|
||||
export const renderTotalRequests = (
|
||||
export const TotalRequestsChartRenderer = (
|
||||
props: ReportWidgetProps<{
|
||||
timestamp: string
|
||||
count: number
|
||||
@@ -10,13 +19,21 @@ export const renderTotalRequests = (
|
||||
const total = props.data.reduce((acc, datum) => {
|
||||
return acc + datum.count
|
||||
}, 0)
|
||||
const data = useFillTimeseriesSorted(
|
||||
props.data,
|
||||
'timestamp',
|
||||
'count',
|
||||
0,
|
||||
props.params?.iso_timestamp_start,
|
||||
props.params?.iso_timestamp_end
|
||||
)
|
||||
return (
|
||||
<BarChart
|
||||
size="small"
|
||||
minimalHeader
|
||||
highlightedValue={total}
|
||||
className="w-full"
|
||||
data={props.data}
|
||||
data={data}
|
||||
yAxisKey="count"
|
||||
xAxisKey="timestamp"
|
||||
displayDateInUtc
|
||||
@@ -24,7 +41,58 @@ export const renderTotalRequests = (
|
||||
)
|
||||
}
|
||||
|
||||
export const renderErrorCounts = (
|
||||
export const TopApiRoutesRenderer = (
|
||||
props: ReportWidgetRendererProps<{
|
||||
method: string
|
||||
// shown for error table but not all requests table
|
||||
status_code?: number
|
||||
path: string
|
||||
search: string
|
||||
count: number
|
||||
// used for response speed table only
|
||||
avg?: number
|
||||
}>
|
||||
) => {
|
||||
if (props.data.length === 0) return null
|
||||
const headerClasses = '!text-xs !py-2 p-0 font-bold !bg-scale-400'
|
||||
const cellClasses = '!text-xs !py-2'
|
||||
return (
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th className={headerClasses}>Request</Table.th>
|
||||
<Table.th className={headerClasses + ' text-right'}>Count</Table.th>
|
||||
{props.data[0].avg !== undefined && (
|
||||
<Table.th className={headerClasses + ' text-right'}>Avg</Table.th>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
{props.data.map((datum) => (
|
||||
<Fragment key={datum.path + (datum.search || '')}>
|
||||
<Table.tr className="p-0">
|
||||
<Table.td className={[cellClasses].join(' ')}>
|
||||
<RouteTdContent {...datum} />
|
||||
</Table.td>
|
||||
<Table.td className={[cellClasses, 'text-right align-top'].join(' ')}>
|
||||
{datum.count}
|
||||
</Table.td>
|
||||
{props.data[0].avg !== undefined && (
|
||||
<Table.td className={[cellClasses, 'text-right align-top'].join(' ')}>
|
||||
{Number(datum.avg).toFixed(2)}ms
|
||||
</Table.td>
|
||||
)}
|
||||
</Table.tr>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ErrorCountsChartRenderer = (
|
||||
props: ReportWidgetProps<{
|
||||
timestamp: string
|
||||
count: number
|
||||
@@ -33,13 +101,23 @@ export const renderErrorCounts = (
|
||||
const total = props.data.reduce((acc, datum) => {
|
||||
return acc + datum.count
|
||||
}, 0)
|
||||
|
||||
const data = useFillTimeseriesSorted(
|
||||
props.data,
|
||||
'timestamp',
|
||||
'count',
|
||||
0,
|
||||
props.params?.iso_timestamp_start,
|
||||
props.params?.iso_timestamp_end
|
||||
)
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
size="small"
|
||||
minimalHeader
|
||||
className="w-full"
|
||||
highlightedValue={total}
|
||||
data={props.data}
|
||||
data={data}
|
||||
yAxisKey="count"
|
||||
xAxisKey="timestamp"
|
||||
displayDateInUtc
|
||||
@@ -47,18 +125,26 @@ export const renderErrorCounts = (
|
||||
)
|
||||
}
|
||||
|
||||
export const renderResponseSpeed = (
|
||||
export const ResponseSpeedChartRenderer = (
|
||||
props: ReportWidgetProps<{
|
||||
timestamp: string
|
||||
avg: number
|
||||
quantiles: number[]
|
||||
}>
|
||||
) => {
|
||||
const transformedData = props.data.map((datum) => ({
|
||||
timestamp: datum.timestamp,
|
||||
avg: datum.avg,
|
||||
median: datum.quantiles[49],
|
||||
}))
|
||||
|
||||
const data = useFillTimeseriesSorted(
|
||||
transformedData,
|
||||
'timestamp',
|
||||
'avg',
|
||||
0,
|
||||
props.params?.iso_timestamp_start,
|
||||
props.params?.iso_timestamp_end
|
||||
)
|
||||
|
||||
const lastAvg = props.data[props.data.length - 1]?.avg
|
||||
return (
|
||||
<BarChart
|
||||
@@ -67,10 +153,65 @@ export const renderResponseSpeed = (
|
||||
format="ms"
|
||||
minimalHeader
|
||||
className="w-full"
|
||||
data={transformedData}
|
||||
data={data}
|
||||
yAxisKey="avg"
|
||||
xAxisKey="timestamp"
|
||||
displayDateInUtc
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface RouteTdContentProps {
|
||||
method: string
|
||||
status_code?: number
|
||||
path: string
|
||||
search: string
|
||||
}
|
||||
const RouteTdContent = (datum: RouteTdContentProps) => (
|
||||
<Collapsible>
|
||||
<Collapsible.Trigger asChild>
|
||||
<div className="flex gap-2">
|
||||
<Button as="span" type="text" className=" !py-0 !p-1" title="Show more route details">
|
||||
<IconChevronRight
|
||||
size={14}
|
||||
className="transition data-open-parent:rotate-90 data-closed-parent:rotate-0"
|
||||
/>
|
||||
</Button>
|
||||
<TextFormatter className="w-10 h-4 text-center rounded bg-scale-500" value={datum.method} />
|
||||
{datum.status_code && (
|
||||
<TextFormatter
|
||||
className={`w-10 h-4 text-center rounded ${
|
||||
datum.status_code >= 400
|
||||
? 'bg-orange-500'
|
||||
: datum.status_code >= 300
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-green-500'
|
||||
}`}
|
||||
value={String(datum.status_code)}
|
||||
/>
|
||||
)}
|
||||
<div className=" truncate max-w-sm lg:max-w-lg">
|
||||
<TextFormatter className="text-scale-1100" value={datum.path} />
|
||||
<TextFormatter
|
||||
className="max-w-sm text-scale-900 truncate "
|
||||
value={decodeURIComponent(datum.search || '')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content className="pt-2">
|
||||
{datum.search ? (
|
||||
<pre className={`syntax-highlight overflow-auto rounded bg-scale-300 p-2 !text-xs`}>
|
||||
<div
|
||||
className="text-wrap"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: jsonSyntaxHighlight(queryParamsToObject(datum.search)),
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
) : (
|
||||
<p className="text-xs text-scale-900">No query parameters in this request</p>
|
||||
)}
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
)
|
||||
@@ -43,12 +43,12 @@ const BarChart: React.FC<BarChartProps> = ({
|
||||
const resolvedHighlightedLabel =
|
||||
(focusDataIndex !== null &&
|
||||
data &&
|
||||
data[focusDataIndex] &&
|
||||
data[focusDataIndex] !== undefined &&
|
||||
day(data[focusDataIndex][xAxisKey]).format(customDateFormat)) ||
|
||||
highlightedLabel
|
||||
|
||||
const resolvedHighlightedValue =
|
||||
(focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : null) || highlightedValue
|
||||
(focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue)
|
||||
|
||||
return (
|
||||
<div className={['flex flex-col gap-3', className].join(' ')}>
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
||||
import { queriesFactory } from 'components/interfaces/Reports/Reports.utils'
|
||||
import {
|
||||
renderTotalRequests,
|
||||
renderErrorCounts,
|
||||
renderResponseSpeed,
|
||||
TotalRequestsChartRenderer,
|
||||
ErrorCountsChartRenderer,
|
||||
ResponseSpeedChartRenderer,
|
||||
TopApiRoutesRenderer,
|
||||
} from 'components/interfaces/Reports/renderers/ApiRenderers'
|
||||
import { useState, useEffect } from 'react'
|
||||
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
|
||||
@@ -63,7 +64,9 @@ export const ApiReport: NextPageWithLayout = () => {
|
||||
params={report.params.totalRequests}
|
||||
title="Total Requests"
|
||||
data={report.data.totalRequests || []}
|
||||
renderer={renderTotalRequests}
|
||||
renderer={TotalRequestsChartRenderer}
|
||||
append={TopApiRoutesRenderer}
|
||||
appendProps={{ data: report.data.topRoutes || [] }}
|
||||
/>
|
||||
<ReportWidget
|
||||
isLoading={report.isLoading}
|
||||
@@ -71,7 +74,9 @@ export const ApiReport: NextPageWithLayout = () => {
|
||||
title="Response Errors"
|
||||
tooltip="Error responses with 4XX or 5XX status codes"
|
||||
data={report.data.errorCounts || []}
|
||||
renderer={renderErrorCounts}
|
||||
renderer={ErrorCountsChartRenderer}
|
||||
appendProps={{ data: report.data.topErrorRoutes || [] }}
|
||||
append={TopApiRoutesRenderer}
|
||||
/>
|
||||
<ReportWidget
|
||||
isLoading={report.isLoading}
|
||||
@@ -79,7 +84,9 @@ export const ApiReport: NextPageWithLayout = () => {
|
||||
title="Response Speed"
|
||||
tooltip="Average response speed (in miliseconds) of a request"
|
||||
data={report.data.responseSpeed || []}
|
||||
renderer={renderResponseSpeed}
|
||||
renderer={ResponseSpeedChartRenderer}
|
||||
appendProps={{ data: report.data.topSlowRoutes || [] }}
|
||||
append={TopApiRoutesRenderer}
|
||||
/>
|
||||
</ReportPadding>
|
||||
)
|
||||
@@ -94,9 +101,19 @@ const useApiReport = () => {
|
||||
projectRef ?? 'default'
|
||||
)
|
||||
const totalRequests = queryHooks.totalRequests()
|
||||
const topRoutes = queryHooks.topRoutes()
|
||||
const errorCounts = queryHooks.errorCounts()
|
||||
const topErrorRoutes = queryHooks.topErrorRoutes()
|
||||
const responseSpeed = queryHooks.responseSpeed()
|
||||
const activeHooks = [totalRequests, errorCounts, responseSpeed]
|
||||
const topSlowRoutes = queryHooks.topSlowRoutes()
|
||||
const activeHooks = [
|
||||
totalRequests,
|
||||
topRoutes,
|
||||
errorCounts,
|
||||
topErrorRoutes,
|
||||
responseSpeed,
|
||||
topSlowRoutes,
|
||||
]
|
||||
const [filters, setFilters] = useState<ReportFilterItem[]>([])
|
||||
const addFilter = (filter: ReportFilterItem) => {
|
||||
// use a deep equal when comparing objects.
|
||||
@@ -127,12 +144,23 @@ const useApiReport = () => {
|
||||
if (totalRequests[1].changeQuery) {
|
||||
totalRequests[1].changeQuery(PRESET_CONFIG.api.queries.totalRequests.sql(filters))
|
||||
}
|
||||
if (topRoutes[1].changeQuery) {
|
||||
topRoutes[1].changeQuery(PRESET_CONFIG.api.queries.topRoutes.sql(filters))
|
||||
}
|
||||
if (errorCounts[1].changeQuery) {
|
||||
errorCounts[1].changeQuery(PRESET_CONFIG.api.queries.errorCounts.sql(filters))
|
||||
}
|
||||
|
||||
if (topErrorRoutes[1].changeQuery) {
|
||||
topErrorRoutes[1].changeQuery(PRESET_CONFIG.api.queries.topErrorRoutes.sql(filters))
|
||||
}
|
||||
if (responseSpeed[1].changeQuery) {
|
||||
responseSpeed[1].changeQuery(PRESET_CONFIG.api.queries.responseSpeed.sql(filters))
|
||||
}
|
||||
|
||||
if (topSlowRoutes[1].changeQuery) {
|
||||
topSlowRoutes[1].changeQuery(PRESET_CONFIG.api.queries.topSlowRoutes.sql(filters))
|
||||
}
|
||||
}, [JSON.stringify(filters)])
|
||||
|
||||
const handleRefresh = async () => {
|
||||
@@ -151,11 +179,17 @@ const useApiReport = () => {
|
||||
totalRequests: totalRequests[0].logData,
|
||||
errorCounts: errorCounts[0].logData,
|
||||
responseSpeed: responseSpeed[0].logData,
|
||||
topRoutes: topRoutes[0].logData,
|
||||
topErrorRoutes: topErrorRoutes[0].logData,
|
||||
topSlowRoutes: topSlowRoutes[0].logData,
|
||||
},
|
||||
params: {
|
||||
totalRequests: totalRequests[0].params,
|
||||
errorCounts: errorCounts[0].params,
|
||||
responseSpeed: responseSpeed[0].params,
|
||||
topRoutes: topRoutes[0].params,
|
||||
topErrorRoutes: topErrorRoutes[0].params,
|
||||
topSlowRoutes: topSlowRoutes[0].params,
|
||||
},
|
||||
mergeParams: handleSetParams,
|
||||
filters,
|
||||
|
||||
@@ -1,46 +1,12 @@
|
||||
// mock the fetch function
|
||||
jest.mock('lib/common/fetch')
|
||||
import { get } from 'lib/common/fetch'
|
||||
|
||||
// mock mobx
|
||||
jest.mock('mobx-react-lite')
|
||||
import { observer } from 'mobx-react-lite'
|
||||
observer.mockImplementation((v) => v)
|
||||
|
||||
// mock the router
|
||||
jest.mock('next/router')
|
||||
import { useRouter } from 'next/router'
|
||||
const defaultRouterMock = () => {
|
||||
const router = jest.fn()
|
||||
router.query = {}
|
||||
router.push = jest.fn()
|
||||
router.pathname = 'logs/path'
|
||||
return router
|
||||
}
|
||||
useRouter.mockReturnValue(defaultRouterMock())
|
||||
|
||||
// mock usage flags
|
||||
jest.mock('components/ui/Flag/Flag')
|
||||
import Flag from 'components/ui/Flag/Flag'
|
||||
Flag.mockImplementation(({ children }) => <>{children}</>)
|
||||
jest.mock('hooks')
|
||||
import { useFlag } from 'hooks'
|
||||
useFlag.mockReturnValue(true)
|
||||
|
||||
import { fireEvent, waitFor, screen, act } from '@testing-library/react'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { render } from '../../helpers'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { wait } from '@testing-library/user-event/dist/utils'
|
||||
import { logDataFixture } from '../../fixtures'
|
||||
import { LogsTableName } from 'components/interfaces/Settings/Logs'
|
||||
import { Presets } from 'components/interfaces/Reports/Reports.types'
|
||||
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
||||
import { clickDropdown } from 'tests/helpers'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
beforeEach(() => {
|
||||
// reset mocks between tests
|
||||
get.mockReset()
|
||||
useRouter.mockReset()
|
||||
useRouter.mockReturnValue(defaultRouterMock())
|
||||
})
|
||||
|
||||
test('static elements', async () => {
|
||||
@@ -48,3 +14,9 @@ test('static elements', async () => {
|
||||
await screen.findByText(/Some chart/)
|
||||
await screen.findByText(/something/)
|
||||
})
|
||||
|
||||
test("append", async ()=>{
|
||||
const appendable = ()=> "some text"
|
||||
render(<ReportWidget data={[]} renderer={() => null} append={appendable} />)
|
||||
await screen.findByText(/some text/)
|
||||
})
|
||||
@@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event'
|
||||
beforeEach(() => {
|
||||
// reset mocks between tests
|
||||
get.mockReset()
|
||||
get.mockImplementation(async (_url) => [{ data: [] }])
|
||||
get.mockImplementation(async (_url) => [{ result: [] }])
|
||||
})
|
||||
|
||||
test(`static elements`, async () => {
|
||||
@@ -26,3 +26,86 @@ test('refresh button', async () => {
|
||||
userEvent.click(await screen.findByText(/Refresh/))
|
||||
await waitFor(() => expect(get).toBeCalled())
|
||||
})
|
||||
|
||||
test('append - api request routes', async () => {
|
||||
get.mockImplementation(async (url) => {
|
||||
if (decodeURIComponent(url).includes("request.path")) {
|
||||
return { result: [{ path: 'mypath', method: 'GET', status_code: 200, search: 'some-query', count: 22 }] }
|
||||
}
|
||||
return { result: [{ timestamp: new Date().toISOString(), count: 123 }] }
|
||||
})
|
||||
render(<ApiReport />)
|
||||
await waitFor(() => expect(get).toBeCalled())
|
||||
await screen.findAllByText(/mypath/)
|
||||
await screen.findAllByText(/GET/)
|
||||
await screen.findAllByText(/200/)
|
||||
await screen.findAllByText(/some\-query/)
|
||||
await screen.findAllByText(/22/)
|
||||
})
|
||||
|
||||
|
||||
test('append - error routes', async () => {
|
||||
get.mockImplementation(async (url) => {
|
||||
const uri = decodeURIComponent(url)
|
||||
if (uri.includes("400")) {
|
||||
return { result: [{ path: 'mypath', method: 'GET', status_code: 200, search: 'some-query', count: 22 }] }
|
||||
}
|
||||
return { result: [{ timestamp: new Date().toISOString(), count: 123 }] }
|
||||
})
|
||||
render(<ApiReport />)
|
||||
await waitFor(() => expect(get).toBeCalled())
|
||||
await screen.findAllByText(/mypath/)
|
||||
await screen.findAllByText(/GET/)
|
||||
await screen.findAllByText(/200/)
|
||||
await screen.findAllByText(/some\-query/)
|
||||
await screen.findAllByText(/22/)
|
||||
})
|
||||
|
||||
|
||||
test('append - error routes', async () => {
|
||||
get.mockImplementation(async (url) => {
|
||||
const uri = decodeURIComponent(url)
|
||||
if (uri.includes("avg") && uri.includes("request.path")) {
|
||||
return { result: [{ path: 'mypath', method: 'GET', status_code: 200, avg: 534, search: 'some-query', count: 22 }] }
|
||||
}
|
||||
return { result: [{ timestamp: new Date().toISOString(), count: 123 }] }
|
||||
})
|
||||
render(<ApiReport />)
|
||||
await waitFor(() => expect(get).toBeCalled())
|
||||
await screen.findAllByText(/mypath/)
|
||||
await screen.findAllByText(/GET/)
|
||||
await screen.findAllByText(/200/)
|
||||
await screen.findAllByText(/some\-query/)
|
||||
await screen.findAllByText(/22/)
|
||||
await screen.findAllByText(/534\.00ms/)
|
||||
})
|
||||
|
||||
|
||||
|
||||
// test('expandable error routes', async () => {
|
||||
// get.mockImplementation(async (url) => {
|
||||
// if (url.includes('path')) {
|
||||
// return [{ data: [{ path: '/my-path', method: 'GET', search: '?=123', count: 22 }] }]
|
||||
// }
|
||||
// return [{ data: [{ timestamp: new Date().toISOString(), count: 123 }] }]
|
||||
// })
|
||||
|
||||
// render(<ApiReport />)
|
||||
// await screen.findByText(/\/my\-path/)
|
||||
// await screen.findByText(/\?\=123/)
|
||||
// await screen.findByText(/22/)
|
||||
// })
|
||||
|
||||
// test('expandable high latency routes', async () => {
|
||||
// get.mockImplementation(async (url) => {
|
||||
// if (url.includes('path')) {
|
||||
// return [{ data: [{ path: '/my-path', method: 'GET', search: '?=123', avg_ms: 55, count: 22 }] }]
|
||||
// }
|
||||
// return [{ data: [{ timestamp: new Date().toISOString(), avg_ms: 123 }] }]
|
||||
// })
|
||||
// render(<ApiReport />)
|
||||
// await screen.findByText(/\/my\-path/)
|
||||
// await screen.findByText(/\?\=123/)
|
||||
// await screen.findByText(/22/)
|
||||
// await screen.findByText(/55/)
|
||||
// })
|
||||
Reference in new issue
Block a user