SQLEditorMenu.tsx169 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useDebounce } from '@uidotdev/usehooks'
3import { LOCAL_STORAGE_KEYS, useParams } from 'common'
4import { FilePlus, FolderPlus, Plus, X } from 'lucide-react'
5import { useRouter } from 'next/router'
6import { useEffect, useState } from 'react'
7import { toast } from 'sonner'
8import {
9 Button,
10 DropdownMenu,
11 DropdownMenuContent,
12 DropdownMenuItem,
13 DropdownMenuTrigger,
14 Tooltip,
15 TooltipContent,
16 TooltipTrigger,
17} from 'ui'
18import {
19 InnerSideBarFilters,
20 InnerSideBarFilterSearchInput,
21 InnerSideBarFilterSortDropdown,
22 InnerSideBarFilterSortDropdownItem,
23} from 'ui-patterns/InnerSideMenu'
24
25import { SearchList } from './SQLEditorNavV2/SearchList'
26import { SQLEditorNav } from './SQLEditorNavV2/SQLEditorNav'
27import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
28import { useLocalStorage } from '@/hooks/misc/useLocalStorage'
29import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
30import { useProfile } from '@/lib/profile'
31import { getAppStateSnapshot } from '@/state/app-state'
32import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
33
34export 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}