Files
supabase/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.tsx
Ali Waseem bffe49bb1d feat(edge-functions): keyboard shortcuts on overview, detail, and test sheet (#45947)
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 -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](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>
2026-05-15 07:59:18 -06:00

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