mirror of
https://github.com/supabase/supabase.git
synced 2026-06-10 04:26:19 +08:00
Closes [FE-3245](https://linear.app/supabase/issue/FE-3245/add-keyboard-shortcuts-to-edge-functions-pages). Adds keyboard shortcuts across the Edge Functions surface, mirroring the patterns already in place for Database / Auth / Storage. ## Summary Three layers of new shortcuts, plus one quality-of-life fix on the existing search input: ### 1. Edge Functions list page (`/project/:ref/functions`) | Key | Action | |---|---| | `Shift+F` | Focus the search input | | `Shift+N` | Route to `/functions/new` (deploy a new function) | | `F` then `C` | Clear search filter | | `Shift+R` | Refresh the functions list (new toolbar button) | | `S` then `C` | Reset sort to `name:asc` | | `Esc` (in search) | Clears value, then blurs on a second press (`onSearchInputEscape`) | ### 2. Edge Functions section nav (active anywhere under `/functions/*`) | Key | Action | |---|---| | `F` then `O` | Functions overview | | `F` then `K` | Secrets | Wired through `EdgeFunctionsProductMenu` items via `shortcutId`, registered by `<ProductMenuShortcuts />` mounted in `EdgeFunctionsLayout`. ### 3. Per-function detail (active anywhere under `/functions/:slug/*`) | Key | Action | |---|---| | `1` | Overview | | `2` | Invocations | | `3` | Logs | | `4` | Code | | `5` | Settings | | `Shift+T` | Open the Test sheet | | `Shift+D` | Toggle the Download popover | | `Shift+C` | Copy the function URL (with toast) | ### 4. Test sheet (active when `EdgeFunctionTesterSheet` is open) | Key | Action | |---|---| | `Mod+Enter` | Send Request — first binding for this; mirrors `SQL_EDITOR_RUN` semantics | ### 5. New per-function Overview (`edgeFunctionsOverview` flag) | Key | Action | |---|---| | `I` then `M` | 15 min | | `I` then `H` | 1 hour | | `I` then `T` | 3 hours | | `I` then `D` | 1 day | | `Shift+R` | Refresh combined stats query | | `O` then `L` | Open Logs (or Invocations if unified-logs preview is off) | `ShortcutTooltip` added to the most prominent buttons (search, refresh, copy URL, download, test, send request). Interval/refresh/open-logs on the overview are registered without inline tooltips but remain discoverable via `Cmd+K` and the shortcut reference sheet (`Mod+/`). ## Implementation notes - New reference group `NAVIGATION_FUNCTION_DETAIL` ("Function Page Navigation") added to keep the reference sheet grouped sensibly. - Three new registry files: `functions-list.ts`, `functions-nav.ts`, `functions-detail.ts`, `functions-detail-nav.ts`, `functions-overview.ts`. - Three new hooks: `useFunctionsListShortcuts`, `useFunctionsDetailShortcuts`, `useEdgeFunctionOverviewShortcuts`. - `EdgeFunctionsLayout` refactored to share a single `useGenerateEdgeFunctionsMenu` hook between `<ProductMenu>` and `<ProductMenuShortcuts>` (matches the AuthLayout / DatabaseLayout pattern). - Download popover hoisted to controlled state so `Shift+D` can toggle it. ## Test plan ### Functions list page - [x] On `/project/:ref/functions`, press `Shift+F` — search input gains focus and value is selected - [x] Type in the search → press `Esc` → value clears (focus retained). Press `Esc` again → blurs - [x] Press `Shift+N` → routes to `/functions/new` - [x] With a non-default sort, press `S` then `C` → sort resets to `name:asc`. Confirm shortcut is disabled when already at default - [x] Press `Shift+R` → list refetches; loading indicator appears on the new Refresh button - [x] Press `F` then `C` → search clears ### Section nav (anywhere under `/functions/*`) - [x] From any page under `/functions/*`, press `F` then `O` → navigates to Functions list - [x] Press `F` then `K` → navigates to Secrets - [x] Verify the chord doesn't fire while typing in an input ### Per-function detail (any sub-page) - [x] On any function detail tab, press `1`/`2`/`3`/`4`/`5` → navigates to Overview / Invocations / Logs / Code / Settings respectively (digits 2 and 3 only on platform builds) - [x] Press `Shift+T` → Test sheet opens. Press escape to close - [x] Press `Shift+D` → Download popover opens; press escape to close - [x] Press `Shift+C` → URL copied + toast appears - [x] Hover the URL copy button, Download button, Test button — `ShortcutTooltip` shows the chord ### Test sheet - [x] Open the Test sheet (button or `Shift+T`) - [x] Without focusing anything, press `Mod+Enter` → request fires - [x] With focus inside the body editor / a header input, press `Mod+Enter` → request still fires (`Mod+`-keys bypass input guard) - [x] While `isPending`, `Mod+Enter` is a no-op (shortcut disabled) - [x] Hover Send Request → tooltip shows `Mod+Enter` ### New overview (with `edgeFunctionsOverview` flag enabled) - [x] Press `I` then `M` / `H` / `T` / `D` → interval segmented buttons highlight accordingly and chart re-fetches - [x] Press `Shift+R` → stats refetch - [x] Press `O` then `L` → routes to logs (or invocations when unified-logs preview is off) ### Regression checks - [x] `Cmd+/` opens the reference sheet and the new "Edge Functions Navigation" and "Function Page Navigation" groups render - [x] `Cmd+K` command palette includes the new shortcut entries under "Shortcuts" - [x] On the list page, the existing X button on the search still clears value - [x] Esc handler does not interfere with closing modals/popovers elsewhere on the page <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added comprehensive keyboard shortcuts for Edge Functions (navigation, tab switching, chart intervals, create/refresh, test/send request, download, copy URL) with visible shortcut hints on relevant buttons and inputs. * **Refactor** * Layouts and product menu updated to surface and wire these shortcuts across the UI. * **Tests** * Shortcut reference tests updated to include Edge Functions groups and entries. * **Documentation** * Shortcut reference sheet labels updated to include Edge Functions sections. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45947) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
252 lines
8.0 KiB
TypeScript
252 lines
8.0 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { IS_PLATFORM, useParams } from 'common'
|
|
import { ExternalLink } from 'lucide-react'
|
|
import { useRouter } from 'next/router'
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
import { EdgeFunctionInvocationsSection } from './EdgeFunctionInvocationsSection'
|
|
import {
|
|
EDGE_FUNCTION_CHART_INTERVALS,
|
|
getBucketedTimeRange,
|
|
getExecutionMetrics,
|
|
getInvocationChartData,
|
|
getInvocationTotals,
|
|
getInvocationUpdateAnnotation,
|
|
getRollingTimeRange,
|
|
getUsageMetrics,
|
|
toEdgeFunctionChartData,
|
|
} from './EdgeFunctionOverview.utils'
|
|
import type { EdgeFunctionChartRawDatum } from './EdgeFunctionOverview.utils'
|
|
import { EdgeFunctionPerformanceSection } from './EdgeFunctionPerformanceSection'
|
|
import { EdgeFunctionRecentErrors } from './EdgeFunctionRecentErrors'
|
|
import { EdgeFunctionUsageSection } from './EdgeFunctionUsageSection'
|
|
import { useEdgeFunctionOverviewShortcuts } from './useEdgeFunctionOverviewShortcuts'
|
|
import { useUnifiedLogsPreview } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
|
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 { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
|
|
export const EdgeFunctionOverview = () => {
|
|
const router = useRouter()
|
|
const { ref: projectRef, functionSlug } = useParams()
|
|
const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
|
|
|
|
const [interval, setInterval] = useState<string>('15min')
|
|
const selectedInterval =
|
|
EDGE_FUNCTION_CHART_INTERVALS.find((item) => item.key === interval) ||
|
|
EDGE_FUNCTION_CHART_INTERVALS[1]
|
|
const {
|
|
data: selectedFunction,
|
|
error: functionError,
|
|
isPending: isLoadingFunction,
|
|
isError: isErrorFunction,
|
|
} = useEdgeFunctionQuery({
|
|
projectRef,
|
|
slug: functionSlug,
|
|
})
|
|
const id = selectedFunction?.id
|
|
const combinedStatsResults = useFunctionsCombinedStatsQuery(
|
|
{
|
|
projectRef,
|
|
functionId: id,
|
|
interval: selectedInterval.key as FunctionsCombinedStatsVariables['interval'],
|
|
},
|
|
{
|
|
enabled: IS_PLATFORM,
|
|
}
|
|
)
|
|
|
|
const combinedStatsData = useMemo(
|
|
() => (combinedStatsResults.data?.result as EdgeFunctionChartRawDatum[] | undefined) || [],
|
|
[combinedStatsResults.data]
|
|
)
|
|
|
|
const [startDate, endDate] = useMemo(
|
|
() => getBucketedTimeRange(selectedInterval),
|
|
[selectedInterval]
|
|
)
|
|
const [selectedWindowStart, selectedWindowEnd] = useMemo(
|
|
() => getRollingTimeRange(selectedInterval),
|
|
[selectedInterval]
|
|
)
|
|
const dateTimeFormat = selectedInterval.format ?? 'MMM D, h:mma'
|
|
|
|
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 chartData = useMemo(
|
|
() => toEdgeFunctionChartData(combinedStatsChartData),
|
|
[combinedStatsChartData]
|
|
)
|
|
const invocationChartData = useMemo(() => getInvocationChartData(chartData), [chartData])
|
|
const { totalInvocationCount, totalWarningCount, totalErrorCount } = useMemo(
|
|
() => getInvocationTotals(invocationChartData),
|
|
[invocationChartData]
|
|
)
|
|
const { averageExecutionTime, maxExecutionTime } = useMemo(
|
|
() => getExecutionMetrics(chartData),
|
|
[chartData]
|
|
)
|
|
const {
|
|
averageCpuTime,
|
|
maxCpuTime,
|
|
averageMemoryUsage,
|
|
totalHeapMemory,
|
|
totalExternalMemory,
|
|
totalMemoryByType,
|
|
} = useMemo(() => getUsageMetrics(chartData), [chartData])
|
|
const invocationUpdateAnnotation = useMemo(
|
|
() =>
|
|
getInvocationUpdateAnnotation({
|
|
updatedAt:
|
|
selectedFunction?.updated_at === undefined
|
|
? undefined
|
|
: String(selectedFunction.updated_at),
|
|
invocationChartData,
|
|
windowStart: selectedWindowStart,
|
|
windowEnd: selectedWindowEnd,
|
|
}),
|
|
[invocationChartData, selectedFunction?.updated_at, selectedWindowEnd, selectedWindowStart]
|
|
)
|
|
|
|
const invocationActions = useMemo(
|
|
() => [
|
|
{
|
|
label: isUnifiedLogsEnabled ? 'Open logs' : 'Open invocations',
|
|
href: `/project/${projectRef}/functions/${functionSlug}/${
|
|
isUnifiedLogsEnabled ? 'logs' : 'invocations'
|
|
}`,
|
|
icon: <ExternalLink size={12} />,
|
|
},
|
|
],
|
|
[functionSlug, isUnifiedLogsEnabled, projectRef]
|
|
)
|
|
|
|
useEdgeFunctionOverviewShortcuts({
|
|
onSetInterval: setInterval,
|
|
onRefresh: () => {
|
|
combinedStatsResults.refetch()
|
|
},
|
|
onOpenLogs: () => {
|
|
router.push(
|
|
`/project/${projectRef}/functions/${functionSlug}/${
|
|
isUnifiedLogsEnabled ? 'logs' : 'invocations'
|
|
}`
|
|
)
|
|
},
|
|
})
|
|
|
|
const { isLoading: permissionsLoading, can: canReadFunction } = useAsyncCheckPermissions(
|
|
PermissionAction.FUNCTIONS_READ,
|
|
functionSlug as string
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!IS_PLATFORM && projectRef && functionSlug) {
|
|
router.replace(`/project/${projectRef}/functions/${functionSlug}/details`)
|
|
}
|
|
}, [functionSlug, projectRef, router])
|
|
|
|
if (!canReadFunction && !permissionsLoading) {
|
|
return <NoPermission isFullPage resourceText="access this edge function" />
|
|
}
|
|
|
|
if (!IS_PLATFORM) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<EdgeFunctionInvocationsSection
|
|
interval={interval}
|
|
onIntervalChange={setInterval}
|
|
selectedInterval={selectedInterval}
|
|
actions={invocationActions}
|
|
totalInvocationCount={totalInvocationCount}
|
|
totalErrorCount={totalErrorCount}
|
|
totalWarningCount={totalWarningCount}
|
|
isLoadingFunction={isLoadingFunction}
|
|
isErrorFunction={isErrorFunction}
|
|
functionError={functionError}
|
|
isLoadingChart={combinedStatsResults.isLoading}
|
|
isErrorChart={isErrorCombinedStats}
|
|
chartErrorMessage={combinedStatsError?.message ?? 'Unknown error'}
|
|
chartData={invocationChartData}
|
|
onChartClick={() => {
|
|
router.push(
|
|
`/project/${projectRef}/functions/${functionSlug}/${
|
|
isUnifiedLogsEnabled ? 'logs' : 'invocations'
|
|
}${isUnifiedLogsEnabled ? '' : `?its=${startDate.toISOString()}`}`
|
|
)
|
|
}}
|
|
updateAnnotation={invocationUpdateAnnotation}
|
|
/>
|
|
|
|
<EdgeFunctionRecentErrors
|
|
functionId={id}
|
|
functionSlug={functionSlug as string}
|
|
projectRef={projectRef as string}
|
|
updatedAt={selectedFunction?.updated_at}
|
|
/>
|
|
|
|
<EdgeFunctionPerformanceSection
|
|
data={chartData}
|
|
dateTimeFormat={dateTimeFormat}
|
|
isLoading={combinedStatsResults.isLoading}
|
|
isError={isErrorCombinedStats}
|
|
errorMessage={combinedStatsError?.message ?? 'Unknown error'}
|
|
averageExecutionTime={averageExecutionTime}
|
|
maxExecutionTime={maxExecutionTime}
|
|
/>
|
|
|
|
<EdgeFunctionUsageSection
|
|
data={chartData}
|
|
dateTimeFormat={dateTimeFormat}
|
|
isLoading={combinedStatsResults.isLoading}
|
|
isError={isErrorCombinedStats}
|
|
errorMessage={combinedStatsError?.message ?? 'Unknown error'}
|
|
averageCpuTime={averageCpuTime}
|
|
maxCpuTime={maxCpuTime}
|
|
averageMemoryUsage={averageMemoryUsage}
|
|
totalHeapMemory={totalHeapMemory}
|
|
totalExternalMemory={totalExternalMemory}
|
|
totalMemoryByType={totalMemoryByType}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default EdgeFunctionOverview
|