PolicySearchResults.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. 'use client'
  2. import { useParams } from 'common'
  3. import { Auth } from 'icons'
  4. import { Loader2 } from 'lucide-react'
  5. import { useMemo } from 'react'
  6. import {
  7. EmptyState,
  8. ResultsList,
  9. SkeletonResults,
  10. type SearchResult,
  11. } from './ContextSearchResults.shared'
  12. import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
  13. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  14. interface PolicySearchResultsProps {
  15. query: string
  16. }
  17. export function PolicySearchResults({ query }: PolicySearchResultsProps) {
  18. const { ref: projectRef } = useParams()
  19. const { data: project } = useSelectedProjectQuery()
  20. const trimmedQuery = query.trim()
  21. const {
  22. data: policies,
  23. isLoading: isLoadingPolicies,
  24. isError: isErrorPolicies,
  25. } = useDatabasePoliciesQuery(
  26. {
  27. projectRef: project?.ref,
  28. connectionString: project?.connectionString,
  29. },
  30. {
  31. enabled: !!project?.ref,
  32. }
  33. )
  34. const policyResults: SearchResult[] = useMemo(() => {
  35. if (!policies) return []
  36. const filtered = trimmedQuery
  37. ? policies.filter((policy) => {
  38. const searchLower = trimmedQuery.toLowerCase()
  39. const policyName = policy.name?.toLowerCase() || ''
  40. const tableName = policy.table?.toLowerCase() || ''
  41. const schemaName = policy.schema?.toLowerCase() || ''
  42. const fullTableName = `${schemaName}.${tableName}`
  43. const command = policy.command?.toLowerCase() || ''
  44. return (
  45. policyName.includes(searchLower) ||
  46. tableName.includes(searchLower) ||
  47. schemaName.includes(searchLower) ||
  48. fullTableName.includes(searchLower) ||
  49. command.includes(searchLower)
  50. )
  51. })
  52. : policies
  53. // Limit results for performance
  54. return filtered.slice(0, 20).map((policy) => {
  55. const displayName = policy.name || 'Untitled Policy'
  56. const tableDisplay =
  57. policy.schema && policy.schema !== 'public'
  58. ? `${policy.schema}.${policy.table}`
  59. : policy.table || 'unknown table'
  60. const commandDisplay = policy.command || 'ALL'
  61. const description = `${commandDisplay} on ${tableDisplay}`
  62. return {
  63. id: String(policy.id),
  64. name: displayName,
  65. description,
  66. }
  67. })
  68. }, [policies, trimmedQuery])
  69. const totalPolicies = policies?.length ?? 0
  70. const renderFooter = () => (
  71. <div className="absolute bottom-0 left-0 right-0 flex items-center justify-between min-h-9 h-9 px-4 border-t bg-surface-200 text-xs text-foreground-light z-10">
  72. <div className="flex items-center gap-x-2">
  73. {isLoadingPolicies ? (
  74. <span className="flex items-center gap-2">
  75. <Loader2 size={14} className="animate-spin" /> Loading...
  76. </span>
  77. ) : (
  78. <span>
  79. Total: {totalPolicies.toLocaleString()} polic{totalPolicies !== 1 ? 'ies' : 'y'}
  80. </span>
  81. )}
  82. </div>
  83. </div>
  84. )
  85. if (isLoadingPolicies) {
  86. return (
  87. <div className="relative h-full flex flex-col">
  88. <div className="flex-1 min-h-0 overflow-hidden">
  89. <SkeletonResults />
  90. </div>
  91. {renderFooter()}
  92. </div>
  93. )
  94. }
  95. if (isErrorPolicies) {
  96. return (
  97. <div className="relative h-full flex flex-col">
  98. <div className="flex-1 min-h-0 overflow-hidden">
  99. <div className="h-full flex flex-col items-center justify-center py-12 px-4 gap-4 text-center text-foreground-lighter">
  100. <Auth className="h-6 w-6" strokeWidth={1.5} />
  101. <p className="text-sm">Failed to load policies</p>
  102. </div>
  103. </div>
  104. {renderFooter()}
  105. </div>
  106. )
  107. }
  108. if (policyResults.length === 0) {
  109. return (
  110. <div className="relative h-full flex flex-col">
  111. <div className="flex-1 min-h-0 overflow-hidden">
  112. <EmptyState icon={Auth} label="RLS Policies" query={query} />
  113. </div>
  114. {renderFooter()}
  115. </div>
  116. )
  117. }
  118. return (
  119. <div className="relative h-full flex flex-col">
  120. <div className="flex-1 min-h-0 overflow-hidden">
  121. <ResultsList
  122. results={policyResults}
  123. icon={Auth}
  124. getRoute={(result) => {
  125. const policy = policies?.find((p) => String(p.id) === result.id)
  126. if (!policy || !projectRef)
  127. return `/project/${projectRef}/auth/policies` as `/${string}`
  128. const params = new URLSearchParams()
  129. params.set('edit', String(policy.id))
  130. if (policy.schema) {
  131. params.set('schema', policy.schema)
  132. }
  133. return `/project/${projectRef}/auth/policies?${params.toString()}` as `/${string}`
  134. }}
  135. className="pb-9"
  136. />
  137. </div>
  138. {renderFooter()}
  139. </div>
  140. )
  141. }