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