Skip to content

Commit 7d7de47

Browse files
authored
chore: use react-hook-form in logs query modals (supabase#44243)
## Problem - Logs query modals still use `formik` and we want to remove it in favour of `react-hook-form` to keep only one form library ## Solution - Migrate to `react-hook-form` ## Screenshots New query: <img width="566" height="387" alt="image" src="https://github.com/user-attachments/assets/f300a292-88f6-454e-8f86-84a27a8c4892" /> Existing query: <img width="554" height="378" alt="image" src="https://github.com/user-attachments/assets/2333f041-7185-47f6-8fb0-5176b5a0c77b" />
1 parent 81b1c50 commit 7d7de47

3 files changed

Lines changed: 103 additions & 122 deletions

File tree

apps/studio/components/interfaces/Settings/Logs/Logs.SavedQueriesItem.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { useRouter } from 'next/router'
2-
import { useState } from 'react'
3-
import { toast } from 'sonner'
4-
51
import { useParams } from 'common'
62
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
73
import { useContentUpsertMutation } from 'data/content/content-upsert-mutation'
4+
import { SqlEditor } from 'icons'
5+
import { Edit, Trash } from 'lucide-react'
6+
import { useRouter } from 'next/router'
7+
import { useState } from 'react'
8+
import { toast } from 'sonner'
89
import { DropdownMenuItem, DropdownMenuSeparator } from 'ui'
910
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
11+
1012
import { UpdateSavedQueryModal } from './Logs.UpdateSavedQueryModal'
11-
import { Edit, Trash } from 'lucide-react'
12-
import { SqlEditor } from 'icons'
1313
import { LogsSidebarItem } from './SidebarV2/SidebarItem'
1414

1515
interface SavedQueriesItemProps {
@@ -39,7 +39,7 @@ const SavedQueriesItem = ({ item }: SavedQueriesItemProps) => {
3939
toast.error(`Failed to delete saved query: ${error.message}`)
4040
},
4141
})
42-
const { mutate: updateContent } = useContentUpsertMutation({
42+
const { mutateAsync: updateContent } = useContentUpsertMutation({
4343
onSuccess: () => {
4444
setShowUpdateModal(false)
4545
toast.success('Successfully updated query')
@@ -56,7 +56,7 @@ const SavedQueriesItem = ({ item }: SavedQueriesItemProps) => {
5656

5757
const onConfirmUpdate = async ({ name, description }: { name: string; description?: string }) => {
5858
if (!ref || typeof ref !== 'string') return console.error('Invalid project reference')
59-
updateContent({
59+
await updateContent({
6060
projectRef: ref,
6161
payload: {
6262
...item,
@@ -110,14 +110,13 @@ const SavedQueriesItem = ({ item }: SavedQueriesItemProps) => {
110110
</p>
111111
</ConfirmationModal>
112112
<UpdateSavedQueryModal
113+
header="Update saved query"
113114
visible={showUpdateModal}
114115
initialValues={{ name: item.name, description: item.description }}
115116
onCancel={() => {
116117
setShowUpdateModal(false)
117118
}}
118-
onSubmit={(newValues) => {
119-
onConfirmUpdate(newValues)
120-
}}
119+
onSubmit={onConfirmUpdate}
121120
/>
122121
</>
123122
)
Lines changed: 76 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,98 @@
1-
import { Button, Form, Input, Modal } from 'ui'
1+
import { zodResolver } from '@hookform/resolvers/zod'
2+
import { useEffect } from 'react'
3+
import { SubmitHandler, useForm } from 'react-hook-form'
4+
import {
5+
Button,
6+
Form_Shadcn_,
7+
FormControl_Shadcn_,
8+
FormField_Shadcn_,
9+
Input_Shadcn_,
10+
Modal,
11+
Textarea,
12+
} from 'ui'
13+
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
14+
import * as z from 'zod'
215

3-
type SavedQuery = { name: string; description?: string }
16+
const formSchema = z.object({
17+
name: z.string().min(1, 'Required'),
18+
description: z.string().optional(),
19+
})
20+
21+
type SavedQuery = z.infer<typeof formSchema>
422

523
export interface UpdateSavedQueryProps {
24+
header: string
625
visible: boolean
726
onCancel: () => void
8-
onSubmit: (newValues: SavedQuery) => void
27+
onSubmit: SubmitHandler<SavedQuery>
928
initialValues: SavedQuery
1029
}
1130

1231
export const UpdateSavedQueryModal = ({
32+
header,
1333
visible,
1434
onCancel,
1535
onSubmit,
1636
initialValues,
1737
}: UpdateSavedQueryProps) => {
18-
function validate(values: SavedQuery) {
19-
const errors: Partial<SavedQuery> = {}
38+
const form = useForm<SavedQuery>({
39+
resolver: zodResolver(formSchema),
40+
defaultValues: { ...initialValues, description: initialValues.description ?? '' },
41+
})
42+
const { reset, formState } = form
43+
const { isDirty, isSubmitting } = formState
2044

21-
if (!values.name) {
22-
errors.name = 'Required'
23-
}
45+
useEffect(() => {
46+
if (isDirty) return
47+
reset({ ...initialValues, description: initialValues.description ?? '' })
48+
}, [isDirty, initialValues, reset])
2449

25-
return errors
50+
const handleCancel = () => {
51+
form.reset()
52+
onCancel()
2653
}
2754

2855
return (
29-
<Modal
30-
visible={visible}
31-
onCancel={onCancel}
32-
hideFooter
33-
header="Update saved query"
34-
size="medium"
35-
>
36-
<Form
37-
onReset={onCancel}
38-
validateOnBlur
39-
initialValues={initialValues}
40-
validate={validate}
41-
onSubmit={onSubmit}
42-
>
43-
{({ isSubmitting }: { isSubmitting: boolean }) => (
44-
<>
45-
<Modal.Content>
46-
<Input label="Name" id="name" name="name" />
47-
</Modal.Content>
48-
<Modal.Content>
49-
<Input.TextArea
50-
label="Description"
51-
id="description"
52-
placeholder="Describe query"
53-
size="medium"
54-
textAreaClassName="resize-none"
55-
/>
56-
</Modal.Content>
57-
<Modal.Separator />
58-
<Modal.Content className="flex items-center justify-end gap-2">
59-
<Button htmlType="reset" type="default" onClick={onCancel} disabled={isSubmitting}>
60-
Cancel
61-
</Button>
62-
<Button htmlType="submit" loading={isSubmitting} disabled={isSubmitting}>
63-
Save query
64-
</Button>
65-
</Modal.Content>
66-
</>
67-
)}
68-
</Form>
56+
<Modal visible={visible} onCancel={handleCancel} hideFooter header={header} size="medium">
57+
<Form_Shadcn_ {...form}>
58+
<form onSubmit={form.handleSubmit(onSubmit)} noValidate>
59+
<Modal.Content>
60+
<FormField_Shadcn_
61+
control={form.control}
62+
name="name"
63+
render={({ field }) => (
64+
<FormItemLayout layout="vertical" label="Name">
65+
<FormControl_Shadcn_>
66+
<Input_Shadcn_ {...field} placeholder="Enter text" />
67+
</FormControl_Shadcn_>
68+
</FormItemLayout>
69+
)}
70+
/>
71+
</Modal.Content>
72+
<Modal.Content>
73+
<FormField_Shadcn_
74+
control={form.control}
75+
name="description"
76+
render={({ field }) => (
77+
<FormItemLayout layout="vertical" label="Description">
78+
<FormControl_Shadcn_>
79+
<Textarea {...field} placeholder="Describe query" className="resize-none" />
80+
</FormControl_Shadcn_>
81+
</FormItemLayout>
82+
)}
83+
/>
84+
</Modal.Content>
85+
<Modal.Separator />
86+
<Modal.Content className="flex items-center justify-end gap-2">
87+
<Button htmlType="reset" type="default" onClick={handleCancel} disabled={isSubmitting}>
88+
Cancel
89+
</Button>
90+
<Button htmlType="submit" loading={isSubmitting} disabled={isSubmitting || !isDirty}>
91+
Save query
92+
</Button>
93+
</Modal.Content>
94+
</form>
95+
</Form_Shadcn_>
6996
</Modal>
7097
)
7198
}

apps/studio/pages/project/[ref]/logs/explorer/index.tsx

Lines changed: 17 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,9 @@ import { useRouter } from 'next/router'
4545
import { useEffect, useMemo, useRef, useState } from 'react'
4646
import { toast } from 'sonner'
4747
import type { LogSqlSnippets, NextPageWithLayout } from 'types'
48-
import {
49-
Button,
50-
Form,
51-
Input,
52-
Modal,
53-
ResizableHandle,
54-
ResizablePanel,
55-
ResizablePanelGroup,
56-
} from 'ui'
48+
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
49+
50+
import { UpdateSavedQueryModal } from '@/components/interfaces/Settings/Logs/Logs.UpdateSavedQueryModal'
5751

5852
const LOCAL_PLACEHOLDER_QUERY =
5953
'select\n timestamp, event_message, metadata\n from edge_logs limit 5'
@@ -148,7 +142,7 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
148142
const results = logData
149143
const isLoading = logsLoading
150144

151-
const { mutate: upsertContent, isPending: isUpsertingContent } = useContentUpsertMutation({
145+
const { mutateAsync: upsertContent, isPending: isUpsertingContent } = useContentUpsertMutation({
152146
onError: (e) => {
153147
const error = e as { message: string }
154148
console.error(error)
@@ -254,13 +248,9 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
254248

255249
type SaveQueryFormValues = { name: string; description?: string }
256250

257-
const handleCreateQuery = async (
258-
values: SaveQueryFormValues,
259-
{ setSubmitting }: { setSubmitting: (value: boolean) => void }
260-
) => {
251+
const handleCreateQuery = async (values: SaveQueryFormValues) => {
261252
if (!projectRef) return console.error('Project ref is required')
262253
if (!profile) return console.error('Profile is required')
263-
setSubmitting(true)
264254

265255
const id = uuidv4()
266256
const payload: UpsertContentPayload = {
@@ -277,20 +267,20 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
277267
owner_id: profile.id,
278268
visibility: 'user' as const,
279269
}
280-
upsertContent(
270+
await upsertContent(
281271
{ projectRef, payload },
282272
{
283273
onSuccess: () => router.push(`/project/${projectRef}/logs/explorer?queryId=${id}`),
284274
}
285275
)
286276
}
287277

288-
function handleOnSave() {
278+
async function handleOnSave() {
289279
if (!projectRef) return console.error('Project ref is required')
290280

291281
// if we have a queryId, we are editing a saved query
292282
if (queryId && query) {
293-
upsertContent({
283+
await upsertContent({
294284
projectRef: projectRef!,
295285
payload: {
296286
...query,
@@ -392,6 +382,8 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
392382
/>
393383
<ShimmerLine active={isLoading} />
394384
<CodeEditor
385+
// Ensure we reset the editor to the query content whenever the selected query changes
386+
key={queryId}
395387
id={editorId}
396388
editorRef={editorRef}
397389
language="pgsql"
@@ -424,52 +416,15 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
424416
</ResizablePanel>
425417
</ResizablePanelGroup>
426418

427-
<Modal
428-
size="medium"
429-
onCancel={() => setSaveModalOpen(!saveModalOpen)}
419+
<UpdateSavedQueryModal
430420
header="Save log query"
431421
visible={saveModalOpen}
432-
hideFooter
433-
>
434-
<Form
435-
initialValues={{
436-
name: '',
437-
description: '',
438-
}}
439-
onSubmit={handleCreateQuery}
440-
>
441-
{() => (
442-
<>
443-
<Modal.Content className="space-y-6">
444-
<Input layout="horizontal" label="Name" id="name" />
445-
<div className="text-area-text-sm">
446-
<Input.TextArea
447-
layout="horizontal"
448-
labelOptional="Optional"
449-
label="Description"
450-
id="description"
451-
rows={2}
452-
/>
453-
</div>
454-
</Modal.Content>
455-
<Modal.Separator />
456-
<Modal.Content className="flex items-center justify-end gap-2">
457-
<Button size="tiny" type="default" onClick={() => setSaveModalOpen(!saveModalOpen)}>
458-
Cancel
459-
</Button>
460-
<Button
461-
size="tiny"
462-
loading={isUpsertingContent}
463-
disabled={isUpsertingContent}
464-
htmlType="submit"
465-
>
466-
Save
467-
</Button>
468-
</Modal.Content>
469-
</>
470-
)}
471-
</Form>
472-
</Modal>
422+
initialValues={{ name: '', description: '' }}
423+
onCancel={() => {
424+
setSaveModalOpen(false)
425+
}}
426+
onSubmit={handleCreateQuery}
427+
/>
473428
</div>
474429
)
475430
}

0 commit comments

Comments
 (0)