Improve dashboard responsive layouts (#805)
* Improve dashboard responsive layouts * Improve proxy pools mobile layout * Improve CLI tool card input responsiveness --------- Co-authored-by: Delynn Assistant <zhen@dkzhen.org>
This commit is contained in:
1 parent
9ca388972c
commit
0da61d8f7b
29 files changed
+561
-464
No files matched your search
@@ -332,28 +332,28 @@ export default function ProfilePage() {
|
||||
const observabilityEnabled = settings.enableObservability === true;
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="max-w-2xl mx-auto px-4 sm:px-0">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Local Mode Info */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="size-12 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-2xl">computer</span>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
|
||||
<div className="flex items-center gap-3 sm:gap-4">
|
||||
<div className="size-10 sm:size-12 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center shrink-0">
|
||||
<span className="material-symbols-outlined text-xl sm:text-2xl">computer</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Local Mode</h2>
|
||||
<p className="text-text-muted">Running on your machine</p>
|
||||
<h2 className="text-lg sm:text-xl font-semibold">Local Mode</h2>
|
||||
<p className="text-sm text-text-muted">Running on your machine</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-flex p-1 rounded-lg bg-black/5 dark:bg-white/5">
|
||||
<div className="inline-flex p-1 rounded-lg bg-black/5 dark:bg-white/5 w-full sm:w-auto">
|
||||
{["light", "dark", "system"].map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
onClick={() => setTheme(option)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 rounded-md font-medium transition-all",
|
||||
"flex items-center justify-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1.5 rounded-md font-medium transition-all flex-1 sm:flex-initial",
|
||||
theme === option
|
||||
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
|
||||
: "text-text-muted hover:text-text-main"
|
||||
@@ -362,24 +362,25 @@ export default function ProfilePage() {
|
||||
<span className="material-symbols-outlined text-[18px]">
|
||||
{option === "light" ? "light_mode" : option === "dark" ? "dark_mode" : "contrast"}
|
||||
</span>
|
||||
<span className="capitalize text-sm">{option}</span>
|
||||
<span className="capitalize text-xs sm:text-sm">{option}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 pt-4 border-t border-border">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-bg border border-border">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 rounded-lg bg-bg border border-border gap-2">
|
||||
<div>
|
||||
<p className="font-medium">Database Location</p>
|
||||
<p className="text-sm text-text-muted font-mono">~/.9router/db.json</p>
|
||||
<p className="font-medium text-sm sm:text-base">Database Location</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted font-mono break-all">~/.9router/db.json</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="download"
|
||||
onClick={handleExportDatabase}
|
||||
loading={dbLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Download Backup
|
||||
</Button>
|
||||
@@ -388,6 +389,7 @@ export default function ProfilePage() {
|
||||
icon="upload"
|
||||
onClick={() => importFileRef.current?.click()}
|
||||
disabled={dbLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Import Backup
|
||||
</Button>
|
||||
@@ -410,16 +412,16 @@ export default function ProfilePage() {
|
||||
{/* Security */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-primary/10 text-primary">
|
||||
<div className="p-2 rounded-lg bg-primary/10 text-primary shrink-0">
|
||||
<span className="material-symbols-outlined text-[20px]">shield</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Security</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold">Security</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Require login</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Require login</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
When ON, dashboard requires password. When OFF, access without login.
|
||||
</p>
|
||||
</div>
|
||||
@@ -433,7 +435,7 @@ export default function ProfilePage() {
|
||||
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4 pt-4 border-t border-border/50">
|
||||
{settings.hasPassword && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Current Password</label>
|
||||
<label className="text-xs sm:text-sm font-medium">Current Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter current password"
|
||||
@@ -450,9 +452,9 @@ export default function ProfilePage() {
|
||||
</p>
|
||||
</div>
|
||||
)} */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">New Password</label>
|
||||
<label className="text-xs sm:text-sm font-medium">New Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
@@ -462,7 +464,7 @@ export default function ProfilePage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Confirm New Password</label>
|
||||
<label className="text-xs sm:text-sm font-medium">Confirm New Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
@@ -474,13 +476,13 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
|
||||
{passStatus.message && (
|
||||
<p className={`text-sm ${passStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
|
||||
<p className={`text-xs sm:text-sm ${passStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
|
||||
{passStatus.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="pt-2">
|
||||
<Button type="submit" variant="primary" loading={passLoading}>
|
||||
<Button type="submit" variant="primary" loading={passLoading} className="w-full sm:w-auto">
|
||||
{settings.hasPassword ? "Update Password" : "Set Password"}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -492,16 +494,16 @@ export default function ProfilePage() {
|
||||
{/* Routing Preferences */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500 shrink-0">
|
||||
<span className="material-symbols-outlined text-[20px]">route</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Routing Strategy</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold">Routing Strategy</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Round Robin</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Round Robin</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
Cycle through accounts to distribute load
|
||||
</p>
|
||||
</div>
|
||||
@@ -514,10 +516,10 @@ export default function ProfilePage() {
|
||||
|
||||
{/* Sticky Round Robin Limit */}
|
||||
{settings.fallbackStrategy === "round-robin" && (
|
||||
<div className="flex items-center justify-between pt-2 border-t border-border/50">
|
||||
<div>
|
||||
<p className="font-medium">Sticky Limit</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4 pt-2 border-t border-border/50">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Sticky Limit</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
Calls per account before switching
|
||||
</p>
|
||||
</div>
|
||||
@@ -528,16 +530,16 @@ export default function ProfilePage() {
|
||||
value={settings.stickyRoundRobinLimit || 3}
|
||||
onChange={(e) => updateStickyLimit(e.target.value)}
|
||||
disabled={loading}
|
||||
className="w-20 text-center"
|
||||
className="w-16 sm:w-20 text-center shrink-0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Combo Round Robin */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-border/50">
|
||||
<div>
|
||||
<p className="font-medium">Combo Round Robin</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4 pt-4 border-t border-border/50">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Combo Round Robin</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
Cycle through providers in combos instead of always starting with first
|
||||
</p>
|
||||
</div>
|
||||
@@ -559,17 +561,17 @@ export default function ProfilePage() {
|
||||
{/* Network */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500">
|
||||
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500 shrink-0">
|
||||
<span className="material-symbols-outlined text-[20px]">wifi</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Network</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold">Network</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Outbound Proxy</p>
|
||||
<p className="text-sm text-text-muted">Enable proxy for OAuth + provider outbound requests.</p>
|
||||
<div className="flex items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Outbound Proxy</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">Enable proxy for OAuth + provider outbound requests.</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={settings.outboundProxyEnabled === true}
|
||||
@@ -581,38 +583,39 @@ export default function ProfilePage() {
|
||||
{settings.outboundProxyEnabled === true && (
|
||||
<form onSubmit={updateOutboundProxy} className="flex flex-col gap-4 pt-2 border-t border-border/50">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="font-medium">Proxy URL</label>
|
||||
<label className="font-medium text-sm sm:text-base">Proxy URL</label>
|
||||
<Input
|
||||
placeholder="http://127.0.0.1:7897"
|
||||
value={proxyForm.outboundProxyUrl}
|
||||
onChange={(e) => setProxyForm((prev) => ({ ...prev, outboundProxyUrl: e.target.value }))}
|
||||
disabled={loading || proxyLoading}
|
||||
/>
|
||||
<p className="text-sm text-text-muted">Leave empty to inherit existing env proxy (if any).</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">Leave empty to inherit existing env proxy (if any).</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 pt-2 border-t border-border/50">
|
||||
<label className="font-medium">No Proxy</label>
|
||||
<label className="font-medium text-sm sm:text-base">No Proxy</label>
|
||||
<Input
|
||||
placeholder="localhost,127.0.0.1"
|
||||
value={proxyForm.outboundNoProxy}
|
||||
onChange={(e) => setProxyForm((prev) => ({ ...prev, outboundNoProxy: e.target.value }))}
|
||||
disabled={loading || proxyLoading}
|
||||
/>
|
||||
<p className="text-sm text-text-muted">Comma-separated hostnames/domains to bypass the proxy.</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">Comma-separated hostnames/domains to bypass the proxy.</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-border/50 flex items-center gap-2">
|
||||
<div className="pt-2 border-t border-border/50 flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
loading={proxyTestLoading}
|
||||
disabled={loading || proxyLoading}
|
||||
onClick={testOutboundProxy}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Test proxy URL
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" loading={proxyLoading}>
|
||||
<Button type="submit" variant="primary" loading={proxyLoading} className="w-full sm:w-auto">
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
@@ -620,7 +623,7 @@ export default function ProfilePage() {
|
||||
)}
|
||||
|
||||
{proxyStatus.message && (
|
||||
<p className={`text-sm ${proxyStatus.type === "error" ? "text-red-500" : "text-green-500"} pt-2 border-t border-border/50`}>
|
||||
<p className={`text-xs sm:text-sm ${proxyStatus.type === "error" ? "text-red-500" : "text-green-500"} pt-2 border-t border-border/50`}>
|
||||
{proxyStatus.message}
|
||||
</p>
|
||||
)}
|
||||
@@ -630,15 +633,15 @@ export default function ProfilePage() {
|
||||
{/* Observability Settings */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-orange-500/10 text-orange-500">
|
||||
<div className="p-2 rounded-lg bg-orange-500/10 text-orange-500 shrink-0">
|
||||
<span className="material-symbols-outlined text-[20px]">monitoring</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Observability</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold">Observability</h3>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Enable Observability</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Enable Observability</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
Record request details for inspection in the logs view
|
||||
</p>
|
||||
</div>
|
||||
@@ -651,7 +654,7 @@ export default function ProfilePage() {
|
||||
</Card>
|
||||
|
||||
{/* App Info */}
|
||||
<div className="text-center text-sm text-text-muted py-4">
|
||||
<div className="text-center text-xs sm:text-sm text-text-muted py-4">
|
||||
<p>{APP_CONFIG.name} v{APP_CONFIG.version}</p>
|
||||
<p className="mt-1">Local Mode - All data stored on your machine</p>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user