fix: Add token editing, DDNS record management, wildcard display
Dashboard: - Cloudflare token can be changed when already configured (was only editable when missing/invalid) - DDNS records can be added and removed inline (was read-only) Services: - Wildcard services display as *.domain instead of domain with a separate subdomains badge — clearer at a glance Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import { Alert, AlertDescription } from './ui/alert';
|
||||
import { Badge } from './ui/badge';
|
||||
import {
|
||||
LayoutDashboard, Cloud, CheckCircle, XCircle, AlertCircle, Loader2,
|
||||
RefreshCw, BookOpen, Globe, Shield, Router, Server, RotateCw, Key,
|
||||
RefreshCw, BookOpen, Globe, Shield, Router, Server, RotateCw, Key, Plus, X,
|
||||
} from 'lucide-react';
|
||||
import { useCloudflare } from '../hooks/useCloudflare';
|
||||
import { useDdns } from '../hooks/useDdns';
|
||||
@@ -23,7 +23,7 @@ export function DashboardComponent() {
|
||||
const { verification, isLoading: cfLoading } = useCloudflare();
|
||||
const { status: ddnsStatus, isLoadingStatus: ddnsLoading, trigger: triggerDdns, isTriggering } = useDdns();
|
||||
const { status: certStatus } = useCert();
|
||||
const { config: globalConfig } = useConfig();
|
||||
const { config: globalConfig, updateConfig } = useConfig();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { data: globalSecrets } = useQuery({
|
||||
@@ -36,6 +36,8 @@ export function DashboardComponent() {
|
||||
|
||||
const [editingToken, setEditingToken] = useState(false);
|
||||
const [tokenValue, setTokenValue] = useState('');
|
||||
const [addingDdnsRecord, setAddingDdnsRecord] = useState(false);
|
||||
const [newDdnsRecord, setNewDdnsRecord] = useState('');
|
||||
|
||||
const updateSecretsMutation = useMutation({
|
||||
mutationFn: (values: Record<string, unknown>) => secretsApi.update(values),
|
||||
@@ -168,6 +170,29 @@ export function DashboardComponent() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!editingToken ? (
|
||||
<Button variant="ghost" size="sm" onClick={() => setEditingToken(true)} className="gap-1 text-muted-foreground">
|
||||
<Key className="h-3 w-3" />
|
||||
Change Token
|
||||
</Button>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="New Cloudflare API token..."
|
||||
value={tokenValue}
|
||||
onChange={(e) => setTokenValue(e.target.value)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => updateSecretsMutation.mutate({ cloudflare: { apiToken: tokenValue } })} disabled={!tokenValue}>
|
||||
Save
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => { setEditingToken(false); setTokenValue(''); }}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-7">
|
||||
@@ -249,13 +274,13 @@ export function DashboardComponent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Record list */}
|
||||
{/* Record list with remove buttons */}
|
||||
{globalConfig?.cloud?.ddns?.records?.length ? (
|
||||
<div className="space-y-1">
|
||||
{globalConfig.cloud.ddns.records.map((r) => {
|
||||
const rs: DdnsRecordStatus | undefined = ddnsStatus.records?.find(s => s.name === r);
|
||||
{globalConfig.cloud.ddns.records.map((r: string) => {
|
||||
const rs: DdnsRecordStatus | undefined = ddnsStatus.records?.find((s: DdnsRecordStatus) => s.name === r);
|
||||
return (
|
||||
<div key={r} className="flex items-center gap-2 text-xs">
|
||||
<div key={r} className="flex items-center gap-2 text-xs group">
|
||||
{rs?.ok ? (
|
||||
<CheckCircle className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
||||
) : rs && !rs.ok ? (
|
||||
@@ -270,12 +295,57 @@ export function DashboardComponent() {
|
||||
{rs && !rs.ok && rs.error && (
|
||||
<span className="text-red-500 truncate" title={rs.error}>{rs.error}</span>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 ml-auto"
|
||||
onClick={async () => {
|
||||
if (!globalConfig) return;
|
||||
const records = (globalConfig.cloud?.ddns?.records || []).filter((rec: string) => rec !== r);
|
||||
await updateConfig({
|
||||
...globalConfig,
|
||||
cloud: { ...globalConfig.cloud, ddns: { ...globalConfig.cloud?.ddns, records } },
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Add DDNS record */}
|
||||
{!addingDdnsRecord ? (
|
||||
<Button variant="ghost" size="sm" onClick={() => setAddingDdnsRecord(true)} className="gap-1 text-muted-foreground">
|
||||
<Plus className="h-3 w-3" />
|
||||
Add Record
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input
|
||||
value={newDdnsRecord}
|
||||
onChange={(e) => setNewDdnsRecord(e.target.value)}
|
||||
placeholder="dev.payne.io"
|
||||
className="h-8 text-xs font-mono flex-1"
|
||||
/>
|
||||
<Button size="sm" className="h-8" disabled={!newDdnsRecord} onClick={async () => {
|
||||
if (!globalConfig || !newDdnsRecord) return;
|
||||
const records = [...(globalConfig.cloud?.ddns?.records || []), newDdnsRecord];
|
||||
await updateConfig({
|
||||
...globalConfig,
|
||||
cloud: { ...globalConfig.cloud, ddns: { ...globalConfig.cloud?.ddns, records } },
|
||||
});
|
||||
setNewDdnsRecord('');
|
||||
setAddingDdnsRecord(false);
|
||||
}}>Add</Button>
|
||||
<Button size="sm" variant="ghost" className="h-8" onClick={() => { setAddingDdnsRecord(false); setNewDdnsRecord(''); }}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
||||
@@ -108,7 +108,7 @@ function ServiceRow({ service, isExpanded, onToggle, certDomains, ddnsDomains, d
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
<span className="font-mono text-sm font-medium truncate">{service.domain}</span>
|
||||
<span className="font-mono text-sm font-medium truncate">{service.subdomains ? `*.${service.domain}` : service.domain}</span>
|
||||
<Badge variant="outline" className="text-xs h-4 px-1 shrink-0">{typeBadge}</Badge>
|
||||
<Badge
|
||||
variant={service.reach === 'public' ? 'success' : 'default'}
|
||||
@@ -116,9 +116,6 @@ function ServiceRow({ service, isExpanded, onToggle, certDomains, ddnsDomains, d
|
||||
>
|
||||
{service.reach}
|
||||
</Badge>
|
||||
{service.subdomains && (
|
||||
<Badge variant="outline" className="text-xs h-4 px-1 shrink-0">*.sub</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<StatusDot status="ok" label="DNS" />
|
||||
|
||||
Reference in New Issue
Block a user