Skip to content

Commit 1e5fed0

Browse files
refactor: made separate components for database-lists
1 parent 5a3ebe0 commit 1e5fed0

4 files changed

Lines changed: 283 additions & 269 deletions

File tree

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import type { connections } from '~/drizzle'
2+
import { SafeURL } from '@conar/shared/utils/safe-url'
3+
import { Badge } from '@conar/ui/components/badge'
4+
import { Button } from '@conar/ui/components/button'
5+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@conar/ui/components/dropdown-menu'
6+
import { copy } from '@conar/ui/lib/copy'
7+
import { cn } from '@conar/ui/lib/utils'
8+
import { RiArrowDownSLine, RiArrowUpSLine, RiDatabase2Line, RiDeleteBinLine, RiEditLine, RiFileCopyLine, RiLoader4Line, RiMoreLine } from '@remixicon/react'
9+
import { Link } from '@tanstack/react-router'
10+
import { AnimatePresence, motion } from 'motion/react'
11+
import { ConnectionIcon } from '~/entities/connection/components'
12+
import { useConnectionLinkParams, useServerConnections } from '~/entities/connection/hooks'
13+
import { connectionsCollection } from '~/entities/connection/sync'
14+
import { cloneConnectionForConnection } from '~/entities/connection/utils'
15+
16+
export function ConnectionCard({ connection, onRemove, onRename }: { connection: typeof connections.$inferSelect, onRemove: () => void, onRename: () => void }) {
17+
const url = new SafeURL(connection.connectionString)
18+
19+
if (connection.isPasswordExists || url.password) {
20+
url.password = '*'.repeat(url.password.length || 6)
21+
}
22+
23+
const connectionString = url.toString()
24+
25+
const params = useConnectionLinkParams(connection.id)
26+
27+
const { databasesList, isLoading, isExpanded, toggleExpand } = useServerConnections(connection)
28+
29+
return (
30+
<motion.div
31+
layout
32+
initial={{ opacity: 0, scale: 0.75 }}
33+
animate={{ opacity: 1, scale: 1 }}
34+
exit={{ opacity: 0, scale: 0.75 }}
35+
transition={{ duration: 0.15 }}
36+
className="flex flex-col gap-1"
37+
>
38+
<Link
39+
className={cn(
40+
`
41+
group relative flex items-center justify-between gap-4
42+
overflow-hidden rounded-lg border border-l-4 border-border/50
43+
bg-muted/30 p-5
44+
`,
45+
connection.color
46+
? `
47+
border-l-(--color)/60
48+
hover:border-(--color)/60
49+
`
50+
: 'hover:border-primary/60',
51+
)}
52+
style={connection.color ? { '--color': connection.color } : {}}
53+
preload={false}
54+
{...params}
55+
>
56+
<div className="size-12 shrink-0 rounded-lg bg-muted/70 p-3">
57+
<ConnectionIcon
58+
type={connection.type}
59+
className="size-full"
60+
/>
61+
</div>
62+
<div className="flex min-w-0 flex-1 flex-col">
63+
<div className={`
64+
flex items-center gap-2 truncate font-medium tracking-tight
65+
`}
66+
>
67+
<span className={connection.color
68+
? `
69+
text-(--color)
70+
group-hover:text-(--color)/80
71+
`
72+
: ''}
73+
>
74+
{connection.name}
75+
</span>
76+
{connection.label && (
77+
<Badge variant="secondary">
78+
{connection.label}
79+
</Badge>
80+
)}
81+
</div>
82+
<div
83+
data-mask
84+
className="truncate font-mono text-xs text-muted-foreground"
85+
>
86+
{connectionString.replaceAll('*', '•')}
87+
</div>
88+
</div>
89+
90+
<div className="z-10 flex items-center gap-1">
91+
{databasesList.length > 0 && (
92+
<Button
93+
variant="ghost"
94+
size="icon"
95+
className="size-8"
96+
onClick={(e) => {
97+
e.preventDefault()
98+
e.stopPropagation()
99+
toggleExpand()
100+
}}
101+
>
102+
{isExpanded
103+
? <RiArrowUpSLine className="size-4" />
104+
: (
105+
<RiArrowDownSLine className="size-4" />
106+
)}
107+
</Button>
108+
)}
109+
110+
<DropdownMenu>
111+
<DropdownMenuTrigger className={`
112+
rounded-md p-2
113+
hover:bg-accent-foreground/5
114+
`}
115+
>
116+
<RiMoreLine className="size-4" />
117+
</DropdownMenuTrigger>
118+
<DropdownMenuContent align="end">
119+
<DropdownMenuItem
120+
onClick={(e) => {
121+
e.stopPropagation()
122+
copy(connection.connectionString, 'Connection string copied to clipboard')
123+
}}
124+
>
125+
<RiFileCopyLine className="size-4 opacity-50" />
126+
Copy Connection String
127+
</DropdownMenuItem>
128+
<DropdownMenuItem
129+
onClick={(e) => {
130+
e.stopPropagation()
131+
onRename()
132+
}}
133+
>
134+
<RiEditLine className="size-4 opacity-50" />
135+
Rename
136+
</DropdownMenuItem>
137+
<DropdownMenuItem
138+
className={`
139+
text-destructive
140+
focus:text-destructive
141+
`}
142+
onClick={(e) => {
143+
e.stopPropagation()
144+
onRemove()
145+
}}
146+
>
147+
<RiDeleteBinLine className="size-4" />
148+
Remove
149+
</DropdownMenuItem>
150+
</DropdownMenuContent>
151+
</DropdownMenu>
152+
</div>
153+
</Link>
154+
155+
<AnimatePresence>
156+
{isExpanded && (
157+
<motion.div
158+
initial={{ height: 0, opacity: 0 }}
159+
animate={{ height: 'auto', opacity: 1 }}
160+
exit={{ height: 0, opacity: 0 }}
161+
className="overflow-hidden"
162+
>
163+
<div className={`
164+
ml-4 flex max-h-60 flex-col gap-1 overflow-y-auto rounded-lg
165+
border bg-muted/30 p-2
166+
`}
167+
>
168+
{isLoading
169+
? (
170+
<div className={`
171+
flex items-center gap-2 p-2 text-sm text-muted-foreground
172+
`}
173+
>
174+
<RiLoader4Line className="size-4 animate-spin" />
175+
Loading databases...
176+
</div>
177+
)
178+
: databasesList.length > 0
179+
? (
180+
databasesList.map(dbName => (
181+
<div
182+
key={dbName}
183+
role="button"
184+
onKeyDown={() => {}}
185+
tabIndex={0}
186+
className={`
187+
flex cursor-pointer items-center gap-2 rounded-md
188+
border border-transparent p-2 text-sm
189+
hover:border-border hover:bg-muted
190+
`}
191+
onClick={(e) => {
192+
e.preventDefault()
193+
e.stopPropagation()
194+
195+
const newConnection = cloneConnectionForConnection(connection, dbName)
196+
connectionsCollection.insert(newConnection)
197+
}}
198+
>
199+
<RiDatabase2Line className={`
200+
size-4 text-muted-foreground
201+
`}
202+
/>
203+
<span>{dbName}</span>
204+
</div>
205+
))
206+
)
207+
: (
208+
<div className="p-2 text-sm text-muted-foreground">
209+
No other databases found.
210+
</div>
211+
)}
212+
</div>
213+
</motion.div>
214+
)}
215+
</AnimatePresence>
216+
</motion.div>
217+
)
218+
}

0 commit comments

Comments
 (0)