ProjectNeedsSecuringView.tsx239 lines · main
1import { ArrowRight, Check, ExternalLink, Lightbulb, X } from 'lucide-react'
2import Link from 'next/link'
3import { useRouter } from 'next/router'
4import { useMemo } from 'react'
5import {
6 Button,
7 Button_Shadcn_,
8 Card,
9 Table,
10 TableBody,
11 TableCell,
12 TableHead,
13 TableHeader,
14 TableRow,
15} from 'ui'
16import { PageContainer } from 'ui-patterns/PageContainer'
17import {
18 PageHeader,
19 PageHeaderAside,
20 PageHeaderDescription,
21 PageHeaderIcon,
22 PageHeaderMeta,
23 PageHeaderSummary,
24 PageHeaderTitle,
25} from 'ui-patterns/PageHeader'
26import {
27 PageSection,
28 PageSectionAside,
29 PageSectionContent,
30 PageSectionMeta,
31 PageSectionSummary,
32 PageSectionTitle,
33} from 'ui-patterns/PageSection'
34import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
35
36import type {
37 ProjectSecurityActionDetails,
38 ProjectSecurityActionType,
39 ProjectSecurityTable,
40} from './ProjectNeedsSecuring.types'
41import {
42 buildSecurityPromptMarkdown,
43 formatRlsDescription,
44 getTableKey,
45 getTablePoliciesHref,
46} from './ProjectNeedsSecuring.utils'
47import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
48import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
49import AlertError from '@/components/ui/AlertError'
50import { createNavigationHandler } from '@/lib/navigation'
51import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
52import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
53
54const StatusCell = ({ enabled, label }: { enabled: boolean; label: string }) => (
55 <div className="flex items-center gap-2 text-sm">
56 {enabled ? (
57 <Check size={14} className="text-brand" aria-hidden="true" />
58 ) : (
59 <X size={14} className="text-destructive" aria-hidden="true" />
60 )}
61 <span>{label}</span>
62 </div>
63)
64
65export const ProjectNeedsSecuringView = ({
66 projectRef,
67 issueCount,
68 tables,
69 isLoading,
70 error,
71 onDismiss,
72 onTrackAction,
73}: {
74 projectRef: string
75 issueCount: number
76 tables: ProjectSecurityTable[]
77 isLoading: boolean
78 error?: { message: string } | null
79 onDismiss: () => void
80 onTrackAction: (type: ProjectSecurityActionType, details?: ProjectSecurityActionDetails) => void
81}) => {
82 const router = useRouter()
83 const aiSnap = useAiAssistantStateSnapshot()
84 const { openSidebar } = useSidebarManagerSnapshot()
85
86 const promptMarkdown = useMemo(
87 () => buildSecurityPromptMarkdown(issueCount, tables),
88 [issueCount, tables]
89 )
90
91 const handleOpenAssistant = () => {
92 onTrackAction('ask_assistant')
93 openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
94 aiSnap.newChat({
95 name: 'Review project security',
96 initialInput: promptMarkdown,
97 })
98 }
99
100 return (
101 <div className="flex flex-1 flex-col overflow-y-auto">
102 <PageHeader size="default">
103 <PageHeaderMeta>
104 <PageHeaderIcon>
105 <div className="shrink-0 w-14 h-14 relative bg-destructive-200 border border-destructive-400 rounded-md flex items-center justify-center">
106 <Lightbulb size={20} strokeWidth={1.5} className="text-destructive" />
107 </div>
108 </PageHeaderIcon>
109 <PageHeaderSummary>
110 <PageHeaderTitle>Your project needs securing</PageHeaderTitle>
111 <PageHeaderDescription>{formatRlsDescription(issueCount)}</PageHeaderDescription>
112 </PageHeaderSummary>
113 <PageHeaderAside>
114 <Button asChild type="text" iconRight={<ArrowRight />}>
115 <Link
116 href={`/project/${projectRef}`}
117 onClick={() => {
118 onTrackAction('skip_to_home')
119 onDismiss()
120 }}
121 >
122 Skip to home
123 </Link>
124 </Button>
125 </PageHeaderAside>
126 </PageHeaderMeta>
127 </PageHeader>
128
129 <PageContainer size="default" className="pb-12">
130 <PageSection>
131 <PageSectionMeta>
132 <PageSectionSummary>
133 <PageSectionTitle>Review and fix</PageSectionTitle>
134 </PageSectionSummary>
135 <PageSectionAside>
136 <AiAssistantDropdown
137 label="Ask Assistant"
138 size="tiny"
139 buildPrompt={() => promptMarkdown}
140 onOpenAssistant={handleOpenAssistant}
141 onCopyPrompt={() => onTrackAction('copy_prompt')}
142 copyLabel="Copy Markdown"
143 disabled={isLoading}
144 />
145 </PageSectionAside>
146 </PageSectionMeta>
147 <PageSectionContent>
148 {isLoading ? (
149 <GenericSkeletonLoader />
150 ) : error ? (
151 <AlertError
152 projectRef={projectRef}
153 error={error}
154 subject="Failed to retrieve project tables"
155 />
156 ) : (
157 <Card>
158 <Table>
159 <TableHeader>
160 <TableRow>
161 <TableHead>Name</TableHead>
162 <TableHead>Schema</TableHead>
163 <TableHead>
164 <div className="flex items-center gap-1.5">
165 <span>Accessible via Data API</span>
166 <Button_Shadcn_ asChild variant="ghost" size="icon" className="h-6 w-6">
167 <Link
168 href={`/project/${projectRef}/integrations/data_api/settings`}
169 target="_blank"
170 rel="noreferrer"
171 aria-label="Open Data API settings"
172 >
173 <ExternalLink size={14} aria-hidden="true" />
174 </Link>
175 </Button_Shadcn_>
176 </div>
177 </TableHead>
178 <TableHead>RLS</TableHead>
179 </TableRow>
180 </TableHeader>
181 <TableBody>
182 {tables.map((table) => {
183 const policiesHref = getTablePoliciesHref(
184 projectRef,
185 table.schema,
186 table.name
187 )
188 const handleNavigation = createNavigationHandler(policiesHref, router)
189 const trackViewPolicies = () =>
190 onTrackAction('view_policies', {
191 schema: table.schema,
192 tableName: table.name,
193 })
194
195 return (
196 <TableRow
197 key={getTableKey(table)}
198 className="relative cursor-pointer inset-focus"
199 onClick={(event) => {
200 trackViewPolicies()
201 handleNavigation(event)
202 }}
203 onAuxClick={(event) => {
204 if (event.button === 1) trackViewPolicies()
205 handleNavigation(event)
206 }}
207 onKeyDown={(event) => {
208 if (event.key === 'Enter' || event.key === ' ') trackViewPolicies()
209 handleNavigation(event)
210 }}
211 tabIndex={0}
212 >
213 <TableCell className="font-medium">{table.name}</TableCell>
214 <TableCell>{table.schema}</TableCell>
215 <TableCell>
216 <StatusCell
217 enabled={table.dataApiAccessible}
218 label={table.dataApiAccessible ? 'Accessible' : 'Not accessible'}
219 />
220 </TableCell>
221 <TableCell>
222 <StatusCell
223 enabled={table.rlsEnabled}
224 label={table.rlsEnabled ? 'Enabled' : 'Disabled'}
225 />
226 </TableCell>
227 </TableRow>
228 )
229 })}
230 </TableBody>
231 </Table>
232 </Card>
233 )}
234 </PageSectionContent>
235 </PageSection>
236 </PageContainer>
237 </div>
238 )
239}