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