CreateRuleSheet.tsx247 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { useParams } from 'common'
3import { useRouter } from 'next/router'
4import { useEffect } from 'react'
5import { SubmitHandler, useForm } from 'react-hook-form'
6import { toast } from 'sonner'
7import {
8 Button,
9 Form,
10 FormControl,
11 FormField,
12 Select,
13 SelectContent,
14 SelectItem,
15 SelectTrigger,
16 SelectValue,
17 Separator,
18 Sheet,
19 SheetContent,
20 SheetFooter,
21 SheetHeader,
22 SheetSection,
23 SheetTitle,
24 Switch,
25 TextArea,
26 Tooltip,
27 TooltipContent,
28 TooltipTrigger,
29} from 'ui'
30import { Admonition } from 'ui-patterns'
31import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
32import * as z from 'zod'
33
34import { LintInfo } from '../Linter/Linter.constants'
35import { lintInfoMap } from '../Linter/Linter.utils'
36import { generateRuleDescription } from './AdvisorRules.utils'
37import { useLintRuleCreateMutation } from '@/data/lint/create-lint-rule-mutation'
38import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query'
39import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
40
41interface CreateRuleSheetProps {
42 lint?: LintInfo
43 open: boolean
44 onOpenChange: (value: boolean) => void
45}
46
47const FormSchema = z.object({
48 lint_name: z.string().optional(),
49 note: z.string().optional(),
50 assigned_to: z.string().optional(),
51 is_disabled: z.boolean(),
52})
53
54const defaultValues = {
55 lint_name: undefined,
56 note: undefined,
57 assigned_to: 'all',
58 is_disabled: true,
59}
60
61/**
62 * [Joshen] JFYI while the API supports adding rules on a category, I'm intentionally leaving that functionality out for now
63 * as the only use case for that would be to ignore _all_ lints in that category (which I'm not sure if that's what we want to advise users doing atm)
64 *
65 * (Spoken with Hieu) We'll eventually support granularity of Entity/Items as well, just not atm
66 */
67export const CreateRuleSheet = ({ lint, open, onOpenChange }: CreateRuleSheetProps) => {
68 const router = useRouter()
69 const { ref: projectRef } = useParams()
70
71 const routeCategory = router.pathname.split('/').pop()
72 const { data: organization } = useSelectedOrganizationQuery()
73 const { data: members = [] } = useOrganizationMembersQuery({ slug: organization?.slug })
74
75 const { mutate: createRule, isPending: isCreating } = useLintRuleCreateMutation({
76 onSuccess: (_, vars) => {
77 const ruleLint = vars.exception.lint_name
78 const ruleLintMeta = lintInfoMap.find((x) => x.name === ruleLint)
79 toast.success(`Successfully created new rule for ${ruleLintMeta?.title}`)
80
81 if (ruleLintMeta) {
82 if (!!routeCategory && routeCategory !== ruleLintMeta.category) {
83 router.push(
84 `/project/${projectRef}/advisors/rules/${ruleLintMeta.category}?lint=${ruleLintMeta.name}`
85 )
86 } else {
87 // setExpandedLint(ruleLintMeta?.name)
88 }
89 }
90 onOpenChange(false)
91 },
92 })
93
94 const formId = 'create-lint-rule-form'
95 const form = useForm<z.infer<typeof FormSchema>>({
96 mode: 'onBlur',
97 reValidateMode: 'onChange',
98 resolver: zodResolver(FormSchema as any),
99 defaultValues,
100 })
101
102 const { lint_name, assigned_to, is_disabled } = form.watch()
103
104 const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
105 if (!projectRef) return console.error('Project ref is required')
106
107 createRule({
108 projectRef,
109 exception: {
110 ...values,
111 lint_category: undefined,
112 lint_name: values.lint_name,
113 assigned_to: values.assigned_to === 'all' ? undefined : values.assigned_to,
114 },
115 })
116 }
117
118 useEffect(() => {
119 if (open) form.reset({ ...defaultValues, lint_name: lint?.name })
120 }, [open])
121
122 return (
123 <Sheet open={open} onOpenChange={onOpenChange}>
124 <SheetContent className="flex flex-col gap-0">
125 <SheetHeader className="shrink-0 flex items-center gap-4">
126 <SheetTitle>Create a rule for "{lint?.title}"</SheetTitle>
127 </SheetHeader>
128 <SheetSection className="overflow-auto grow px-0">
129 <Form {...form}>
130 <form
131 id={formId}
132 className="flex flex-col gap-y-4"
133 onSubmit={form.handleSubmit(onSubmit)}
134 >
135 <FormField
136 name="is_disabled"
137 control={form.control}
138 render={({ field }) => (
139 <FormItemLayout
140 layout="flex-row-reverse"
141 className="px-5"
142 label={`Disable this lint for ${assigned_to === 'all' ? 'project' : 'the assigned member'}`}
143 description="Toggles the visiblity of this lint in the Advisor reports"
144 >
145 <Tooltip>
146 <TooltipTrigger type="button">
147 <FormControl>
148 <Switch
149 checked={field.value}
150 onCheckedChange={field.onChange}
151 disabled={field.disabled || assigned_to === 'all'}
152 />
153 </FormControl>
154 </TooltipTrigger>
155 {assigned_to === 'all' && (
156 <TooltipContent side="bottom" className="w-72">
157 Assign this rule to a specific project member before toggling this option
158 off. This will then configure the rule to{' '}
159 <span className="text-brand">only be visible</span> to that member in the
160 advisor reports.
161 </TooltipContent>
162 )}
163 </Tooltip>
164 </FormItemLayout>
165 )}
166 />
167
168 <Separator />
169
170 <FormField
171 name="assigned_to"
172 control={form.control}
173 render={({ field }) => (
174 <FormItemLayout label="Assign rule to" layout="vertical" className="px-5">
175 <Select
176 onValueChange={(val) => {
177 field.onChange(val)
178 if (val === 'all') form.setValue('is_disabled', true)
179 }}
180 defaultValue={field.value}
181 >
182 <SelectTrigger className="col-span-8">
183 <SelectValue />
184 </SelectTrigger>
185 <SelectContent>
186 <SelectItem value="all">All project members</SelectItem>
187 {members.map((m) => (
188 <SelectItem key={m.gotrue_id} value={m.gotrue_id}>
189 {m.username || m.primary_email}
190 </SelectItem>
191 ))}
192 </SelectContent>
193 </Select>
194 </FormItemLayout>
195 )}
196 />
197
198 {!!lint_name && (
199 <div className="px-5">
200 <Admonition showIcon={false} type="default">
201 {generateRuleDescription({
202 name: lint_name,
203 disabled: is_disabled,
204 member: members.find((x) => x.gotrue_id === assigned_to),
205 })}
206 </Admonition>
207 </div>
208 )}
209
210 <Separator />
211
212 <FormField
213 name="note"
214 control={form.control}
215 render={({ field }) => (
216 <FormItemLayout
217 layout="vertical"
218 className="px-5"
219 label="Description"
220 labelOptional="Optional"
221 >
222 <FormControl>
223 <TextArea
224 {...field}
225 rows={4}
226 className="text-sm"
227 placeholder="e.g Describe why this rule is being set"
228 />
229 </FormControl>
230 </FormItemLayout>
231 )}
232 />
233 </form>
234 </Form>
235 </SheetSection>
236 <SheetFooter>
237 <Button disabled={isCreating} type="default" onClick={() => onOpenChange(false)}>
238 Cancel
239 </Button>
240 <Button form={formId} htmlType="submit" loading={isCreating}>
241 Create rule
242 </Button>
243 </SheetFooter>
244 </SheetContent>
245 </Sheet>
246 )
247}