ContextSearchCommands.tsx152 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import { IS_PLATFORM } from 'common' |
| 4 | import { Auth, EdgeFunctions, Storage } from 'icons' |
| 5 | import { Database } from 'lucide-react' |
| 6 | import { useMemo } from 'react' |
| 7 | import type { ICommand } from 'ui-patterns/CommandMenu' |
| 8 | import { |
| 9 | CommandHeader, |
| 10 | CommandMenuInput, |
| 11 | CommandWrapper, |
| 12 | PageType, |
| 13 | useQuery, |
| 14 | useRegisterCommands, |
| 15 | useRegisterPage, |
| 16 | useSetPage, |
| 17 | } from 'ui-patterns/CommandMenu' |
| 18 | |
| 19 | import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils' |
| 20 | import { ContextSearchResults } from './ContextSearchResults' |
| 21 | import { orderCommandSectionsByPriority } from './ordering' |
| 22 | import type { SearchContextValue } from './SearchContext.types' |
| 23 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 24 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 25 | |
| 26 | interface SearchContextOption { |
| 27 | value: SearchContextValue |
| 28 | label: string |
| 29 | pageName: string |
| 30 | placeholder: string |
| 31 | icon: React.ComponentType<React.SVGProps<SVGSVGElement>> |
| 32 | } |
| 33 | |
| 34 | const SEARCH_CONTEXT_OPTIONS: SearchContextOption[] = [ |
| 35 | { |
| 36 | value: 'database-tables', |
| 37 | label: 'Database Tables', |
| 38 | pageName: 'Search Database Tables', |
| 39 | placeholder: 'Search database tables...', |
| 40 | icon: Database, |
| 41 | }, |
| 42 | { |
| 43 | value: 'auth-policies', |
| 44 | label: 'RLS Policies', |
| 45 | pageName: 'Search RLS Policies', |
| 46 | placeholder: 'Search rls policies...', |
| 47 | icon: Auth, |
| 48 | }, |
| 49 | { |
| 50 | value: 'edge-functions', |
| 51 | label: 'Edge Functions', |
| 52 | pageName: 'Search Edge Functions', |
| 53 | placeholder: 'Search edge functions...', |
| 54 | icon: EdgeFunctions, |
| 55 | }, |
| 56 | { |
| 57 | value: 'storage', |
| 58 | label: 'Storage', |
| 59 | pageName: 'Search Storage', |
| 60 | placeholder: 'Search buckets...', |
| 61 | icon: Storage, |
| 62 | }, |
| 63 | ] |
| 64 | |
| 65 | function ContextSearchPage({ |
| 66 | context, |
| 67 | placeholder, |
| 68 | }: { |
| 69 | context: SearchContextValue |
| 70 | placeholder: string |
| 71 | }) { |
| 72 | const query = useQuery() |
| 73 | |
| 74 | return ( |
| 75 | <CommandWrapper> |
| 76 | <CommandHeader> |
| 77 | <CommandMenuInput placeholder={placeholder} /> |
| 78 | </CommandHeader> |
| 79 | <ContextSearchResults context={context} query={query} /> |
| 80 | </CommandWrapper> |
| 81 | ) |
| 82 | } |
| 83 | |
| 84 | export function useContextSearchCommands() { |
| 85 | const { data: project } = useSelectedProjectQuery() |
| 86 | const setPage = useSetPage() |
| 87 | |
| 88 | const { |
| 89 | projectAuthAll: authEnabled, |
| 90 | projectEdgeFunctionAll: edgeFunctionsEnabled, |
| 91 | projectStorageAll: storageEnabled, |
| 92 | } = useIsFeatureEnabled(['project_auth:all', 'project_edge_function:all', 'project_storage:all']) |
| 93 | |
| 94 | const pageDefinitions = [ |
| 95 | { title: 'Search Database Tables', context: 'database-tables' as const }, |
| 96 | { title: 'Search RLS Policies', context: 'auth-policies' as const }, |
| 97 | { title: 'Search Edge Functions', context: 'edge-functions' as const }, |
| 98 | { title: 'Search Storage', context: 'storage' as const }, |
| 99 | ] |
| 100 | |
| 101 | // Register pages - pageDefinitions is constant, so hooks are called in consistent order |
| 102 | for (const { title, context } of pageDefinitions) { |
| 103 | const placeholder = |
| 104 | SEARCH_CONTEXT_OPTIONS.find((opt) => opt.value === context)?.placeholder ?? '' |
| 105 | // eslint-disable-next-line react-hooks/rules-of-hooks |
| 106 | useRegisterPage(title, { |
| 107 | type: PageType.Component, |
| 108 | component: () => <ContextSearchPage context={context} placeholder={placeholder} />, |
| 109 | }) |
| 110 | } |
| 111 | |
| 112 | const contextCommands = useMemo(() => { |
| 113 | return SEARCH_CONTEXT_OPTIONS.filter((option) => { |
| 114 | let isFeatureEnabled = false |
| 115 | switch (option.value) { |
| 116 | case 'database-tables': |
| 117 | isFeatureEnabled = true |
| 118 | break |
| 119 | case 'auth-policies': |
| 120 | isFeatureEnabled = authEnabled |
| 121 | break |
| 122 | case 'edge-functions': |
| 123 | isFeatureEnabled = edgeFunctionsEnabled |
| 124 | break |
| 125 | case 'storage': |
| 126 | isFeatureEnabled = storageEnabled |
| 127 | break |
| 128 | } |
| 129 | |
| 130 | if (!isFeatureEnabled) return false |
| 131 | |
| 132 | // If self-hosted, show if feature is enabled |
| 133 | if (!IS_PLATFORM) { |
| 134 | return true |
| 135 | } |
| 136 | |
| 137 | // only show when inside a project if not self-hosted |
| 138 | return !!project |
| 139 | }).map((option) => ({ |
| 140 | id: `search-${option.value}`, |
| 141 | name: `Search ${option.label}...`, |
| 142 | action: () => setPage(option.pageName), |
| 143 | icon: () => <option.icon className="h-4 w-4" strokeWidth={1.5} />, |
| 144 | })) as ICommand[] |
| 145 | }, [setPage, authEnabled, edgeFunctionsEnabled, storageEnabled, project]) |
| 146 | |
| 147 | useRegisterCommands(COMMAND_MENU_SECTIONS.QUERY, contextCommands, { |
| 148 | orderSection: orderCommandSectionsByPriority, |
| 149 | sectionMeta: { priority: 3 }, |
| 150 | enabled: !IS_PLATFORM || !!project, |
| 151 | }) |
| 152 | } |