diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx index 6b50bf3dcc5..d94f82a8afe 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx @@ -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" /> - + + + diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.tsx index 55822ec9cf6..14589b9d9b7 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.tsx @@ -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 diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/useEdgeFunctionOverviewShortcuts.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/useEdgeFunctionOverviewShortcuts.ts new file mode 100644 index 00000000000..87d3709b6ff --- /dev/null +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/useEdgeFunctionOverviewShortcuts.ts @@ -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) +} diff --git a/apps/studio/components/interfaces/Functions/useFunctionsDetailShortcuts.ts b/apps/studio/components/interfaces/Functions/useFunctionsDetailShortcuts.ts new file mode 100644 index 00000000000..e7d3cb64d50 --- /dev/null +++ b/apps/studio/components/interfaces/Functions/useFunctionsDetailShortcuts.ts @@ -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 }) +} diff --git a/apps/studio/components/interfaces/Functions/useFunctionsListShortcuts.ts b/apps/studio/components/interfaces/Functions/useFunctionsListShortcuts.ts new file mode 100644 index 00000000000..4d039e2c3c1 --- /dev/null +++ b/apps/studio/components/interfaces/Functions/useFunctionsListShortcuts.ts @@ -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 + setSearch: Dispatch> | ((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, + }) +} diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx index 823aa5f44dc..afcf160c9c5 100644 --- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx +++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx @@ -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 ( @@ -274,7 +308,9 @@ const EdgeFunctionDetailsLayout = ({
{functionUrl} - + + +
- - - - + + + + + + {IS_PLATFORM && ( <> @@ -366,24 +408,14 @@ const EdgeFunctionDetailsLayout = ({ {!!functionSlug && ( - + + )}
diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout.tsx index cd06752a181..4ab6dec77ba 100644 --- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout.tsx +++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout.tsx @@ -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 + return } interface EdgeFunctionsLayoutProps { @@ -45,6 +57,8 @@ const EdgeFunctionsLayout = ({ title, browserTitle, }: PropsWithChildren) => { + const menu = useGenerateEdgeFunctionsMenu() + return ( } isBlocking={false} > + {children} ) diff --git a/apps/studio/components/ui/GlobalShortcuts/ShortcutsReferenceSheet.test.tsx b/apps/studio/components/ui/GlobalShortcuts/ShortcutsReferenceSheet.test.tsx index 43899c02fea..5a4f95a4f65 100644 --- a/apps/studio/components/ui/GlobalShortcuts/ShortcutsReferenceSheet.test.tsx +++ b/apps/studio/components/ui/GlobalShortcuts/ShortcutsReferenceSheet.test.tsx @@ -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() diff --git a/apps/studio/components/ui/GlobalShortcuts/ShortcutsReferenceSheet.tsx b/apps/studio/components/ui/GlobalShortcuts/ShortcutsReferenceSheet.tsx index b8c6fb4b48c..2183923716e 100644 --- a/apps/studio/components/ui/GlobalShortcuts/ShortcutsReferenceSheet.tsx +++ b/apps/studio/components/ui/GlobalShortcuts/ShortcutsReferenceSheet.tsx @@ -46,6 +46,9 @@ const GROUP_LABELS: Record = { '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', diff --git a/apps/studio/pages/project/[ref]/functions/index.tsx b/apps/studio/pages/project/[ref]/functions/index.tsx index 7c67a1f77c9..f62d9ab6215 100644 --- a/apps/studio/pages/project/[ref]/functions/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/index.tsx @@ -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(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 = () => {
- } - size="tiny" - className="w-32 md:w-64" - value={search} - onChange={(event) => setSearch(event.target.value)} - actions={[ - search && ( -
+ + + {search && filteredFunctions.length !== functions.length ? `Viewing ${filteredFunctions.length} of ${functions.length} functions in total` diff --git a/apps/studio/state/shortcuts/referenceGroups.ts b/apps/studio/state/shortcuts/referenceGroups.ts index 4ffcd5f4a59..df3187e4681 100644 --- a/apps/studio/state/shortcuts/referenceGroups.ts +++ b/apps/studio/state/shortcuts/referenceGroups.ts @@ -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 = { @@ -10,6 +12,8 @@ export const SHORTCUT_REFERENCE_GROUP_LABELS: Record = { [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', diff --git a/apps/studio/state/shortcuts/registry.ts b/apps/studio/state/shortcuts/registry.ts index eb7d441fb5d..bc2590ef93f 100644 --- a/apps/studio/state/shortcuts/registry.ts +++ b/apps/studio/state/shortcuts/registry.ts @@ -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 = { ...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, } diff --git a/apps/studio/state/shortcuts/registry/functions-detail-nav.ts b/apps/studio/state/shortcuts/registry/functions-detail-nav.ts new file mode 100644 index 00000000000..4eaf154a8ee --- /dev/null +++ b/apps/studio/state/shortcuts/registry/functions-detail-nav.ts @@ -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//functions//*` 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 = { + [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, + }, +} diff --git a/apps/studio/state/shortcuts/registry/functions-detail.ts b/apps/studio/state/shortcuts/registry/functions-detail.ts new file mode 100644 index 00000000000..d67957600e8 --- /dev/null +++ b/apps/studio/state/shortcuts/registry/functions-detail.ts @@ -0,0 +1,49 @@ +import { RegistryDefinations } from '../types' + +/** + * Shortcuts scoped to a single edge function's detail layout — active anywhere + * under `/project//functions//*`. + * + * 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 = { + [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, + }, +} diff --git a/apps/studio/state/shortcuts/registry/functions-list.ts b/apps/studio/state/shortcuts/registry/functions-list.ts new file mode 100644 index 00000000000..7d5b038ff19 --- /dev/null +++ b/apps/studio/state/shortcuts/registry/functions-list.ts @@ -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 = { + [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 }, + }, +} diff --git a/apps/studio/state/shortcuts/registry/functions-nav.ts b/apps/studio/state/shortcuts/registry/functions-nav.ts new file mode 100644 index 00000000000..ce05a67e2d0 --- /dev/null +++ b/apps/studio/state/shortcuts/registry/functions-nav.ts @@ -0,0 +1,35 @@ +import { SHORTCUT_REFERENCE_GROUPS } from '../referenceGroups' +import { RegistryDefinations } from '../types' + +/** + * Contextual chords for jumping between Edge Functions sub-pages — `F + `. + * + * Active only while EdgeFunctionsLayout is mounted (i.e. the user is somewhere + * under `/project//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 = { + [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, + }, +} diff --git a/apps/studio/state/shortcuts/registry/functions-overview.ts b/apps/studio/state/shortcuts/registry/functions-overview.ts new file mode 100644 index 00000000000..fee6dce1a41 --- /dev/null +++ b/apps/studio/state/shortcuts/registry/functions-overview.ts @@ -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 + ` 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 = { + [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 }, + }, +}