SQLEditorMenu.tsx169 lines · main
| 1 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 2 | import { useDebounce } from '@uidotdev/usehooks' |
| 3 | import { LOCAL_STORAGE_KEYS, useParams } from 'common' |
| 4 | import { FilePlus, FolderPlus, Plus, X } from 'lucide-react' |
| 5 | import { useRouter } from 'next/router' |
| 6 | import { useEffect, useState } from 'react' |
| 7 | import { toast } from 'sonner' |
| 8 | import { |
| 9 | Button, |
| 10 | DropdownMenu, |
| 11 | DropdownMenuContent, |
| 12 | DropdownMenuItem, |
| 13 | DropdownMenuTrigger, |
| 14 | Tooltip, |
| 15 | TooltipContent, |
| 16 | TooltipTrigger, |
| 17 | } from 'ui' |
| 18 | import { |
| 19 | InnerSideBarFilters, |
| 20 | InnerSideBarFilterSearchInput, |
| 21 | InnerSideBarFilterSortDropdown, |
| 22 | InnerSideBarFilterSortDropdownItem, |
| 23 | } from 'ui-patterns/InnerSideMenu' |
| 24 | |
| 25 | import { SearchList } from './SQLEditorNavV2/SearchList' |
| 26 | import { SQLEditorNav } from './SQLEditorNavV2/SQLEditorNav' |
| 27 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 28 | import { useLocalStorage } from '@/hooks/misc/useLocalStorage' |
| 29 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 30 | import { useProfile } from '@/lib/profile' |
| 31 | import { getAppStateSnapshot } from '@/state/app-state' |
| 32 | import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2' |
| 33 | |
| 34 | export const SQLEditorMenu = () => { |
| 35 | const router = useRouter() |
| 36 | const { ref } = useParams() |
| 37 | const { profile } = useProfile() |
| 38 | const { data: project } = useSelectedProjectQuery() |
| 39 | const snapV2 = useSqlEditorV2StateSnapshot() |
| 40 | |
| 41 | const [search, setSearch] = useState('') |
| 42 | const [showSearch, setShowSearch] = useState(false) |
| 43 | const [sort, setSort] = useLocalStorage<'name' | 'inserted_at'>( |
| 44 | LOCAL_STORAGE_KEYS.SQL_EDITOR_SORT(ref ?? ''), |
| 45 | 'inserted_at' |
| 46 | ) |
| 47 | |
| 48 | const appState = getAppStateSnapshot() |
| 49 | const debouncedSearch = useDebounce(search, 500) |
| 50 | |
| 51 | const { can: canCreateSQLSnippet } = useAsyncCheckPermissions( |
| 52 | PermissionAction.CREATE, |
| 53 | 'user_content', |
| 54 | { |
| 55 | resource: { type: 'sql', owner_id: profile?.id }, |
| 56 | subject: { id: profile?.id }, |
| 57 | } |
| 58 | ) |
| 59 | |
| 60 | const createNewFolder = () => { |
| 61 | if (!ref) return console.error('Project ref is required') |
| 62 | setSearch('') |
| 63 | setShowSearch(false) |
| 64 | snapV2.addNewFolder({ projectRef: ref }) |
| 65 | } |
| 66 | |
| 67 | const handleNewQuery = async () => { |
| 68 | if (!ref) return console.error('Project ref is required') |
| 69 | if (!project) return console.error('Project is required') |
| 70 | if (!profile) return console.error('Profile is required') |
| 71 | if (!canCreateSQLSnippet) { |
| 72 | return toast('Your queries will not be saved as you do not have sufficient permissions') |
| 73 | } |
| 74 | try { |
| 75 | router.push(`/project/${ref}/sql/new?skip=true`) |
| 76 | setSearch('') |
| 77 | setShowSearch(false) |
| 78 | } catch (error: any) { |
| 79 | toast.error(`Failed to create new query: ${error.message}`) |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | useEffect(() => { |
| 84 | setShowSearch(debouncedSearch.length > 0) |
| 85 | }, [debouncedSearch]) |
| 86 | |
| 87 | return ( |
| 88 | <div className="h-full flex flex-col justify-between"> |
| 89 | <div className="flex flex-col gap-y-4 grow"> |
| 90 | <div className="mt-4 mx-4 flex items-center justify-between gap-x-2"> |
| 91 | <InnerSideBarFilters className="w-full p-0 gap-0"> |
| 92 | <InnerSideBarFilterSearchInput |
| 93 | name="search-queries" |
| 94 | placeholder="Search queries..." |
| 95 | aria-labelledby="Search queries" |
| 96 | value={search} |
| 97 | onChange={(e) => { |
| 98 | const value = e.target.value |
| 99 | setSearch(value) |
| 100 | if (value.length === 0) setShowSearch(false) |
| 101 | }} |
| 102 | onKeyDown={(e) => { |
| 103 | if (e.code === 'Escape') { |
| 104 | setSearch('') |
| 105 | setShowSearch(false) |
| 106 | } |
| 107 | }} |
| 108 | > |
| 109 | {showSearch ? ( |
| 110 | <Tooltip> |
| 111 | <TooltipTrigger |
| 112 | className="absolute right-1 top-[.4rem] md:top-[.3rem] transition-colors text-foreground-light hover:text-foreground" |
| 113 | onClick={() => { |
| 114 | setSearch('') |
| 115 | setShowSearch(false) |
| 116 | }} |
| 117 | > |
| 118 | <X size={18} /> |
| 119 | </TooltipTrigger> |
| 120 | <TooltipContent>Clear search</TooltipContent> |
| 121 | </Tooltip> |
| 122 | ) : ( |
| 123 | <InnerSideBarFilterSortDropdown |
| 124 | value={sort} |
| 125 | onValueChange={(value: any) => setSort(value)} |
| 126 | > |
| 127 | <InnerSideBarFilterSortDropdownItem key="name" value="name"> |
| 128 | Alphabetical |
| 129 | </InnerSideBarFilterSortDropdownItem> |
| 130 | <InnerSideBarFilterSortDropdownItem key="inserted_at" value="inserted_at"> |
| 131 | Created At |
| 132 | </InnerSideBarFilterSortDropdownItem> |
| 133 | </InnerSideBarFilterSortDropdown> |
| 134 | )} |
| 135 | </InnerSideBarFilterSearchInput> |
| 136 | </InnerSideBarFilters> |
| 137 | <DropdownMenu> |
| 138 | <DropdownMenuTrigger asChild> |
| 139 | <Button |
| 140 | data-testid="sql-editor-new-query-button" |
| 141 | type="default" |
| 142 | icon={<Plus className="text-foreground" />} |
| 143 | className="w-[26px]" |
| 144 | /> |
| 145 | </DropdownMenuTrigger> |
| 146 | <DropdownMenuContent align="end" side="bottom" className="w-48"> |
| 147 | <DropdownMenuItem className="gap-x-2" onClick={() => handleNewQuery()}> |
| 148 | <FilePlus size={14} /> |
| 149 | Create a new snippet |
| 150 | </DropdownMenuItem> |
| 151 | <DropdownMenuItem className="gap-x-2" onClick={() => createNewFolder()}> |
| 152 | <FolderPlus size={14} /> |
| 153 | Create a new folder |
| 154 | </DropdownMenuItem> |
| 155 | </DropdownMenuContent> |
| 156 | </DropdownMenu> |
| 157 | </div> |
| 158 | |
| 159 | {showSearch ? <SearchList search={debouncedSearch} /> : <SQLEditorNav sort={sort} />} |
| 160 | </div> |
| 161 | |
| 162 | <div className="p-4 border-t sticky bottom-0 bg-studio"> |
| 163 | <Button block type="default" onClick={() => appState.setOnGoingQueriesPanelOpen(true)}> |
| 164 | View running queries |
| 165 | </Button> |
| 166 | </div> |
| 167 | </div> |
| 168 | ) |
| 169 | } |