ProjectNeedsSecuringView.tsx 8.3 KB

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