index.tsx182 lines · main
1import { useParams } from 'common'
2import { VectorBucket as VectorBucketIcon } from 'icons'
3import { ChevronRight, Search } from 'lucide-react'
4import { useRouter } from 'next/navigation'
5import { parseAsBoolean, useQueryState } from 'nuqs'
6import { useState } from 'react'
7import { Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
8import { Input } from 'ui-patterns/DataInputs/Input'
9import { PageContainer } from 'ui-patterns/PageContainer'
10import { PageSection, PageSectionContent, PageSectionTitle } from 'ui-patterns/PageSection'
11import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
12import { TimestampInfo } from 'ui-patterns/TimestampInfo'
13
14import { EmptyBucketState } from '../EmptyBucketState'
15import { CreateBucketButton } from '../NewBucketButton'
16import { CreateVectorBucketDialog } from './CreateVectorBucketDialog'
17import AlertError from '@/components/ui/AlertError'
18import { AlphaNotice } from '@/components/ui/AlphaNotice'
19import { useVectorBucketsQuery } from '@/data/storage/vector-buckets-query'
20import { createNavigationHandler } from '@/lib/navigation'
21
22/**
23 * [Joshen] Low-priority refactor: We should use a virtualized table here as per how we do it
24 * for the files buckets for consistency. Not pressing, just an optimization area.
25 */
26
27export const VectorsBuckets = () => {
28 const { ref: projectRef } = useParams()
29 const router = useRouter()
30
31 const [filterString, setFilterString] = useState('')
32 const [visible, setVisible] = useQueryState(
33 'new',
34 parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
35 )
36
37 const {
38 data,
39 error: bucketsError,
40 isError: isErrorBuckets,
41 isPending: isLoadingBuckets,
42 isSuccess: isSuccessBuckets,
43 } = useVectorBucketsQuery({ projectRef })
44 const bucketsList = data?.vectorBuckets ?? []
45
46 const filteredBuckets =
47 filterString.length === 0
48 ? bucketsList
49 : bucketsList.filter((bucket) =>
50 bucket.vectorBucketName.toLowerCase().includes(filterString.toLowerCase())
51 )
52
53 return (
54 <>
55 <PageContainer>
56 <PageSection>
57 <PageSectionContent className="flex flex-col gap-y-8">
58 <AlphaNotice
59 entity="Vector buckets"
60 feedbackUrl="https://github.com/orgs/briven/discussions/40815"
61 />
62
63 {isLoadingBuckets && <GenericSkeletonLoader />}
64
65 {isErrorBuckets && (
66 <AlertError error={bucketsError} subject="Failed to retrieve vector buckets" />
67 )}
68
69 {isSuccessBuckets && (
70 <>
71 {bucketsList.length === 0 ? (
72 <EmptyBucketState bucketType="vectors" onCreateBucket={() => setVisible(true)} />
73 ) : (
74 <div className="flex flex-col gap-y-4">
75 <div className="py-0">
76 <PageSectionTitle>Buckets</PageSectionTitle>
77 </div>
78 <div className="flex grow justify-between gap-x-2 items-center">
79 <Input
80 size="tiny"
81 className="grow lg:grow-0 w-52"
82 placeholder="Search for a bucket"
83 value={filterString}
84 onChange={(e) => setFilterString(e.target.value)}
85 icon={<Search />}
86 />
87
88 <CreateBucketButton onClick={() => setVisible(true)} />
89 </div>
90
91 {isLoadingBuckets ? (
92 <GenericSkeletonLoader />
93 ) : (
94 <Card>
95 <Table>
96 <TableHeader>
97 <TableRow>
98 {filteredBuckets.length > 0 && (
99 <TableHead className="w-2 pr-1">
100 <span className="sr-only">Icon</span>
101 </TableHead>
102 )}
103 <TableHead>Name</TableHead>
104 <TableHead>Created at</TableHead>
105 <TableHead>
106 <span className="sr-only">Actions</span>
107 </TableHead>
108 </TableRow>
109 </TableHeader>
110 <TableBody>
111 {filteredBuckets.length === 0 && filterString.length > 0 && (
112 <TableRow className="[&>td]:hover:bg-inherit">
113 <TableCell colSpan={3}>
114 <p className="text-sm text-foreground">No results found</p>
115 <p className="text-sm text-foreground-lighter">
116 Your search for "{filterString}" did not return any results
117 </p>
118 </TableCell>
119 </TableRow>
120 )}
121 {filteredBuckets.map((bucket, idx: number) => {
122 const id = `bucket-${idx}`
123 const name = bucket.vectorBucketName
124 // the creation time is in seconds, convert it to milliseconds
125 const created = +bucket.creationTime * 1000
126
127 const handleBucketNavigation = createNavigationHandler(
128 `/project/${projectRef}/storage/vectors/buckets/${encodeURIComponent(name)}`,
129 router
130 )
131
132 return (
133 <TableRow
134 key={id}
135 className="relative cursor-pointer h-16 inset-focus"
136 onClick={handleBucketNavigation}
137 onAuxClick={handleBucketNavigation}
138 onKeyDown={handleBucketNavigation}
139 tabIndex={0}
140 >
141 <TableCell className="w-2 pr-1">
142 <VectorBucketIcon size={16} className="text-foreground-muted" />
143 </TableCell>
144 <TableCell>
145 <p className="whitespace-nowrap max-w-[512px] truncate">
146 {name}
147 </p>
148 </TableCell>
149 <TableCell>
150 <p className="text-foreground-light">
151 <TimestampInfo
152 utcTimestamp={created}
153 className="text-sm text-foreground-light"
154 />
155 </p>
156 </TableCell>
157 <TableCell>
158 <div className="flex justify-end items-center h-full">
159 <ChevronRight
160 size={14}
161 className="text-foreground-muted/60"
162 />
163 </div>
164 </TableCell>
165 </TableRow>
166 )
167 })}
168 </TableBody>
169 </Table>
170 </Card>
171 )}
172 </div>
173 )}
174 </>
175 )}
176 </PageSectionContent>
177 </PageSection>
178 </PageContainer>
179 <CreateVectorBucketDialog visible={visible} setVisible={setVisible} />
180 </>
181 )
182}