mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
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 --> [](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>
This commit is contained in:
1 parent
8c6d5036ea
commit
bffe49bb1d
17 files changed
+657
-90
No files matched your search
+33
-20
@@ -38,6 +38,7 @@ import * as z from 'zod'
|
||||
import { HTTP_METHODS } from './EdgeFunctionDetails.constants'
|
||||
import { ErrorWithStatus, ResponseData } from './EdgeFunctionDetails.types'
|
||||
import { RoleImpersonationPopover } from '@/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover'
|
||||
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
|
||||
import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
|
||||
import { useSessionAccessTokenQuery } from '@/data/auth/session-access-token-query'
|
||||
import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
|
||||
@@ -53,6 +54,8 @@ import {
|
||||
RoleImpersonationStateContextProvider,
|
||||
useGetImpersonatedRoleState,
|
||||
} from '@/state/role-impersonation-state'
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
||||
|
||||
interface EdgeFunctionTesterSheetProps {
|
||||
visible: boolean
|
||||
@@ -172,6 +175,14 @@ const EdgeFunctionTesterSheetContent = ({ visible, onClose }: EdgeFunctionTester
|
||||
}
|
||||
}
|
||||
|
||||
useShortcut(
|
||||
SHORTCUT_IDS.FUNCTION_DETAIL_SUBMIT_TEST,
|
||||
() => {
|
||||
form.handleSubmit(onSubmit)()
|
||||
},
|
||||
{ enabled: visible && !isPending }
|
||||
)
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
setError(null)
|
||||
setResponse(null)
|
||||
@@ -461,26 +472,28 @@ const EdgeFunctionTesterSheetContent = ({ visible, onClose }: EdgeFunctionTester
|
||||
disallowAuthenticatedOption
|
||||
header="Run edge function as a role"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isPending}
|
||||
disabled={isPending}
|
||||
onClick={() =>
|
||||
sendEvent({
|
||||
action: 'edge_function_test_send_button_clicked',
|
||||
properties: {
|
||||
httpMethod: method,
|
||||
},
|
||||
groups: {
|
||||
project: projectRef ?? 'Unknown',
|
||||
organization: org?.slug ?? 'Unknown',
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Send Request
|
||||
</Button>
|
||||
<ShortcutTooltip shortcutId={SHORTCUT_IDS.FUNCTION_DETAIL_SUBMIT_TEST} side="top">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isPending}
|
||||
disabled={isPending}
|
||||
onClick={() =>
|
||||
sendEvent({
|
||||
action: 'edge_function_test_send_button_clicked',
|
||||
properties: {
|
||||
httpMethod: method,
|
||||
},
|
||||
groups: {
|
||||
project: projectRef ?? 'Unknown',
|
||||
organization: org?.slug ?? 'Unknown',
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Send Request
|
||||
</Button>
|
||||
</ShortcutTooltip>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
+15
@@ -20,6 +20,7 @@ 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 {
|
||||
@@ -153,6 +154,20 @@ export const EdgeFunctionOverview = () => {
|
||||
[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
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
||||
|
||||
interface UseEdgeFunctionOverviewShortcutsParams {
|
||||
onSetInterval: (key: string) => void
|
||||
onRefresh: () => void
|
||||
onOpenLogs: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcuts scoped to the per-function Overview tab (the new flag-gated UI).
|
||||
*
|
||||
* - I+M / I+H / I+T / I+D: select chart interval
|
||||
* - Shift+R: refresh the combined stats query
|
||||
* - O+L: jump to the Logs (or Invocations) sub-page
|
||||
*
|
||||
* Should be mounted once inside `EdgeFunctionOverview`.
|
||||
*/
|
||||
export function useEdgeFunctionOverviewShortcuts({
|
||||
onSetInterval,
|
||||
onRefresh,
|
||||
onOpenLogs,
|
||||
}: UseEdgeFunctionOverviewShortcutsParams) {
|
||||
useShortcut(SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_15MIN, () => onSetInterval('15min'))
|
||||
useShortcut(SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_1HR, () => onSetInterval('1hr'))
|
||||
useShortcut(SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_3HR, () => onSetInterval('3hr'))
|
||||
useShortcut(SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_1DAY, () => onSetInterval('1day'))
|
||||
useShortcut(SHORTCUT_IDS.FUNCTION_OVERVIEW_REFRESH, onRefresh)
|
||||
useShortcut(SHORTCUT_IDS.FUNCTION_OVERVIEW_OPEN_LOGS, onOpenLogs)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
||||
|
||||
interface UseFunctionsDetailShortcutsParams {
|
||||
projectRef: string | undefined
|
||||
functionSlug: string | undefined
|
||||
isPlatform: boolean
|
||||
onOpenTest: () => void
|
||||
onOpenDownload: () => void
|
||||
onCopyUrl: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers shortcuts that apply across every tab of a single edge function:
|
||||
* - 1..5: jump between Overview / Invocations / Logs / Code / Settings
|
||||
* - Shift+T: open the test sheet
|
||||
* - Shift+D: open the download popover
|
||||
* - Shift+C: copy the function URL
|
||||
*
|
||||
* Should be mounted once at the EdgeFunctionDetailsLayout level.
|
||||
*/
|
||||
export function useFunctionsDetailShortcuts({
|
||||
projectRef,
|
||||
functionSlug,
|
||||
isPlatform,
|
||||
onOpenTest,
|
||||
onOpenDownload,
|
||||
onCopyUrl,
|
||||
}: UseFunctionsDetailShortcutsParams) {
|
||||
const router = useRouter()
|
||||
|
||||
const ready = Boolean(projectRef && functionSlug)
|
||||
const base = `/project/${projectRef}/functions/${functionSlug}`
|
||||
|
||||
useShortcut(SHORTCUT_IDS.NAV_FUNCTION_DETAIL_OVERVIEW, () => router.push(base), {
|
||||
enabled: ready && isPlatform,
|
||||
})
|
||||
|
||||
useShortcut(
|
||||
SHORTCUT_IDS.NAV_FUNCTION_DETAIL_INVOCATIONS,
|
||||
() => router.push(`${base}/invocations`),
|
||||
{ enabled: ready && isPlatform }
|
||||
)
|
||||
|
||||
useShortcut(SHORTCUT_IDS.NAV_FUNCTION_DETAIL_LOGS, () => router.push(`${base}/logs`), {
|
||||
enabled: ready && isPlatform,
|
||||
})
|
||||
|
||||
useShortcut(SHORTCUT_IDS.NAV_FUNCTION_DETAIL_CODE, () => router.push(`${base}/code`), {
|
||||
enabled: ready,
|
||||
})
|
||||
|
||||
useShortcut(SHORTCUT_IDS.NAV_FUNCTION_DETAIL_SETTINGS, () => router.push(`${base}/details`), {
|
||||
enabled: ready,
|
||||
})
|
||||
|
||||
useShortcut(SHORTCUT_IDS.FUNCTION_DETAIL_OPEN_TEST, onOpenTest, { enabled: ready })
|
||||
|
||||
useShortcut(SHORTCUT_IDS.FUNCTION_DETAIL_OPEN_DOWNLOAD, onOpenDownload, { enabled: ready })
|
||||
|
||||
useShortcut(SHORTCUT_IDS.FUNCTION_DETAIL_COPY_URL, onCopyUrl, { enabled: ready })
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Dispatch, RefObject, SetStateAction } from 'react'
|
||||
|
||||
import type { EdgeFunctionsSort } from '@/components/interfaces/EdgeFunctions/EdgeFunctionsSortDropdown'
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
||||
|
||||
const DEFAULT_SORT: EdgeFunctionsSort = 'name:asc'
|
||||
|
||||
interface UseFunctionsListShortcutsParams {
|
||||
searchInputRef: RefObject<HTMLInputElement>
|
||||
setSearch: Dispatch<SetStateAction<string>> | ((value: string) => void)
|
||||
sort: EdgeFunctionsSort
|
||||
setSort: (value: EdgeFunctionsSort) => void
|
||||
onCreateNew: () => void
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export function useFunctionsListShortcuts({
|
||||
searchInputRef,
|
||||
setSearch,
|
||||
sort,
|
||||
setSort,
|
||||
onCreateNew,
|
||||
onRefresh,
|
||||
}: UseFunctionsListShortcutsParams) {
|
||||
useShortcut(
|
||||
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
|
||||
() => {
|
||||
searchInputRef.current?.focus()
|
||||
searchInputRef.current?.select()
|
||||
},
|
||||
{ label: 'Search functions' }
|
||||
)
|
||||
|
||||
useShortcut(SHORTCUT_IDS.LIST_PAGE_NEW_ITEM, onCreateNew, {
|
||||
label: 'Deploy a new function',
|
||||
})
|
||||
|
||||
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
|
||||
setSearch('')
|
||||
})
|
||||
|
||||
useShortcut(SHORTCUT_IDS.FUNCTIONS_LIST_REFRESH, onRefresh)
|
||||
|
||||
useShortcut(SHORTCUT_IDS.FUNCTIONS_LIST_CLEAR_SORT, () => setSort(DEFAULT_SORT), {
|
||||
enabled: sort !== DEFAULT_SORT,
|
||||
})
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
BreadcrumbList_Shadcn_ as BreadcrumbList,
|
||||
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
|
||||
Button,
|
||||
copyToClipboard,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
@@ -40,9 +41,11 @@ import {
|
||||
import { ProjectLayout } from '../ProjectLayout'
|
||||
import EdgeFunctionsLayout from './EdgeFunctionsLayout'
|
||||
import { EdgeFunctionTesterSheet } from '@/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet'
|
||||
import { useFunctionsDetailShortcuts } from '@/components/interfaces/Functions/useFunctionsDetailShortcuts'
|
||||
import CopyButton from '@/components/ui/CopyButton'
|
||||
import { DocsButton } from '@/components/ui/DocsButton'
|
||||
import NoPermission from '@/components/ui/NoPermission'
|
||||
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
|
||||
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
|
||||
import { useEdgeFunctionBodyQuery } from '@/data/edge-functions/edge-function-body-query'
|
||||
import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
|
||||
@@ -51,6 +54,7 @@ import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { withAuth } from '@/hooks/misc/withAuth'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
@@ -73,6 +77,7 @@ const EdgeFunctionDetailsLayout = ({
|
||||
)
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isDownloadOpen, setIsDownloadOpen] = useState(false)
|
||||
const [isTimestampHoverCardOpen, setIsTimestampHoverCardOpen] = useState(false)
|
||||
|
||||
const {
|
||||
@@ -235,6 +240,35 @@ const EdgeFunctionDetailsLayout = ({
|
||||
}
|
||||
}, [isError])
|
||||
|
||||
const openTestSheet = () => {
|
||||
if (!functionSlug) return
|
||||
setIsOpen(true)
|
||||
if (IS_PLATFORM) {
|
||||
sendEvent({
|
||||
action: 'edge_function_test_side_panel_opened',
|
||||
groups: {
|
||||
project: ref ?? 'Unknown',
|
||||
organization: org?.slug ?? 'Unknown',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const copyFunctionUrl = () => {
|
||||
if (!functionUrl) return
|
||||
copyToClipboard(functionUrl)
|
||||
toast.success('Function URL copied to clipboard')
|
||||
}
|
||||
|
||||
useFunctionsDetailShortcuts({
|
||||
projectRef: ref,
|
||||
functionSlug,
|
||||
isPlatform: IS_PLATFORM,
|
||||
onOpenTest: openTestSheet,
|
||||
onOpenDownload: () => setIsDownloadOpen((prev) => !prev),
|
||||
onCopyUrl: copyFunctionUrl,
|
||||
})
|
||||
|
||||
if (!isLoading && !canReadFunctions) {
|
||||
return (
|
||||
<ProjectLayout product="Edge Functions" browserTitle={browserTitle}>
|
||||
@@ -274,7 +308,9 @@ const EdgeFunctionDetailsLayout = ({
|
||||
<PageHeaderDescription className="flex flex-row flex-wrap items-center gap-x-4 gap-y-1 text-sm!">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span className="flex items-center gap-2">{functionUrl}</span>
|
||||
<CopyButton iconOnly type="text" text={functionUrl} />
|
||||
<ShortcutTooltip shortcutId={SHORTCUT_IDS.FUNCTION_DETAIL_COPY_URL} side="bottom">
|
||||
<CopyButton iconOnly type="text" text={functionUrl} />
|
||||
</ShortcutTooltip>
|
||||
</div>
|
||||
|
||||
<HoverCard
|
||||
@@ -330,12 +366,18 @@ const EdgeFunctionDetailsLayout = ({
|
||||
<PageHeaderAside>
|
||||
<div className="flex items-center space-x-2">
|
||||
<DocsButton href={`${DOCS_URL}/guides/functions`} />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button type="default" icon={<Download />}>
|
||||
Download
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<Popover open={isDownloadOpen} onOpenChange={setIsDownloadOpen}>
|
||||
<ShortcutTooltip
|
||||
shortcutId={SHORTCUT_IDS.FUNCTION_DETAIL_OPEN_DOWNLOAD}
|
||||
side="bottom"
|
||||
open={isDownloadOpen ? false : undefined}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button type="default" icon={<Download />}>
|
||||
Download
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</ShortcutTooltip>
|
||||
<PopoverContent align="end" className="p-0">
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
@@ -366,24 +408,14 @@ const EdgeFunctionDetailsLayout = ({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{!!functionSlug && (
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Send />}
|
||||
onClick={() => {
|
||||
setIsOpen(true)
|
||||
if (IS_PLATFORM) {
|
||||
sendEvent({
|
||||
action: 'edge_function_test_side_panel_opened',
|
||||
groups: {
|
||||
project: ref ?? 'Unknown',
|
||||
organization: org?.slug ?? 'Unknown',
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
<ShortcutTooltip
|
||||
shortcutId={SHORTCUT_IDS.FUNCTION_DETAIL_OPEN_TEST}
|
||||
side="bottom"
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
<Button type="default" icon={<Send />} onClick={openTestSheet}>
|
||||
Test
|
||||
</Button>
|
||||
</ShortcutTooltip>
|
||||
)}
|
||||
</div>
|
||||
</PageHeaderAside>
|
||||
|
||||
@@ -1,38 +1,50 @@
|
||||
import { useParams } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import type { ComponentProps, PropsWithChildren } from 'react'
|
||||
import { useMemo, type ComponentProps, type PropsWithChildren } from 'react'
|
||||
|
||||
import { ProjectLayout } from '../ProjectLayout'
|
||||
import { ProductMenu } from '@/components/ui/ProductMenu'
|
||||
import type { ProductMenuGroup } from '@/components/ui/ProductMenu/ProductMenu.types'
|
||||
import { ProductMenuShortcuts } from '@/components/ui/ProductMenu/ProductMenuShortcuts'
|
||||
import { withAuth } from '@/hooks/misc/withAuth'
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
|
||||
const useGenerateEdgeFunctionsMenu = (): ProductMenuGroup[] => {
|
||||
const { ref: projectRef = 'default' } = useParams()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Manage',
|
||||
items: [
|
||||
{
|
||||
name: 'Functions',
|
||||
key: 'main',
|
||||
pages: ['', '[functionSlug]', 'new'],
|
||||
url: `/project/${projectRef}/functions`,
|
||||
items: [],
|
||||
shortcutId: SHORTCUT_IDS.NAV_FUNCTIONS_OVERVIEW,
|
||||
},
|
||||
{
|
||||
name: 'Secrets',
|
||||
key: 'secrets',
|
||||
url: `/project/${projectRef}/functions/secrets`,
|
||||
items: [],
|
||||
shortcutId: SHORTCUT_IDS.NAV_FUNCTIONS_SECRETS,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[projectRef]
|
||||
)
|
||||
}
|
||||
|
||||
export const EdgeFunctionsProductMenu = () => {
|
||||
const { ref: projectRef = 'default' } = useParams()
|
||||
const router = useRouter()
|
||||
const page = router.pathname.split('/')[4]
|
||||
const menu = useGenerateEdgeFunctionsMenu()
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
title: 'Manage',
|
||||
items: [
|
||||
{
|
||||
name: 'Functions',
|
||||
key: 'main',
|
||||
pages: ['', '[functionSlug]', 'new'],
|
||||
url: `/project/${projectRef}/functions`,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Secrets',
|
||||
key: 'secrets',
|
||||
url: `/project/${projectRef}/functions/secrets`,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return <ProductMenu page={page} menu={menuItems} />
|
||||
return <ProductMenu page={page} menu={menu} />
|
||||
}
|
||||
|
||||
interface EdgeFunctionsLayoutProps {
|
||||
@@ -45,6 +57,8 @@ const EdgeFunctionsLayout = ({
|
||||
title,
|
||||
browserTitle,
|
||||
}: PropsWithChildren<EdgeFunctionsLayoutProps>) => {
|
||||
const menu = useGenerateEdgeFunctionsMenu()
|
||||
|
||||
return (
|
||||
<ProjectLayout
|
||||
product="Edge Functions"
|
||||
@@ -52,6 +66,7 @@ const EdgeFunctionsLayout = ({
|
||||
productMenu={<EdgeFunctionsProductMenu />}
|
||||
isBlocking={false}
|
||||
>
|
||||
<ProductMenuShortcuts menu={menu} />
|
||||
{children}
|
||||
</ProjectLayout>
|
||||
)
|
||||
|
||||
@@ -37,9 +37,17 @@ const ACTIVE_AUTH_SHORTCUT_IDS = [
|
||||
SHORTCUT_IDS.NAV_AUTH_USERS,
|
||||
] satisfies ShortcutId[]
|
||||
|
||||
const ACTIVE_FUNCTION_DETAIL_NAV_SHORTCUT_IDS = [
|
||||
...ACTIVE_SHORTCUT_IDS,
|
||||
SHORTCUT_IDS.NAV_FUNCTION_DETAIL_OVERVIEW,
|
||||
] satisfies ShortcutId[]
|
||||
|
||||
const ACTIVE_SURFACE_SHORTCUT_IDS = [
|
||||
...ACTIVE_SHORTCUT_IDS,
|
||||
SHORTCUT_IDS.AUTH_USERS_REFRESH,
|
||||
SHORTCUT_IDS.FUNCTION_DETAIL_OPEN_TEST,
|
||||
SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_15MIN,
|
||||
SHORTCUT_IDS.FUNCTIONS_LIST_REFRESH,
|
||||
SHORTCUT_IDS.SQL_EDITOR_FORMAT,
|
||||
SHORTCUT_IDS.STORAGE_BUCKETS_REFRESH,
|
||||
SHORTCUT_IDS.STORAGE_EXPLORER_REFRESH,
|
||||
@@ -150,14 +158,29 @@ describe('ShortcutsReferenceSheet', () => {
|
||||
expect(screen.getByText('Go to Users')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the edge function tabs section when function tab shortcuts are active', async () => {
|
||||
renderShortcutsReferenceSheet(ACTIVE_FUNCTION_DETAIL_NAV_SHORTCUT_IDS)
|
||||
|
||||
expect(await screen.findByText('Global Navigation')).toBeInTheDocument()
|
||||
expect(screen.getByText('Edge Function Tabs')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Edge Function Page Navigation')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Go to Overview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses human labels for active surface shortcut groups', async () => {
|
||||
renderShortcutsReferenceSheet(ACTIVE_SURFACE_SHORTCUT_IDS)
|
||||
|
||||
expect(await screen.findByText('Auth Users')).toBeInTheDocument()
|
||||
expect(screen.getByText('Edge Function Actions')).toBeInTheDocument()
|
||||
expect(screen.getByText('Edge Function Overview')).toBeInTheDocument()
|
||||
expect(screen.getByText('Edge Functions')).toBeInTheDocument()
|
||||
expect(screen.getByText('SQL Editor')).toBeInTheDocument()
|
||||
expect(screen.getByText('Storage Buckets')).toBeInTheDocument()
|
||||
expect(screen.getByText('Storage File Explorer')).toBeInTheDocument()
|
||||
expect(screen.queryByText('auth-users')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('functions-detail')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('functions-list')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('functions-overview')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('sql-editor')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('storage-buckets')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('storage-explorer')).not.toBeInTheDocument()
|
||||
|
||||
@@ -46,6 +46,9 @@ const GROUP_LABELS: Record<string, string> = {
|
||||
'auth-users': 'Auth Users',
|
||||
'command-menu': 'Command Menu',
|
||||
'data-table': 'Data Tables',
|
||||
'functions-detail': 'Edge Function Actions',
|
||||
'functions-list': 'Edge Functions',
|
||||
'functions-overview': 'Edge Function Overview',
|
||||
'inline-editor': 'Inline Editor',
|
||||
'list-page': 'List pages',
|
||||
nav: 'Navigation',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useFlag, useParams } from 'common'
|
||||
import { ExternalLink, Search, X } from 'lucide-react'
|
||||
import { ExternalLink, RefreshCw, Search, X } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useMemo, useRef } from 'react'
|
||||
import { Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
@@ -31,18 +32,25 @@ import {
|
||||
FunctionsInstructionsLocal,
|
||||
} from '@/components/interfaces/Functions/FunctionsEmptyState'
|
||||
import { TerminalInstructionsDialog } from '@/components/interfaces/Functions/TerminalInstructionsDialog'
|
||||
import { useFunctionsListShortcuts } from '@/components/interfaces/Functions/useFunctionsListShortcuts'
|
||||
import DefaultLayout from '@/components/layouts/DefaultLayout'
|
||||
import EdgeFunctionsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout'
|
||||
import AlertError from '@/components/ui/AlertError'
|
||||
import { DocsButton } from '@/components/ui/DocsButton'
|
||||
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
|
||||
import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
|
||||
import { DOCS_URL, IS_PLATFORM } from '@/lib/constants'
|
||||
import { onSearchInputEscape } from '@/lib/keyboard'
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
import type { NextPageWithLayout } from '@/types'
|
||||
|
||||
const EdgeFunctionsPage: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const showLastHourStats = useFlag('edgeFunctionsRequestMetrics')
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const [search, setSearch] = useQueryState('search', parseAsString.withDefault(''))
|
||||
const [sort, setSortQueryParam] = useQueryState(
|
||||
'sort',
|
||||
@@ -55,8 +63,21 @@ const EdgeFunctionsPage: NextPageWithLayout = () => {
|
||||
isPending: isLoading,
|
||||
isError,
|
||||
isSuccess,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useEdgeFunctionsQuery({ projectRef: ref })
|
||||
|
||||
useFunctionsListShortcuts({
|
||||
searchInputRef,
|
||||
setSearch,
|
||||
sort,
|
||||
setSort: setSortQueryParam,
|
||||
onCreateNew: () => router.push(`/project/${ref}/functions/new`),
|
||||
onRefresh: () => {
|
||||
refetch()
|
||||
},
|
||||
})
|
||||
|
||||
const filteredFunctions = useMemo(() => {
|
||||
const temp = (functions ?? []).filter((x) =>
|
||||
x.name.toLowerCase().includes(search.toLowerCase())
|
||||
@@ -106,31 +127,52 @@ const EdgeFunctionsPage: NextPageWithLayout = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Search function names"
|
||||
icon={<Search />}
|
||||
size="tiny"
|
||||
className="w-32 md:w-64"
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
actions={[
|
||||
search && (
|
||||
<Button
|
||||
key="clear"
|
||||
size="tiny"
|
||||
type="text"
|
||||
icon={<X />}
|
||||
onClick={() => setSearch('')}
|
||||
className="p-0 h-5 w-5"
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
<ShortcutTooltip
|
||||
shortcutId={SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH}
|
||||
label="Search functions"
|
||||
side="bottom"
|
||||
>
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder="Search function names"
|
||||
icon={<Search />}
|
||||
size="tiny"
|
||||
className="w-32 md:w-64"
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
onKeyDown={onSearchInputEscape(search, setSearch)}
|
||||
actions={[
|
||||
search && (
|
||||
<Button
|
||||
key="clear"
|
||||
size="tiny"
|
||||
type="text"
|
||||
icon={<X />}
|
||||
onClick={() => setSearch('')}
|
||||
className="p-0 h-5 w-5"
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</ShortcutTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<EdgeFunctionsSortDropdown value={sort} onChange={setSortQueryParam} />
|
||||
</div>
|
||||
<ShortcutTooltip
|
||||
shortcutId={SHORTCUT_IDS.FUNCTIONS_LIST_REFRESH}
|
||||
side="bottom"
|
||||
>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<RefreshCw />}
|
||||
loading={isFetching}
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</ShortcutTooltip>
|
||||
<span className="border-l border-default pl-2 text-xs text-foreground-light">
|
||||
{search && filteredFunctions.length !== functions.length
|
||||
? `Viewing ${filteredFunctions.length} of ${functions.length} functions in total`
|
||||
|
||||
@@ -3,6 +3,8 @@ export const SHORTCUT_REFERENCE_GROUPS = {
|
||||
NAVIGATION_DATABASE: 'navigation.database',
|
||||
NAVIGATION_AUTH: 'navigation.auth',
|
||||
NAVIGATION_STORAGE: 'navigation.storage',
|
||||
NAVIGATION_FUNCTIONS: 'navigation.functions',
|
||||
NAVIGATION_FUNCTION_DETAIL: 'navigation.function-detail',
|
||||
} as const
|
||||
|
||||
export const SHORTCUT_REFERENCE_GROUP_LABELS: Record<string, string> = {
|
||||
@@ -10,6 +12,8 @@ export const SHORTCUT_REFERENCE_GROUP_LABELS: Record<string, string> = {
|
||||
[SHORTCUT_REFERENCE_GROUPS.NAVIGATION_DATABASE]: 'Database Navigation',
|
||||
[SHORTCUT_REFERENCE_GROUPS.NAVIGATION_AUTH]: 'Auth Navigation',
|
||||
[SHORTCUT_REFERENCE_GROUPS.NAVIGATION_STORAGE]: 'Storage Navigation',
|
||||
[SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTIONS]: 'Edge Functions Navigation',
|
||||
[SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTION_DETAIL]: 'Edge Function Tabs',
|
||||
}
|
||||
|
||||
export const SHORTCUT_REFERENCE_GROUP_ORDER = [
|
||||
@@ -22,6 +26,11 @@ export const SHORTCUT_REFERENCE_GROUP_ORDER = [
|
||||
SHORTCUT_REFERENCE_GROUPS.NAVIGATION_STORAGE,
|
||||
'storage-buckets',
|
||||
'storage-explorer',
|
||||
SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTIONS,
|
||||
'functions-list',
|
||||
SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTION_DETAIL,
|
||||
'functions-detail',
|
||||
'functions-overview',
|
||||
'nav',
|
||||
'ai-assistant',
|
||||
'inline-editor',
|
||||
|
||||
@@ -2,6 +2,17 @@ import { SHORTCUT_REFERENCE_GROUPS } from './referenceGroups'
|
||||
import { AUTH_NAV_SHORTCUT_IDS, authNavRegistry } from './registry/auth-nav'
|
||||
import { AUTH_USERS_SHORTCUT_IDS, authUsersRegistry } from './registry/auth-users'
|
||||
import { DATABASE_NAV_SHORTCUT_IDS, databaseNavRegistry } from './registry/database-nav'
|
||||
import { FUNCTIONS_DETAIL_SHORTCUT_IDS, functionsDetailRegistry } from './registry/functions-detail'
|
||||
import {
|
||||
FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS,
|
||||
functionsDetailNavRegistry,
|
||||
} from './registry/functions-detail-nav'
|
||||
import { FUNCTIONS_LIST_SHORTCUT_IDS, functionsListRegistry } from './registry/functions-list'
|
||||
import { FUNCTIONS_NAV_SHORTCUT_IDS, functionsNavRegistry } from './registry/functions-nav'
|
||||
import {
|
||||
FUNCTIONS_OVERVIEW_SHORTCUT_IDS,
|
||||
functionsOverviewRegistry,
|
||||
} from './registry/functions-overview'
|
||||
import { LIST_PAGE_SHORTCUT_IDS, listPageRegistry } from './registry/list-page'
|
||||
import {
|
||||
SCHEMA_VISUALIZER_SHORTCUT_IDS,
|
||||
@@ -86,6 +97,17 @@ export const SHORTCUT_IDS = {
|
||||
...STORAGE_BUCKETS_SHORTCUT_IDS,
|
||||
// Storage Explorer (file browser) shortcuts
|
||||
...STORAGE_EXPLORER_SHORTCUT_IDS,
|
||||
|
||||
// Edge Functions sub-page navigation chords
|
||||
...FUNCTIONS_NAV_SHORTCUT_IDS,
|
||||
// Edge Functions overview (list) page shortcuts
|
||||
...FUNCTIONS_LIST_SHORTCUT_IDS,
|
||||
// Per-function detail layout shortcuts (header actions + test submit)
|
||||
...FUNCTIONS_DETAIL_SHORTCUT_IDS,
|
||||
// Per-function detail tab navigation (digits)
|
||||
...FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS,
|
||||
// Per-function Overview tab shortcuts (intervals, refresh, open logs)
|
||||
...FUNCTIONS_OVERVIEW_SHORTCUT_IDS,
|
||||
} as const
|
||||
|
||||
/**
|
||||
@@ -377,4 +399,15 @@ export const SHORTCUT_DEFINITIONS: Record<ShortcutId, ShortcutDefinition> = {
|
||||
...storageBucketsRegistry,
|
||||
// Storage Explorer (file browser) shortcut registration
|
||||
...storageExplorerRegistry,
|
||||
|
||||
// Edge Functions sub-page navigation chord registration
|
||||
...functionsNavRegistry,
|
||||
// Edge Functions overview (list) page shortcut registration
|
||||
...functionsListRegistry,
|
||||
// Per-function detail layout shortcut registration
|
||||
...functionsDetailRegistry,
|
||||
// Per-function detail tab navigation registration
|
||||
...functionsDetailNavRegistry,
|
||||
// Per-function Overview tab shortcut registration
|
||||
...functionsOverviewRegistry,
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { SHORTCUT_REFERENCE_GROUPS } from '../referenceGroups'
|
||||
import { RegistryDefinations } from '../types'
|
||||
|
||||
/**
|
||||
* Digit shortcuts (1-5) for jumping between the tabs on a function detail
|
||||
* page — Overview, Invocations, Logs, Code, Settings.
|
||||
*
|
||||
* Active only while EdgeFunctionDetailsLayout is mounted (i.e. the user is
|
||||
* on a `/project/<ref>/functions/<slug>/*` route). Digits are scoped to this
|
||||
* layout so they don't burn global keys.
|
||||
*/
|
||||
export const FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS = {
|
||||
NAV_FUNCTION_DETAIL_OVERVIEW: 'nav.function-detail-overview',
|
||||
NAV_FUNCTION_DETAIL_INVOCATIONS: 'nav.function-detail-invocations',
|
||||
NAV_FUNCTION_DETAIL_LOGS: 'nav.function-detail-logs',
|
||||
NAV_FUNCTION_DETAIL_CODE: 'nav.function-detail-code',
|
||||
NAV_FUNCTION_DETAIL_SETTINGS: 'nav.function-detail-settings',
|
||||
}
|
||||
|
||||
export type FunctionsDetailNavShortcutId =
|
||||
(typeof FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS)[keyof typeof FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS]
|
||||
|
||||
export const functionsDetailNavRegistry: RegistryDefinations<FunctionsDetailNavShortcutId> = {
|
||||
[FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS.NAV_FUNCTION_DETAIL_OVERVIEW]: {
|
||||
id: FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS.NAV_FUNCTION_DETAIL_OVERVIEW,
|
||||
label: 'Go to Overview',
|
||||
sequence: ['1'],
|
||||
showInSettings: false,
|
||||
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTION_DETAIL,
|
||||
},
|
||||
[FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS.NAV_FUNCTION_DETAIL_INVOCATIONS]: {
|
||||
id: FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS.NAV_FUNCTION_DETAIL_INVOCATIONS,
|
||||
label: 'Go to Invocations',
|
||||
sequence: ['2'],
|
||||
showInSettings: false,
|
||||
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTION_DETAIL,
|
||||
},
|
||||
[FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS.NAV_FUNCTION_DETAIL_LOGS]: {
|
||||
id: FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS.NAV_FUNCTION_DETAIL_LOGS,
|
||||
label: 'Go to Logs',
|
||||
sequence: ['3'],
|
||||
showInSettings: false,
|
||||
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTION_DETAIL,
|
||||
},
|
||||
[FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS.NAV_FUNCTION_DETAIL_CODE]: {
|
||||
id: FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS.NAV_FUNCTION_DETAIL_CODE,
|
||||
label: 'Go to Code',
|
||||
sequence: ['4'],
|
||||
showInSettings: false,
|
||||
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTION_DETAIL,
|
||||
},
|
||||
[FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS.NAV_FUNCTION_DETAIL_SETTINGS]: {
|
||||
id: FUNCTIONS_DETAIL_NAV_SHORTCUT_IDS.NAV_FUNCTION_DETAIL_SETTINGS,
|
||||
label: 'Go to Settings',
|
||||
sequence: ['5'],
|
||||
showInSettings: false,
|
||||
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTION_DETAIL,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { RegistryDefinations } from '../types'
|
||||
|
||||
/**
|
||||
* Shortcuts scoped to a single edge function's detail layout — active anywhere
|
||||
* under `/project/<ref>/functions/<slug>/*`.
|
||||
*
|
||||
* The test sheet submit is registered here too, since it conceptually belongs
|
||||
* with the test-related actions even though it's only listened to while the
|
||||
* sheet is open.
|
||||
*/
|
||||
export const FUNCTIONS_DETAIL_SHORTCUT_IDS = {
|
||||
FUNCTION_DETAIL_OPEN_TEST: 'functions-detail.open-test',
|
||||
FUNCTION_DETAIL_OPEN_DOWNLOAD: 'functions-detail.open-download',
|
||||
FUNCTION_DETAIL_COPY_URL: 'functions-detail.copy-url',
|
||||
FUNCTION_DETAIL_SUBMIT_TEST: 'functions-detail.submit-test',
|
||||
}
|
||||
|
||||
export type FunctionsDetailShortcutId =
|
||||
(typeof FUNCTIONS_DETAIL_SHORTCUT_IDS)[keyof typeof FUNCTIONS_DETAIL_SHORTCUT_IDS]
|
||||
|
||||
export const functionsDetailRegistry: RegistryDefinations<FunctionsDetailShortcutId> = {
|
||||
[FUNCTIONS_DETAIL_SHORTCUT_IDS.FUNCTION_DETAIL_OPEN_TEST]: {
|
||||
id: FUNCTIONS_DETAIL_SHORTCUT_IDS.FUNCTION_DETAIL_OPEN_TEST,
|
||||
label: 'Open test sheet',
|
||||
sequence: ['Shift+T'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
[FUNCTIONS_DETAIL_SHORTCUT_IDS.FUNCTION_DETAIL_OPEN_DOWNLOAD]: {
|
||||
id: FUNCTIONS_DETAIL_SHORTCUT_IDS.FUNCTION_DETAIL_OPEN_DOWNLOAD,
|
||||
label: 'Open download menu',
|
||||
sequence: ['Shift+D'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
[FUNCTIONS_DETAIL_SHORTCUT_IDS.FUNCTION_DETAIL_COPY_URL]: {
|
||||
id: FUNCTIONS_DETAIL_SHORTCUT_IDS.FUNCTION_DETAIL_COPY_URL,
|
||||
label: 'Copy function URL',
|
||||
sequence: ['Shift+C'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
[FUNCTIONS_DETAIL_SHORTCUT_IDS.FUNCTION_DETAIL_SUBMIT_TEST]: {
|
||||
id: FUNCTIONS_DETAIL_SHORTCUT_IDS.FUNCTION_DETAIL_SUBMIT_TEST,
|
||||
label: 'Send test request',
|
||||
sequence: ['Mod+Enter'],
|
||||
showInSettings: false,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { RegistryDefinations } from '../types'
|
||||
|
||||
/**
|
||||
* Shortcuts scoped to the Edge Functions overview (list) page.
|
||||
*
|
||||
* The shared list-page shortcuts (focus search, create new item, reset
|
||||
* filters) are reused directly from `list-page.ts`. Only functions-specific
|
||||
* actions live here.
|
||||
*/
|
||||
export const FUNCTIONS_LIST_SHORTCUT_IDS = {
|
||||
FUNCTIONS_LIST_REFRESH: 'functions-list.refresh',
|
||||
FUNCTIONS_LIST_CLEAR_SORT: 'functions-list.clear-sort',
|
||||
}
|
||||
|
||||
export type FunctionsListShortcutId =
|
||||
(typeof FUNCTIONS_LIST_SHORTCUT_IDS)[keyof typeof FUNCTIONS_LIST_SHORTCUT_IDS]
|
||||
|
||||
export const functionsListRegistry: RegistryDefinations<FunctionsListShortcutId> = {
|
||||
[FUNCTIONS_LIST_SHORTCUT_IDS.FUNCTIONS_LIST_REFRESH]: {
|
||||
id: FUNCTIONS_LIST_SHORTCUT_IDS.FUNCTIONS_LIST_REFRESH,
|
||||
label: 'Refresh functions',
|
||||
sequence: ['Shift+R'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
[FUNCTIONS_LIST_SHORTCUT_IDS.FUNCTIONS_LIST_CLEAR_SORT]: {
|
||||
id: FUNCTIONS_LIST_SHORTCUT_IDS.FUNCTIONS_LIST_CLEAR_SORT,
|
||||
label: 'Clear sort',
|
||||
sequence: ['S', 'C'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { SHORTCUT_REFERENCE_GROUPS } from '../referenceGroups'
|
||||
import { RegistryDefinations } from '../types'
|
||||
|
||||
/**
|
||||
* Contextual chords for jumping between Edge Functions sub-pages — `F + <letter>`.
|
||||
*
|
||||
* Active only while EdgeFunctionsLayout is mounted (i.e. the user is somewhere
|
||||
* under `/project/<ref>/functions/*`). Mirrors the database-nav / auth-nav /
|
||||
* storage-nav pattern: the leading `F` is layout-scoped so it doesn't burn a
|
||||
* global key.
|
||||
*/
|
||||
export const FUNCTIONS_NAV_SHORTCUT_IDS = {
|
||||
NAV_FUNCTIONS_OVERVIEW: 'nav.functions-overview',
|
||||
NAV_FUNCTIONS_SECRETS: 'nav.functions-secrets',
|
||||
}
|
||||
|
||||
export type FunctionsNavShortcutId =
|
||||
(typeof FUNCTIONS_NAV_SHORTCUT_IDS)[keyof typeof FUNCTIONS_NAV_SHORTCUT_IDS]
|
||||
|
||||
export const functionsNavRegistry: RegistryDefinations<FunctionsNavShortcutId> = {
|
||||
[FUNCTIONS_NAV_SHORTCUT_IDS.NAV_FUNCTIONS_OVERVIEW]: {
|
||||
id: FUNCTIONS_NAV_SHORTCUT_IDS.NAV_FUNCTIONS_OVERVIEW,
|
||||
label: 'Go to Functions',
|
||||
sequence: ['F', 'O'],
|
||||
showInSettings: false,
|
||||
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTIONS,
|
||||
},
|
||||
[FUNCTIONS_NAV_SHORTCUT_IDS.NAV_FUNCTIONS_SECRETS]: {
|
||||
id: FUNCTIONS_NAV_SHORTCUT_IDS.NAV_FUNCTIONS_SECRETS,
|
||||
label: 'Go to Secrets',
|
||||
sequence: ['F', 'K'],
|
||||
showInSettings: false,
|
||||
referenceGroup: SHORTCUT_REFERENCE_GROUPS.NAVIGATION_FUNCTIONS,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { RegistryDefinations } from '../types'
|
||||
|
||||
/**
|
||||
* Shortcuts scoped to the per-function Overview tab (the new flag-gated UI).
|
||||
*
|
||||
* The interval picker uses an `I + <letter>` chord matching the first letter
|
||||
* of the interval's unit (M=minute, H=hour, T=three-hour, D=day).
|
||||
*/
|
||||
export const FUNCTIONS_OVERVIEW_SHORTCUT_IDS = {
|
||||
FUNCTION_OVERVIEW_INTERVAL_15MIN: 'functions-overview.interval-15min',
|
||||
FUNCTION_OVERVIEW_INTERVAL_1HR: 'functions-overview.interval-1hr',
|
||||
FUNCTION_OVERVIEW_INTERVAL_3HR: 'functions-overview.interval-3hr',
|
||||
FUNCTION_OVERVIEW_INTERVAL_1DAY: 'functions-overview.interval-1day',
|
||||
FUNCTION_OVERVIEW_REFRESH: 'functions-overview.refresh',
|
||||
FUNCTION_OVERVIEW_OPEN_LOGS: 'functions-overview.open-logs',
|
||||
}
|
||||
|
||||
export type FunctionsOverviewShortcutId =
|
||||
(typeof FUNCTIONS_OVERVIEW_SHORTCUT_IDS)[keyof typeof FUNCTIONS_OVERVIEW_SHORTCUT_IDS]
|
||||
|
||||
export const functionsOverviewRegistry: RegistryDefinations<FunctionsOverviewShortcutId> = {
|
||||
[FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_15MIN]: {
|
||||
id: FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_15MIN,
|
||||
label: 'Show last 15 minutes',
|
||||
sequence: ['I', 'M'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
[FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_1HR]: {
|
||||
id: FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_1HR,
|
||||
label: 'Show last hour',
|
||||
sequence: ['I', 'H'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
[FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_3HR]: {
|
||||
id: FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_3HR,
|
||||
label: 'Show last 3 hours',
|
||||
sequence: ['I', 'T'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
[FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_1DAY]: {
|
||||
id: FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_1DAY,
|
||||
label: 'Show last day',
|
||||
sequence: ['I', 'D'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
[FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_REFRESH]: {
|
||||
id: FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_REFRESH,
|
||||
label: 'Refresh function stats',
|
||||
sequence: ['Shift+R'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
[FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_OPEN_LOGS]: {
|
||||
id: FUNCTIONS_OVERVIEW_SHORTCUT_IDS.FUNCTION_OVERVIEW_OPEN_LOGS,
|
||||
label: 'Open logs',
|
||||
sequence: ['O', 'L'],
|
||||
showInSettings: false,
|
||||
options: { ignoreInputs: true, registerInCommandMenu: true },
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user