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