AdvisorRuleItem.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import { useParams } from 'common'
  2. import { ChevronRight, Trash } from 'lucide-react'
  3. import { useState } from 'react'
  4. import { toast } from 'sonner'
  5. import {
  6. Badge,
  7. Button,
  8. Card,
  9. CardContent,
  10. cn,
  11. Collapsible,
  12. CollapsibleContent,
  13. CollapsibleTrigger,
  14. } from 'ui'
  15. import { GenericSkeletonLoader } from 'ui-patterns'
  16. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  17. import { LintInfo } from '../Linter/Linter.constants'
  18. import { generateRuleText } from './AdvisorRules.utils'
  19. import { CreateRuleSheet } from './CreateRuleSheet'
  20. import { DisableRuleModal } from './DisableRuleModal'
  21. import { EnableRuleModal } from './EnableRuleModal'
  22. import AlertError from '@/components/ui/AlertError'
  23. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  24. import { DocsButton } from '@/components/ui/DocsButton'
  25. import { useLintRuleDeleteMutation } from '@/data/lint/delete-lint-rule-mutation'
  26. import { useProjectLintRulesQuery } from '@/data/lint/lint-rules-query'
  27. import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query'
  28. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  29. interface AdvisorRuleItemProps {
  30. lint: LintInfo
  31. }
  32. // [Joshen] Context: We're going with a simplified interface for LW15 as we launch
  33. // this as a feature preview. Going to be purely disabling rules for the whole project,
  34. // not assigning to members, etc (all these can come in the future when we're ready)
  35. // Hence using this to clearly indicate where is being simplified
  36. const SIMPLIFIED_INTERFACE = true
  37. export const AdvisorRuleItem = ({ lint }: AdvisorRuleItemProps) => {
  38. const { ref: projectRef } = useParams()
  39. const { data: organization } = useSelectedOrganizationQuery()
  40. const [open, setOpen] = useState(false)
  41. const [expandedLint, setExpandedLint] = useState<string>()
  42. const [selectedRuleToDelete, setSelectedRuleToDelete] = useState<string>()
  43. const { data: members = [] } = useOrganizationMembersQuery({ slug: organization?.slug })
  44. const {
  45. data = { exceptions: [] },
  46. error,
  47. isPending: isLoading,
  48. isSuccess,
  49. isError,
  50. } = useProjectLintRulesQuery({ projectRef })
  51. const rules = data.exceptions.filter((x) => x.lint_name === lint.name)
  52. const selectedRuleMeta = data.exceptions.find((x) => x.id === selectedRuleToDelete)
  53. const selectedMemberForRule = members.find((x) => x.gotrue_id === selectedRuleMeta?.assigned_to)
  54. const { mutate: deleteRule, isPending: isDeleting } = useLintRuleDeleteMutation({
  55. onSuccess: () => {
  56. toast.success('Successfully deleted rule')
  57. setSelectedRuleToDelete(undefined)
  58. },
  59. })
  60. const onDeleteRule = () => {
  61. if (!projectRef) return console.error('Project ref is required')
  62. if (!selectedRuleToDelete) return console.error('No rules selected')
  63. deleteRule({ projectRef, ids: [selectedRuleToDelete] })
  64. }
  65. if (SIMPLIFIED_INTERFACE) {
  66. return (
  67. <Card className="border-b-0 rounded-none last:border-b first:rounded-t-md last:rounded-b-md">
  68. <CardContent className="py-3 flex items-center justify-between text-sm gap-4 cursor-pointer transition hover:bg-surface-200">
  69. <div className="flex items-center justify-center [&>svg]:text-foreground-lighter">
  70. {lint.icon}
  71. </div>
  72. <div className="flex-1 flex items-center gap-x-2">
  73. <span>{lint.title}</span>
  74. {rules.length > 0 && <Badge>Disabled</Badge>}
  75. </div>
  76. <div className="flex items-center gap-x-2">
  77. <DocsButton href={lint.docsLink} />
  78. {rules.length > 0 ? (
  79. <EnableRuleModal lint={lint} rule={rules[0]} />
  80. ) : (
  81. <DisableRuleModal lint={lint} />
  82. )}
  83. </div>
  84. </CardContent>
  85. </Card>
  86. )
  87. }
  88. return (
  89. <>
  90. <Collapsible
  91. id={lint.name}
  92. open={expandedLint === lint.name}
  93. onOpenChange={(open) => {
  94. if (open) setExpandedLint(lint.name)
  95. else setExpandedLint(undefined)
  96. }}
  97. >
  98. <CollapsibleTrigger asChild className="[&[data-state=open]>div>svg]:rotate-90!">
  99. <Card className="border-b-0 rounded-none">
  100. <CardContent className="py-3 flex items-center justify-between text-sm gap-4 cursor-pointer transition hover:bg-surface-200">
  101. <div className="flex items-center justify-center [&>svg]:text-foreground-lighter">
  102. {lint.icon}
  103. </div>
  104. <div className="flex-1 flex items-center gap-x-2">
  105. <span>{lint.title}</span>
  106. {rules.length > 0 && (
  107. <span className="font-mono text-xs w-5 h-5 rounded-full border border-alternative bg-surface-300 flex items-center justify-center">
  108. {rules.length}
  109. </span>
  110. )}
  111. </div>
  112. <div className="flex items-center gap-x-2">
  113. <DocsButton href={lint.docsLink} />
  114. <Button
  115. type="default"
  116. onClick={(e) => {
  117. e.stopPropagation()
  118. setOpen(true)
  119. }}
  120. >
  121. Create rule
  122. </Button>
  123. </div>
  124. <ChevronRight strokeWidth={1.5} size={16} className="transition" />
  125. </CardContent>
  126. </Card>
  127. </CollapsibleTrigger>
  128. <CollapsibleContent
  129. className={cn(
  130. 'bg-surface border-x border-t rounded-none!',
  131. rules.length > 0 ? 'divide-y' : ''
  132. )}
  133. >
  134. {isLoading && (
  135. <div className="px-6 py-3">
  136. <GenericSkeletonLoader />
  137. </div>
  138. )}
  139. {isError && (
  140. <AlertError
  141. className="rounded-none border-0"
  142. error={error}
  143. subject="Failed to retrieve advisor rules"
  144. />
  145. )}
  146. {isSuccess && (
  147. <>
  148. {rules.length === 0 ? (
  149. <div className="px-6 py-3">
  150. <p className="text-sm text-foreground">
  151. Lint is visible to all project members in the{' '}
  152. <span className="capitalize">{lint.category}</span> Advisor.
  153. </p>
  154. <p className="text-sm text-foreground-lighter">
  155. Create a rule to configure the visibility of this lint in your project
  156. </p>
  157. </div>
  158. ) : (
  159. rules.map((rule) => {
  160. const member = members.find((x) => x.gotrue_id === rule.assigned_to)
  161. const ruleText = generateRuleText(rule, member)
  162. return (
  163. <div key={rule.id} className="px-6 py-3 flex items-center justify-between">
  164. <div>
  165. <p className="text-sm">{ruleText}</p>
  166. {rule.note && (
  167. <p className="text-sm text-foreground-lighter">{rule.note}</p>
  168. )}
  169. </div>
  170. <div className="flex items-center gap-x-2">
  171. {/* [Joshen] Will implement in part 2 */}
  172. {/* <ButtonTooltip
  173. type="default"
  174. icon={<Edit />}
  175. className="w-7"
  176. tooltip={{ content: { side: 'bottom', text: 'Edit rule' } }}
  177. /> */}
  178. <ButtonTooltip
  179. type="default"
  180. icon={<Trash />}
  181. className="w-7"
  182. onClick={() => setSelectedRuleToDelete(rule.id)}
  183. tooltip={{ content: { side: 'bottom', text: 'Delete rule' } }}
  184. />
  185. </div>
  186. </div>
  187. )
  188. })
  189. )}
  190. </>
  191. )}
  192. </CollapsibleContent>
  193. </Collapsible>
  194. <CreateRuleSheet lint={lint} open={open} onOpenChange={setOpen} />
  195. <ConfirmationModal
  196. size="medium"
  197. loading={isDeleting}
  198. visible={!!selectedRuleToDelete}
  199. onCancel={() => setSelectedRuleToDelete(undefined)}
  200. title="Confirm to delete selected rule"
  201. onConfirm={() => onDeleteRule()}
  202. alert={{
  203. base: { variant: 'warning' },
  204. title: 'The following rule will be removed',
  205. description: !!selectedRuleMeta
  206. ? generateRuleText(selectedRuleMeta, selectedMemberForRule)
  207. : '',
  208. }}
  209. >
  210. {!!selectedRuleMeta && (
  211. <>
  212. <p className="text-sm text-foreground">
  213. The selected lint will appear under the{' '}
  214. <span className="capitalize">{lint.category}</span> Advisor again
  215. {!!selectedRuleMeta.assigned_to ? ' for all project members' : ''} once this rule is
  216. removed.
  217. </p>
  218. </>
  219. )}
  220. </ConfirmationModal>
  221. </>
  222. )
  223. }