CreateReportModal.tsx156 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { useRouter } from 'next/router'
3import { useMemo } from 'react'
4import { SubmitHandler, useForm } from 'react-hook-form'
5import { toast } from 'sonner'
6import { Button, Form, FormControl, FormField, Input, Modal, Textarea } from 'ui'
7import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
8import * as z from 'zod'
9
10import { useContentUpsertMutation } from '@/data/content/content-upsert-mutation'
11import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
12import { uuidv4 } from '@/lib/helpers'
13import { useProfile } from '@/lib/profile'
14
15export interface CreateReportModal {
16 visible: boolean
17 onCancel: () => void
18 afterSubmit: () => void
19}
20
21const formSchema = z.object({
22 name: z.string().min(1, 'Required'),
23 description: z.string().optional(),
24})
25
26type CustomReport = z.infer<typeof formSchema>
27
28export const CreateReportModal = ({ visible, onCancel, afterSubmit }: CreateReportModal) => {
29 const router = useRouter()
30 const { profile } = useProfile()
31 const { data: project } = useSelectedProjectQuery()
32 const ref = project?.ref ?? 'default'
33
34 // Preserve date range query parameters when navigating to new report
35 const preservedQueryParams = useMemo(() => {
36 const { its, ite, isHelper, helperText } = router.query
37 const params = new URLSearchParams()
38
39 if (its && typeof its === 'string') params.set('its', its)
40 if (ite && typeof ite === 'string') params.set('ite', ite)
41 if (isHelper && typeof isHelper === 'string') params.set('isHelper', isHelper)
42 if (helperText && typeof helperText === 'string') params.set('helperText', helperText)
43
44 const queryString = params.toString()
45 return queryString ? `?${queryString}` : ''
46 }, [router.query])
47
48 const { mutate: upsertContent, isPending: isCreating } = useContentUpsertMutation({
49 onSuccess: (_, vars) => {
50 toast.success('Successfully created new report')
51 const newReportId = vars.payload.id
52 router.push(`/project/${ref}/observability/${newReportId}${preservedQueryParams}`)
53 afterSubmit()
54 },
55 onError: (error) => {
56 toast.error(`Failed to create report: ${error.message}`)
57 },
58 })
59
60 const createCustomReport: SubmitHandler<CustomReport> = async ({ name, description }) => {
61 if (!ref) return console.error('Project ref is required')
62 if (!profile) return console.error('Profile is required')
63
64 upsertContent({
65 projectRef: ref,
66 payload: {
67 id: uuidv4(),
68 type: 'report',
69 name,
70 description: description || '',
71 visibility: 'project',
72 owner_id: profile?.id,
73 content: {
74 schema_version: 1,
75 period_start: {
76 time_period: '7d',
77 date: '',
78 },
79 period_end: {
80 time_period: 'today',
81 date: '',
82 },
83 interval: '1d',
84 layout: [],
85 },
86 },
87 })
88 }
89
90 const handleCancel = () => {
91 onCancel()
92 form.reset()
93 }
94
95 const form = useForm<CustomReport>({
96 resolver: zodResolver(formSchema as any),
97 defaultValues: { name: '', description: '' },
98 })
99 const { isDirty } = form.formState
100
101 return (
102 <Modal
103 visible={visible}
104 onCancel={handleCancel}
105 hideFooter
106 header="Create a custom report"
107 size="small"
108 >
109 <Form {...form}>
110 <form onSubmit={form.handleSubmit(createCustomReport)} noValidate>
111 <Modal.Content>
112 <FormField
113 control={form.control}
114 name="name"
115 render={({ field }) => (
116 <FormItemLayout name="name" layout="vertical" label="Name">
117 <FormControl>
118 <Input {...field} id="name" />
119 </FormControl>
120 </FormItemLayout>
121 )}
122 />
123 </Modal.Content>
124 <Modal.Content>
125 <FormField
126 control={form.control}
127 name="description"
128 render={({ field }) => (
129 <FormItemLayout name="description" layout="vertical" label="Description">
130 <FormControl>
131 <Textarea
132 {...field}
133 id="description"
134 rows={4}
135 placeholder="Describe your custom report"
136 className="resize-none"
137 />
138 </FormControl>
139 </FormItemLayout>
140 )}
141 />
142 </Modal.Content>
143 <Modal.Separator />
144 <Modal.Content className="flex items-center justify-end gap-2">
145 <Button htmlType="reset" type="default" onClick={handleCancel} disabled={isCreating}>
146 Cancel
147 </Button>
148 <Button htmlType="submit" loading={isCreating} disabled={isCreating || !isDirty}>
149 Create report
150 </Button>
151 </Modal.Content>
152 </form>
153 </Form>
154 </Modal>
155 )
156}