index.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import { useParams } from 'common'
  2. import { AnalyticsBucket as AnalyticsBucketIcon } from 'icons'
  3. import { ChevronRight, Search } from 'lucide-react'
  4. import { useRouter } from 'next/navigation'
  5. import { parseAsBoolean, useQueryState } from 'nuqs'
  6. import { useState } from 'react'
  7. import {
  8. Card,
  9. Table,
  10. TableBody,
  11. TableCell,
  12. TableHead,
  13. TableHeader,
  14. TableRow,
  15. Tooltip,
  16. TooltipContent,
  17. TooltipTrigger,
  18. } from 'ui'
  19. import { TimestampInfo } from 'ui-patterns'
  20. import { Input } from 'ui-patterns/DataInputs/Input'
  21. import { PageContainer } from 'ui-patterns/PageContainer'
  22. import { PageSection, PageSectionContent, PageSectionTitle } from 'ui-patterns/PageSection'
  23. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  24. import { EmptyBucketState } from '../EmptyBucketState'
  25. import { CreateBucketButton } from '../NewBucketButton'
  26. import { CreateAnalyticsBucketModal } from './CreateAnalyticsBucketModal'
  27. import AlertError from '@/components/ui/AlertError'
  28. import { AlphaNotice } from '@/components/ui/AlphaNotice'
  29. import { useProjectStorageConfigQuery } from '@/data/config/project-storage-config-query'
  30. import { useAnalyticsBucketsQuery } from '@/data/storage/analytics-buckets-query'
  31. import { createNavigationHandler } from '@/lib/navigation'
  32. export const AnalyticsBuckets = () => {
  33. const { ref } = useParams()
  34. const router = useRouter()
  35. const [filterString, setFilterString] = useState('')
  36. const [visible, setVisible] = useQueryState(
  37. 'new',
  38. parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
  39. )
  40. const { data: config } = useProjectStorageConfigQuery({ projectRef: ref })
  41. const maxAnalyticsBuckets = config?.features.icebergCatalog.maxCatalogs ?? 2
  42. const {
  43. data: buckets = [],
  44. error: bucketsError,
  45. isError: isErrorBuckets,
  46. isPending: isLoadingBuckets,
  47. isSuccess: isSuccessBuckets,
  48. } = useAnalyticsBucketsQuery({
  49. projectRef: ref,
  50. })
  51. const analyticsBuckets = buckets.filter((bucket) =>
  52. filterString.length === 0
  53. ? true
  54. : bucket.name.toLowerCase().includes(filterString.toLowerCase())
  55. )
  56. const hasNoBuckets = buckets.length === 0
  57. return (
  58. <>
  59. <PageContainer>
  60. <PageSection>
  61. <PageSectionContent className="flex flex-col gap-y-8">
  62. <AlphaNotice
  63. entity="Analytics buckets"
  64. feedbackUrl="https://github.com/orgs/briven/discussions/40116"
  65. />
  66. {isLoadingBuckets && <GenericSkeletonLoader />}
  67. {isErrorBuckets && (
  68. <AlertError error={bucketsError} subject="Failed to retrieve analytics buckets" />
  69. )}
  70. {isSuccessBuckets && (
  71. <>
  72. {hasNoBuckets ? (
  73. <EmptyBucketState
  74. bucketType="analytics"
  75. onCreateBucket={() => setVisible(true)}
  76. />
  77. ) : (
  78. <div className="flex flex-col gap-y-4">
  79. <div className="flex flex-row items-center gap-x-2">
  80. <PageSectionTitle>Buckets</PageSectionTitle>
  81. {analyticsBuckets.length > 0 && (
  82. <Tooltip>
  83. <TooltipTrigger>
  84. <span className="bg-surface-200 rounded-full px-2 py-1 leading-none text-xs text-foreground-lighter tracking-widest">
  85. {analyticsBuckets.length}/{maxAnalyticsBuckets}
  86. </span>
  87. </TooltipTrigger>
  88. <TooltipContent side="bottom" className="w-72 text-center">
  89. Each project can only have up to {maxAnalyticsBuckets} buckets while
  90. analytics buckets are in alpha{' '}
  91. </TooltipContent>
  92. </Tooltip>
  93. )}
  94. </div>
  95. <div className="flex grow justify-between gap-x-2 items-center">
  96. <Input
  97. size="tiny"
  98. className="grow lg:grow-0 w-52"
  99. placeholder="Search for a bucket"
  100. value={filterString}
  101. onChange={(e) => setFilterString(e.target.value)}
  102. icon={<Search />}
  103. />
  104. <CreateBucketButton onClick={() => setVisible(true)} />
  105. </div>
  106. {isLoadingBuckets ? (
  107. <GenericSkeletonLoader />
  108. ) : (
  109. <Card>
  110. <Table>
  111. <TableHeader>
  112. <TableRow>
  113. {analyticsBuckets.length > 0 && (
  114. <TableHead className="w-2 pr-1">
  115. <span className="sr-only">Icon</span>
  116. </TableHead>
  117. )}
  118. <TableHead>Name</TableHead>
  119. <TableHead>Created at</TableHead>
  120. <TableHead>
  121. <span className="sr-only">Actions</span>
  122. </TableHead>
  123. </TableRow>
  124. </TableHeader>
  125. <TableBody>
  126. {analyticsBuckets.length === 0 && filterString.length > 0 && (
  127. <TableRow className="[&>td]:hover:bg-inherit">
  128. <TableCell colSpan={3}>
  129. <p className="text-sm text-foreground">No results found</p>
  130. <p className="text-sm text-foreground-light">
  131. Your search for "{filterString}" did not return any results
  132. </p>
  133. </TableCell>
  134. </TableRow>
  135. )}
  136. {analyticsBuckets.map((bucket) => {
  137. const handleBucketNavigation = createNavigationHandler(
  138. `/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucket.name)}`,
  139. router
  140. )
  141. return (
  142. <TableRow
  143. key={bucket.name}
  144. className="relative cursor-pointer h-16 inset-focus"
  145. onClick={handleBucketNavigation}
  146. onAuxClick={handleBucketNavigation}
  147. onKeyDown={handleBucketNavigation}
  148. tabIndex={0}
  149. >
  150. <TableCell className="w-2 pr-1">
  151. <AnalyticsBucketIcon
  152. size={16}
  153. className="text-foreground-muted"
  154. />
  155. </TableCell>
  156. <TableCell>
  157. <p className="whitespace-nowrap max-w-[512px] truncate">
  158. {bucket.name}
  159. </p>
  160. </TableCell>
  161. <TableCell>
  162. <p className="text-foreground-light">
  163. <TimestampInfo
  164. utcTimestamp={bucket.created_at}
  165. className="text-sm text-foreground-light"
  166. />
  167. </p>
  168. </TableCell>
  169. <TableCell>
  170. <div className="flex justify-end items-center h-full">
  171. <ChevronRight
  172. size={14}
  173. className="text-foreground-muted/60"
  174. />
  175. </div>
  176. </TableCell>
  177. </TableRow>
  178. )
  179. })}
  180. </TableBody>
  181. </Table>
  182. </Card>
  183. )}
  184. </div>
  185. )}
  186. </>
  187. )}
  188. </PageSectionContent>
  189. </PageSection>
  190. </PageContainer>
  191. <CreateAnalyticsBucketModal open={visible} onOpenChange={setVisible} />
  192. </>
  193. )
  194. }