mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 12:25:05 +03:00
437 lines
16 KiB
TypeScript
437 lines
16 KiB
TypeScript
'use client'
|
|
|
|
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { IS_PLATFORM, useParams } from 'common'
|
|
import { EdgeFunctionRecentInvocations } from 'components/interfaces/Functions/EdgeFunctionRecentInvocations'
|
|
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
|
import AreaChart from 'components/ui/Charts/AreaChart'
|
|
import StackedBarChart from 'components/ui/Charts/StackedBarChart'
|
|
import NoPermission from 'components/ui/NoPermission'
|
|
import {
|
|
FunctionsCombinedStatsVariables,
|
|
useFunctionsCombinedStatsQuery,
|
|
} from 'data/analytics/functions-combined-stats-query'
|
|
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
|
|
import dayjs, { Dayjs } from 'dayjs'
|
|
import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted'
|
|
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
|
import maxBy from 'lodash/maxBy'
|
|
import meanBy from 'lodash/meanBy'
|
|
import sumBy from 'lodash/sumBy'
|
|
import { useRouter } from 'next/compat/router'
|
|
import { useMemo, useState } from 'react'
|
|
import type { ChartIntervals } from 'types'
|
|
import {
|
|
Alert_Shadcn_,
|
|
AlertDescription_Shadcn_,
|
|
AlertTitle_Shadcn_,
|
|
Button,
|
|
WarningIcon,
|
|
} from 'ui'
|
|
import { PageContainer } from 'ui-patterns/PageContainer'
|
|
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
|
|
|
|
const CHART_INTERVALS: ChartIntervals[] = [
|
|
{
|
|
key: '15min',
|
|
label: '15 min',
|
|
startValue: 15,
|
|
startUnit: 'minute',
|
|
format: 'MMM D, h:mm:ssa',
|
|
},
|
|
{
|
|
key: '1hr',
|
|
label: '1 hour',
|
|
startValue: 1,
|
|
startUnit: 'hour',
|
|
format: 'MMM D, h:mma',
|
|
},
|
|
{
|
|
key: '3hr',
|
|
label: '3 hours',
|
|
startValue: 3,
|
|
startUnit: 'hour',
|
|
format: 'MMM D, h:mma',
|
|
},
|
|
{
|
|
key: '1day',
|
|
label: '1 day',
|
|
startValue: 1,
|
|
startUnit: 'hour',
|
|
format: 'MMM D, h:mma',
|
|
},
|
|
]
|
|
|
|
/** Shared body of `pages/project/[ref]/functions/[functionSlug]/index.tsx` (Overview). */
|
|
export function EdgeFunctionOverviewPageContent() {
|
|
const router = useRouter()
|
|
const { ref: projectRef, functionSlug } = useParams()
|
|
|
|
const [interval, setInterval] = useState<string>('15min')
|
|
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
|
|
const { data: selectedFunction } = useEdgeFunctionQuery({
|
|
projectRef,
|
|
slug: functionSlug,
|
|
})
|
|
const id = selectedFunction?.id
|
|
const combinedStatsResults = useFunctionsCombinedStatsQuery({
|
|
projectRef,
|
|
functionId: id,
|
|
interval: selectedInterval.key as FunctionsCombinedStatsVariables['interval'],
|
|
})
|
|
|
|
const combinedStatsData = useMemo(() => {
|
|
const result = combinedStatsResults.data?.result as
|
|
| Record<string, string | number>[]
|
|
| undefined
|
|
return result || []
|
|
}, [combinedStatsResults.data])
|
|
|
|
const [startDate, endDate]: [Dayjs, Dayjs] = useMemo(() => {
|
|
const start = dayjs()
|
|
.subtract(selectedInterval.startValue, selectedInterval.startUnit as dayjs.ManipulateType)
|
|
.startOf(selectedInterval.startUnit as dayjs.ManipulateType)
|
|
|
|
const end = dayjs().startOf(selectedInterval.startUnit as dayjs.ManipulateType)
|
|
return [start, end]
|
|
}, [selectedInterval])
|
|
|
|
const {
|
|
data: combinedStatsChartData,
|
|
error: combinedStatsError,
|
|
isError: isErrorCombinedStats,
|
|
} = useFillTimeseriesSorted({
|
|
data: combinedStatsData,
|
|
timestampKey: 'timestamp',
|
|
valueKey: [
|
|
'requests_count',
|
|
'log_count',
|
|
'log_info_count',
|
|
'log_warn_count',
|
|
'log_error_count',
|
|
'success_count',
|
|
'redirect_count',
|
|
'client_err_count',
|
|
'server_err_count',
|
|
'avg_cpu_time_used',
|
|
'avg_memory_used',
|
|
'avg_execution_time',
|
|
'max_execution_time',
|
|
'avg_heap_memory_used',
|
|
'avg_external_memory_used',
|
|
'max_cpu_time_used',
|
|
],
|
|
defaultValue: 0,
|
|
startDate: startDate.toISOString(),
|
|
endDate: endDate.toISOString(),
|
|
})
|
|
|
|
const { isLoading: permissionsLoading, can: canReadFunction } = useAsyncCheckPermissions(
|
|
PermissionAction.FUNCTIONS_READ,
|
|
functionSlug as string
|
|
)
|
|
if (!canReadFunction && !permissionsLoading) {
|
|
return <NoPermission isFullPage resourceText="access this edge function" />
|
|
}
|
|
|
|
const push = (path: string) => {
|
|
if (router) router.push(path)
|
|
else if (typeof window !== 'undefined') window.location.assign(path)
|
|
}
|
|
|
|
return (
|
|
<PageContainer size="full">
|
|
<PageSection>
|
|
<PageSectionContent>
|
|
{IS_PLATFORM && id && (
|
|
<div className="mb-8">
|
|
<EdgeFunctionRecentInvocations
|
|
functionId={id}
|
|
functionSlug={functionSlug as string}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="flex flex-row items-center gap-2 mb-4">
|
|
<div className="flex items-center">
|
|
{CHART_INTERVALS.map((item, i) => {
|
|
const classes = []
|
|
|
|
if (i === 0) {
|
|
classes.push('rounded-tr-none rounded-br-none')
|
|
} else if (i === CHART_INTERVALS.length - 1) {
|
|
classes.push('rounded-tl-none rounded-bl-none')
|
|
} else {
|
|
classes.push('rounded-none')
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
key={`function-filter-${i}`}
|
|
type={interval === item.key ? 'secondary' : 'default'}
|
|
onClick={() => setInterval(item.key)}
|
|
className={classes.join(' ')}
|
|
>
|
|
{item.label}
|
|
</Button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<span className="text-xs text-foreground-light">
|
|
Statistics for past {selectedInterval.label}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-2 lg:gap-8">
|
|
<ReportWidget
|
|
title="Execution time"
|
|
tooltip="Average execution time of function invocations"
|
|
data={combinedStatsChartData}
|
|
isLoading={combinedStatsResults.isLoading}
|
|
renderer={(props) => {
|
|
return isErrorCombinedStats ? (
|
|
<Alert_Shadcn_ variant="warning">
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>Failed to reterieve execution time</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
{combinedStatsError?.message ?? 'Unknown error'}
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
) : (
|
|
<div className="space-y-8">
|
|
<AreaChart
|
|
title="Average execution time"
|
|
className="w-full"
|
|
xAxisKey="timestamp"
|
|
customDateFormat={selectedInterval.format}
|
|
yAxisKey="avg_execution_time"
|
|
data={props.data}
|
|
format="ms"
|
|
highlightedValue={meanBy(props.data, 'avg_execution_time')}
|
|
/>
|
|
<AreaChart
|
|
title="Max execution time"
|
|
className="w-full"
|
|
xAxisKey="timestamp"
|
|
customDateFormat={selectedInterval.format}
|
|
yAxisKey="max_execution_time"
|
|
data={props.data}
|
|
format="ms"
|
|
highlightedValue={
|
|
maxBy(props.data, 'max_execution_time')?.max_execution_time
|
|
}
|
|
/>
|
|
</div>
|
|
)
|
|
}}
|
|
/>
|
|
<ReportWidget
|
|
title="Invocations"
|
|
tooltip="Requests made to a function are considered invocations, and each invocation may have worker logs."
|
|
data={combinedStatsChartData}
|
|
isLoading={combinedStatsResults.isLoading}
|
|
renderer={(props) => {
|
|
if (isErrorCombinedStats) {
|
|
return (
|
|
<Alert_Shadcn_ variant="warning">
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>Failed to reterieve invocations</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
{combinedStatsError?.message ?? 'Unknown error'}
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
)
|
|
} else {
|
|
const requestData = props.data
|
|
.map((d: any) => [
|
|
{
|
|
status: '2xx',
|
|
count: d.success_count,
|
|
timestamp: d.timestamp,
|
|
},
|
|
{
|
|
status: '3xx',
|
|
count: d.redirect_count,
|
|
timestamp: d.timestamp,
|
|
},
|
|
{
|
|
status: '4xx',
|
|
count: d.client_err_count,
|
|
timestamp: d.timestamp,
|
|
},
|
|
{
|
|
status: '5xx',
|
|
count: d.server_err_count,
|
|
timestamp: d.timestamp,
|
|
},
|
|
])
|
|
.flat()
|
|
|
|
const logsData = props.data
|
|
.map((d: any) => [
|
|
{
|
|
status: 'error',
|
|
count: d.log_error_count,
|
|
timestamp: d.timestamp,
|
|
},
|
|
{
|
|
status: 'info',
|
|
count: d.log_info_count,
|
|
timestamp: d.timestamp,
|
|
},
|
|
{
|
|
status: 'warn',
|
|
count: d.log_warn_count,
|
|
timestamp: d.timestamp,
|
|
},
|
|
])
|
|
.flat()
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<StackedBarChart
|
|
title="Invocation Requests"
|
|
className="w-full"
|
|
xAxisKey="timestamp"
|
|
yAxisKey="count"
|
|
stackKey="status"
|
|
data={requestData}
|
|
highlightedValue={sumBy(requestData, 'count')}
|
|
customDateFormat={selectedInterval.format}
|
|
stackColors={['brand', 'slate', 'yellow', 'red']}
|
|
onBarClick={() => {
|
|
push(
|
|
`/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}`
|
|
)
|
|
}}
|
|
/>
|
|
<StackedBarChart
|
|
title="Worker Logs"
|
|
className="w-full"
|
|
xAxisKey="timestamp"
|
|
yAxisKey="count"
|
|
stackKey="status"
|
|
data={logsData}
|
|
highlightedValue={sumBy(logsData, 'count')}
|
|
customDateFormat={selectedInterval.format}
|
|
stackColors={['red', 'brand', 'yellow']}
|
|
onBarClick={() => {
|
|
push(
|
|
`/project/${projectRef}/functions/${functionSlug}/logs?its=${startDate.toISOString()}`
|
|
)
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
}}
|
|
/>
|
|
<ReportWidget
|
|
title="CPU time"
|
|
tooltip="Average CPU time usage for the function"
|
|
data={combinedStatsChartData}
|
|
isLoading={combinedStatsResults.isLoading}
|
|
renderer={(props) => {
|
|
return isErrorCombinedStats ? (
|
|
<Alert_Shadcn_ variant="warning">
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>Failed to retrieve CPU time</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
{combinedStatsError?.message ?? 'Unknown error'}
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
) : (
|
|
<div className="space-y-8">
|
|
<AreaChart
|
|
title="Average CPU Time"
|
|
className="w-full"
|
|
xAxisKey="timestamp"
|
|
customDateFormat={selectedInterval.format}
|
|
yAxisKey="avg_cpu_time_used"
|
|
data={props.data}
|
|
format="ms"
|
|
highlightedValue={meanBy(props.data, 'avg_cpu_time_used')}
|
|
/>
|
|
<AreaChart
|
|
title="Max CPU Time"
|
|
className="w-full"
|
|
xAxisKey="timestamp"
|
|
customDateFormat={selectedInterval.format}
|
|
yAxisKey="max_cpu_time_used"
|
|
data={props.data}
|
|
format="ms"
|
|
highlightedValue={maxBy(props.data, 'max_cpu_time_used')?.max_cpu_time_used}
|
|
/>
|
|
</div>
|
|
)
|
|
}}
|
|
/>
|
|
<ReportWidget
|
|
title="Memory"
|
|
tooltip="Average memory usage for the function"
|
|
data={combinedStatsChartData}
|
|
isLoading={combinedStatsResults.isLoading}
|
|
renderer={(props) => {
|
|
if (isErrorCombinedStats) {
|
|
return (
|
|
<Alert_Shadcn_ variant="warning">
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>Failed to retrieve memory usage</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
{combinedStatsError?.message ?? 'Unknown error'}
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
)
|
|
}
|
|
|
|
const memoryData = props.data
|
|
.map((d: any) => [
|
|
{
|
|
type: 'heap',
|
|
count: d.avg_heap_memory_used,
|
|
timestamp: d.timestamp,
|
|
},
|
|
{
|
|
type: 'external',
|
|
count: d.avg_external_memory_used,
|
|
timestamp: d.timestamp,
|
|
},
|
|
])
|
|
.flat()
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<AreaChart
|
|
title="Average Memory Usage"
|
|
className="w-full"
|
|
xAxisKey="timestamp"
|
|
customDateFormat={selectedInterval.format}
|
|
yAxisKey="avg_memory_used"
|
|
data={props.data}
|
|
format="MB"
|
|
highlightedValue={meanBy(props.data, 'avg_memory_used')}
|
|
/>
|
|
<StackedBarChart
|
|
title="Average Memory Usage by Type"
|
|
className="w-full"
|
|
xAxisKey="timestamp"
|
|
yAxisKey="count"
|
|
stackKey="type"
|
|
format="MB"
|
|
data={memoryData}
|
|
highlightedValue={sumBy(memoryData, 'count')}
|
|
customDateFormat={selectedInterval.format}
|
|
stackColors={['blue', 'brand']}
|
|
/>
|
|
</div>
|
|
)
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</PageSectionContent>
|
|
</PageSection>
|
|
</PageContainer>
|
|
)
|
|
}
|