SQLEditorMenu.tsx 6.0 KB

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