NewTab.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. // @ts-nocheck
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { useParams } from 'common'
  4. import { partition } from 'lodash'
  5. import { Table2 } from 'lucide-react'
  6. import Link from 'next/link'
  7. import { useRouter } from 'next/router'
  8. import { toast } from 'sonner'
  9. import {
  10. Badge,
  11. Button,
  12. Card,
  13. CardContent,
  14. CardHeader,
  15. CardTitle,
  16. cn,
  17. SQL_ICON,
  18. Tabs_Shadcn_,
  19. TabsContent_Shadcn_,
  20. TabsList_Shadcn_,
  21. TabsTrigger_Shadcn_,
  22. } from 'ui'
  23. import { useEditorType } from '../editors/EditorsLayout.hooks'
  24. import { ActionCard } from './ActionCard'
  25. import { RecentItems } from './RecentItems'
  26. import { SQL_TEMPLATES } from '@/components/interfaces/SQLEditor/SQLEditor.queries'
  27. import { createSqlSnippetSkeletonV2 } from '@/components/interfaces/SQLEditor/SQLEditor.utils'
  28. import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
  29. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  30. import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
  31. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  32. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  33. import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
  34. import { useProfile } from '@/lib/profile'
  35. import {
  36. useImpersonatedAAL,
  37. useImpersonatedExternalAuth,
  38. useImpersonatedUser,
  39. useIsImpersonatingAnon,
  40. useRoleImpersonationStateSnapshot,
  41. } from '@/state/role-impersonation-state'
  42. import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
  43. import { useTableEditorStateSnapshot } from '@/state/table-editor'
  44. import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
  45. import { type ResponseError } from '@/types'
  46. export function NewTab() {
  47. const router = useRouter()
  48. const { ref } = useParams()
  49. const editor = useEditorType()
  50. const { profile } = useProfile()
  51. const { data: org } = useSelectedOrganizationQuery()
  52. const { data: project } = useSelectedProjectQuery()
  53. const { selectedSchema } = useQuerySchemaState()
  54. const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
  55. const snap = useTableEditorStateSnapshot()
  56. const snapV2 = useSqlEditorV2StateSnapshot()
  57. const tabs = useTabsStateSnapshot()
  58. const [templates] = partition(SQL_TEMPLATES, { type: 'template' })
  59. const [quickstarts] = partition(SQL_TEMPLATES, { type: 'quickstart' })
  60. const roleState = useRoleImpersonationStateSnapshot()
  61. const impersonatingAnon = useIsImpersonatingAnon()
  62. const impersonatedUser = useImpersonatedUser()
  63. const impersonatedExternalUser = useImpersonatedExternalAuth()
  64. const impersonatedAAL = useImpersonatedAAL()
  65. const { mutate: sendEvent } = useSendEventMutation()
  66. const { can: canCreateSQLSnippet } = useAsyncCheckPermissions(
  67. PermissionAction.CREATE,
  68. 'user_content',
  69. {
  70. resource: { type: 'sql', owner_id: profile?.id },
  71. subject: { id: profile?.id },
  72. }
  73. )
  74. const tableEditorActions = isSchemaLocked
  75. ? []
  76. : [
  77. {
  78. icon: <Table2 className="h-4 w-4 text-foreground" strokeWidth={1.5} />,
  79. title: 'Create a table',
  80. description: 'Design and create a new database table',
  81. bgColor: 'bg-blue-500',
  82. isBeta: false,
  83. onClick: () => snap.onAddTable(),
  84. },
  85. ]
  86. const sqlEditorActions = [
  87. {
  88. icon: <SQL_ICON className={cn('fill-foreground', 'w-4 h-4')} strokeWidth={1.5} />,
  89. title: 'New SQL Snippet',
  90. description: 'Execute SQL queries',
  91. bgColor: 'bg-green-500',
  92. isBeta: false,
  93. onClick: () => router.push(`/project/${ref}/sql/new`),
  94. },
  95. ]
  96. const actions = editor === 'sql' ? sqlEditorActions : tableEditorActions
  97. const handleNewQuery = async (sql: string, name: string) => {
  98. if (!ref) return console.error('Project ref is required')
  99. if (!project) return console.error('Project is required')
  100. if (!profile) return console.error('Profile is required')
  101. if (!canCreateSQLSnippet) {
  102. return toast('Your queries will not be saved as you do not have sufficient permissions')
  103. }
  104. try {
  105. const snippet = createSqlSnippetSkeletonV2({
  106. name,
  107. sql,
  108. owner_id: profile?.id,
  109. project_id: project?.id,
  110. })
  111. snapV2.addSnippet({ projectRef: ref, snippet })
  112. snapV2.addNeedsSaving(snippet.id)
  113. const tabId = createTabId('sql', { id: snippet.id })
  114. tabs.addTab({
  115. id: tabId,
  116. type: 'sql',
  117. label: name,
  118. metadata: { sqlId: snippet.id },
  119. })
  120. router.push(`/project/${ref}/sql/${snippet.id}`)
  121. } catch (error) {
  122. toast.error(`Failed to create new query: ${(error as ResponseError).message}`)
  123. }
  124. }
  125. return (
  126. <div className="bg-surface-100 h-full overflow-y-auto py-12">
  127. <div className="mx-auto max-w-2xl flex flex-col gap-10 px-10">
  128. {(!!impersonatedUser || !!impersonatedExternalUser || impersonatingAnon) && (
  129. <Card>
  130. <CardHeader className="py-2 px-3 flex-row items-center justify-between w-full space-y-0">
  131. <CardTitle className="text-foreground-light">Currently impersonating as</CardTitle>
  132. <Button
  133. type="default"
  134. className="font-sans"
  135. onClick={() => roleState.setRole(undefined)}
  136. >
  137. Stop
  138. </Button>
  139. </CardHeader>
  140. <CardContent className="py-2 px-3 text-sm flex items-center justify-between">
  141. <div className="flex items-center gap-x-2">
  142. {impersonatingAnon ? (
  143. <p>Anonymous</p>
  144. ) : (
  145. <p>{impersonatedUser?.email ?? impersonatedExternalUser}</p>
  146. )}
  147. {impersonatedAAL && <Badge>{impersonatedAAL.toUpperCase()}</Badge>}
  148. </div>
  149. {impersonatingAnon && <p className="text-foreground-lighter">Not logged-in</p>}
  150. {!!impersonatedUser && (
  151. <p>
  152. ID: <code className="text-code-inline">{impersonatedUser.id}</code>
  153. </p>
  154. )}
  155. </CardContent>
  156. </Card>
  157. )}
  158. <div className="grid grid-cols-2 gap-4">
  159. {actions.map((item, i) => (
  160. <ActionCard key={`action-card-${i}`} {...item} />
  161. ))}
  162. </div>
  163. <RecentItems />
  164. </div>
  165. {editor === 'sql' && (
  166. <div className="flex flex-col gap-4 mx-auto py-10">
  167. <Tabs_Shadcn_ defaultValue="templates">
  168. <TabsList_Shadcn_ className="mx-auto justify-center gap-5">
  169. <TabsTrigger_Shadcn_ value="templates">Templates</TabsTrigger_Shadcn_>
  170. <TabsTrigger_Shadcn_ value="quickstarts">Quickstarts</TabsTrigger_Shadcn_>
  171. </TabsList_Shadcn_>
  172. <TabsContent_Shadcn_ value="templates" className="max-w-5xl mx-auto py-5">
  173. <div className="grid grid-cols-3 gap-4 px-8">
  174. {templates.slice(0, 9).map((item, i) => (
  175. <ActionCard
  176. onClick={() => {
  177. handleNewQuery(item.sql, item.title)
  178. sendEvent({
  179. action: 'sql_editor_template_clicked',
  180. properties: { templateName: item.title },
  181. groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
  182. })
  183. }}
  184. bgColor="bg-alternative border"
  185. key={`action-card-${i}`}
  186. {...item}
  187. icon={
  188. <SQL_ICON className={cn('fill-foreground', 'w-4 h-4')} strokeWidth={1.5} />
  189. }
  190. />
  191. ))}
  192. </div>
  193. <div className="flex justify-center mt-5">
  194. <Button asChild type="default">
  195. <Link href={`/project/${ref}/sql/templates`}>View more templates</Link>
  196. </Button>
  197. </div>
  198. </TabsContent_Shadcn_>
  199. <TabsContent_Shadcn_ value="quickstarts" className="max-w-5xl mx-auto py-5">
  200. <div className="grid grid-cols-3 gap-4 px-8">
  201. {quickstarts.map((item, i) => (
  202. <ActionCard
  203. onClick={() => {
  204. handleNewQuery(item.sql, item.title)
  205. sendEvent({
  206. action: 'sql_editor_quickstart_clicked',
  207. properties: { quickstartName: item.title },
  208. groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
  209. })
  210. }}
  211. bgColor="bg-alternative border"
  212. key={`action-card-${i}`}
  213. {...item}
  214. icon={
  215. <SQL_ICON className={cn('fill-foreground', 'w-4 h-4')} strokeWidth={1.5} />
  216. }
  217. />
  218. ))}
  219. </div>
  220. <div className="flex justify-center mt-5">
  221. <Button asChild type="default">
  222. <Link href={`/project/${ref}/sql/quickstarts`}>View more templates</Link>
  223. </Button>
  224. </div>
  225. </TabsContent_Shadcn_>
  226. </Tabs_Shadcn_>
  227. </div>
  228. )}
  229. </div>
  230. )
  231. }