Skip to content

Commit 5b9b39c

Browse files
committed
feat: API info modal
1 parent 5f6bf48 commit 5b9b39c

File tree

14 files changed

+770
-26
lines changed

14 files changed

+770
-26
lines changed

client/bun.lock

Lines changed: 18 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/components.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "new-york",
4+
"rsc": false,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "tailwind.config.js",
8+
"css": "src/index.css",
9+
"baseColor": "neutral",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"iconLibrary": "lucide",
14+
"aliases": {
15+
"components": "@/components",
16+
"utils": "@/lib/utils",
17+
"ui": "@/components/ui",
18+
"lib": "@/lib",
19+
"hooks": "@/hooks"
20+
},
21+
"registries": {}
22+
}

client/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
"@radix-ui/react-tooltip": "^1.2.8",
1818
"@tanstack/react-query": "^5.0.0",
1919
"antd": "^5.27.4",
20-
"class-variance-authority": "^0.7.0",
21-
"clsx": "^2.0.0",
22-
"lucide-react": "^0.294.0",
20+
"class-variance-authority": "^0.7.1",
21+
"clsx": "^2.1.1",
22+
"lucide-react": "^0.544.0",
23+
"motion": "^12.23.22",
2324
"react": "^18.2.0",
2425
"react-dom": "^18.2.0",
25-
"tailwind-merge": "^2.0.0"
26+
"tailwind-merge": "^3.3.1",
27+
"tailwindcss-animate": "^1.0.7"
2628
},
2729
"devDependencies": {
2830
"@types/react": "^18.2.37",
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { useState, useEffect } from 'react'
2+
import {
3+
Dialog,
4+
DialogContent,
5+
DialogDescription,
6+
DialogHeader,
7+
DialogTitle,
8+
} from './ui/dialog'
9+
import { Badge } from './ui/badge'
10+
import { Alert, AlertDescription } from '@/components/ui/alert'
11+
import { Info } from 'lucide-react'
12+
import type { Instance, InstanceConfig, BasicAuthPair } from '../types'
13+
import {
14+
Accordion,
15+
AccordionContent,
16+
AccordionItem,
17+
AccordionTrigger,
18+
} from '@/components/ui/accordion'
19+
import { CopyButton } from '@/components/ui/shadcn-io/copy-button'
20+
21+
interface ApiInfoModalProps {
22+
open: boolean
23+
onOpenChange: (open: boolean) => void
24+
instance: Instance
25+
}
26+
27+
export function ApiInfoModal({ open, onOpenChange, instance }: ApiInfoModalProps) {
28+
const [parseError, setParseError] = useState<string | null>(null)
29+
const [basicAuthPairs, setBasicAuthPairs] = useState<BasicAuthPair[]>([])
30+
31+
// Parse config on open (side effect)
32+
useEffect(() => {
33+
if (!open) return
34+
try {
35+
const config: InstanceConfig = JSON.parse(instance.config || '{}')
36+
const pairs = config.flags?.basicAuth || []
37+
setBasicAuthPairs(pairs)
38+
setParseError(null)
39+
} catch (error) {
40+
setParseError('Invalid configuration: Could not parse JSON. Check instance config.')
41+
setBasicAuthPairs([])
42+
}
43+
}, [open, instance.config])
44+
45+
const proxyUrl = `${window.location.origin}/app/${instance.key}`
46+
47+
const generateToken = (pair: BasicAuthPair): string => {
48+
return `Basic ${btoa(`${pair.username}:${pair.password}`)}`
49+
}
50+
51+
const generateCurl = (token: string, url: string): string => {
52+
return `curl -H "Authorization: ${token}" "${url}"`
53+
}
54+
55+
return (
56+
<Dialog open={open} onOpenChange={onOpenChange}>
57+
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
58+
<DialogHeader>
59+
<DialogTitle className="flex gap-2 items-center">
60+
<Info className="w-5 h-5" />
61+
Gowa API Information for {instance.name}
62+
</DialogTitle>
63+
<DialogDescription>
64+
Access details for the proxied Gowa instance API/UI.
65+
</DialogDescription>
66+
</DialogHeader>
67+
68+
{parseError && (
69+
<Alert variant="destructive" className="mb-4">
70+
<AlertDescription>{parseError}</AlertDescription>
71+
</Alert>
72+
)}
73+
74+
<div className="space-y-6">
75+
{/* API URL */}
76+
<div className="space-y-2">
77+
<h3 className="text-sm font-medium text-gray-900">API Base URL</h3>
78+
<div className="flex justify-between items-center">
79+
<code className="px-3 py-1 font-mono text-sm break-all bg-gray-100 rounded-md">
80+
{proxyUrl}
81+
</code>
82+
<CopyButton
83+
variant="ghost"
84+
size="sm"
85+
className="shrink-0"
86+
content={proxyUrl}
87+
aria-label="Copy API base URL"
88+
/>
89+
</div>
90+
</div>
91+
92+
{/* Auth Tokens */}
93+
<div className="space-y-2">
94+
<h3 className="text-sm font-medium text-gray-900">Authentication Tokens</h3>
95+
{basicAuthPairs.length === 0 ? (
96+
<Alert>
97+
<Info className="w-4 h-4" />
98+
<AlertDescription>No basic auth configured for this instance.</AlertDescription>
99+
</Alert>
100+
) : (
101+
<div className="space-y-2">
102+
{basicAuthPairs.length === 1 ? (
103+
<div className="flex justify-between items-center">
104+
<code className="flex-1 px-3 py-1 mr-2 font-mono text-sm break-all bg-gray-100 rounded-md">
105+
{generateToken(basicAuthPairs[0])}
106+
</code>
107+
<CopyButton
108+
variant="ghost"
109+
size="sm"
110+
className="shrink-0"
111+
content={generateToken(basicAuthPairs[0])}
112+
aria-label="Copy basic auth token"
113+
/>
114+
</div>
115+
) : (
116+
<Accordion type="single" collapsible className="w-full">
117+
{basicAuthPairs.map((pair, index) => (
118+
<AccordionItem key={index} value={`item-${index}`}>
119+
<AccordionTrigger className="hover:no-underline">
120+
<div className="flex gap-2 items-center">
121+
<Badge variant="secondary" className="text-xs">
122+
Auth {index + 1}
123+
</Badge>
124+
<span className="text-sm font-medium">{pair.username}</span>
125+
</div>
126+
</AccordionTrigger>
127+
<AccordionContent>
128+
<div className="pt-2 space-y-2">
129+
<div className="flex justify-between items-center">
130+
<code className="flex-1 px-3 py-1 mr-2 font-mono text-sm break-all bg-gray-100 rounded-md">
131+
{generateToken(pair)}
132+
</code>
133+
<CopyButton
134+
variant="ghost"
135+
size="sm"
136+
className="shrink-0"
137+
content={generateToken(pair)}
138+
aria-label={`Copy basic auth token for ${pair.username}`}
139+
/>
140+
</div>
141+
</div>
142+
</AccordionContent>
143+
</AccordionItem>
144+
))}
145+
</Accordion>
146+
)}
147+
</div>
148+
)}
149+
</div>
150+
151+
{/* Curl Samples */}
152+
<div className="space-y-2">
153+
<h3 className="text-sm font-medium text-gray-900">Curl Sample</h3>
154+
{basicAuthPairs.length === 0 ? (
155+
<div className="flex gap-2 items-start">
156+
<pre className="flex-1 p-3 font-mono text-xs whitespace-pre-wrap break-words bg-gray-100 rounded-md border">
157+
{`curl "${proxyUrl}/app/devices"`}
158+
</pre>
159+
<CopyButton
160+
variant="ghost"
161+
size="sm"
162+
className="shrink-0"
163+
content={`curl "${proxyUrl}/app/devices"`}
164+
aria-label="Copy curl command"
165+
/>
166+
</div>
167+
) : (
168+
<div className="space-y-3">
169+
{basicAuthPairs.map((pair, index) => (
170+
<div key={index}>
171+
<div className="flex gap-2 items-center mb-1">
172+
<Badge variant="secondary" className="text-xs">
173+
Auth {index + 1}
174+
</Badge>
175+
<span className="text-xs text-gray-600">{pair.username}</span>
176+
</div>
177+
<div className="flex gap-2 items-start">
178+
<pre className="flex-1 p-3 font-mono text-xs whitespace-pre-wrap break-words bg-gray-100 rounded-md border">
179+
{generateCurl(generateToken(pair), proxyUrl + '/app/devices')}
180+
</pre>
181+
<CopyButton
182+
variant="ghost"
183+
size="sm"
184+
className="shrink-0"
185+
content={generateCurl(generateToken(pair), proxyUrl + '/app/devices')}
186+
aria-label={`Copy curl command for auth ${index + 1}`}
187+
/>
188+
</div>
189+
</div>
190+
))}
191+
</div>
192+
)}
193+
</div>
194+
</div>
195+
</DialogContent>
196+
</Dialog>
197+
)
198+
}

0 commit comments

Comments
 (0)