CreateReportModal.tsx156 lines · main
| 1 | import { zodResolver } from '@hookform/resolvers/zod' |
| 2 | import { useRouter } from 'next/router' |
| 3 | import { useMemo } from 'react' |
| 4 | import { SubmitHandler, useForm } from 'react-hook-form' |
| 5 | import { toast } from 'sonner' |
| 6 | import { Button, Form, FormControl, FormField, Input, Modal, Textarea } from 'ui' |
| 7 | import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' |
| 8 | import * as z from 'zod' |
| 9 | |
| 10 | import { useContentUpsertMutation } from '@/data/content/content-upsert-mutation' |
| 11 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 12 | import { uuidv4 } from '@/lib/helpers' |
| 13 | import { useProfile } from '@/lib/profile' |
| 14 | |
| 15 | export interface CreateReportModal { |
| 16 | visible: boolean |
| 17 | onCancel: () => void |
| 18 | afterSubmit: () => void |
| 19 | } |
| 20 | |
| 21 | const formSchema = z.object({ |
| 22 | name: z.string().min(1, 'Required'), |
| 23 | description: z.string().optional(), |
| 24 | }) |
| 25 | |
| 26 | type CustomReport = z.infer<typeof formSchema> |
| 27 | |
| 28 | export 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 | } |