CreateRuleSheet.tsx 8.4 KB

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