Enhance DNS record management by adding auto IP detection, notes field, and improved UI.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/e33594f7-ad24-43f5-a4e1-0d9d2f261947.jpg
This commit is contained in:
alphaeusmote
2025-04-10 00:17:00 +00:00
5 changed files with 214 additions and 25 deletions
@@ -28,6 +28,11 @@ export function Sidebar() {
href: "/", href: "/",
icon: <LayoutDashboard className="w-5 h-5 mr-3" />, icon: <LayoutDashboard className="w-5 h-5 mr-3" />,
}, },
{
title: "Organizations",
href: "/organizations",
icon: <Users className="w-5 h-5 mr-3" />,
},
{ {
title: "Domains", title: "Domains",
href: "/domains", href: "/domains",
@@ -0,0 +1,35 @@
import { Button } from "@/components/ui/button";
import { ChevronLeft } from "lucide-react";
import { useLocation } from "wouter";
interface BackButtonProps {
to?: string;
fallbackPath?: string;
className?: string;
}
export function BackButton({ to, fallbackPath = "/", className = "" }: BackButtonProps) {
const [_, setLocation] = useLocation();
const handleBack = () => {
if (to) {
setLocation(to);
} else if (window.history.length > 2) {
window.history.back();
} else {
setLocation(fallbackPath);
}
};
return (
<Button
variant="ghost"
size="sm"
onClick={handleBack}
className={`flex items-center gap-1 px-2 ${className}`}
>
<ChevronLeft className="h-4 w-4" />
<span>Back</span>
</Button>
);
}
+134 -19
View File
@@ -54,10 +54,16 @@ import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@/components/ui/tooltip";
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
import { RecentActivity } from "@/components/activity/recent-activity"; import { RecentActivity } from "@/components/activity/recent-activity";
import { Pagination } from "@/components/shared/pagination"; import { Pagination } from "@/components/shared/pagination";
import { Loader2, Home, Plus, Pencil, Trash2, ArrowLeft } from "lucide-react"; import { Loader2, Home, Plus, Pencil, Trash2, ArrowLeft, FileText } from "lucide-react";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { Link } from "wouter"; import { Link } from "wouter";
@@ -69,6 +75,8 @@ const dnsRecordSchema = z.object({
ttl: z.number().int().min(1).default(3600), ttl: z.number().int().min(1).default(3600),
proxied: z.boolean().default(false), proxied: z.boolean().default(false),
isActive: z.boolean().default(true), isActive: z.boolean().default(true),
isAutoIP: z.boolean().default(false),
notes: z.string().optional(),
}); });
export default function DnsRecordsPage() { export default function DnsRecordsPage() {
@@ -113,6 +121,8 @@ export default function DnsRecordsPage() {
ttl: 3600, ttl: 3600,
proxied: false, proxied: false,
isActive: true, isActive: true,
isAutoIP: false,
notes: "",
}, },
}); });
@@ -123,9 +133,11 @@ export default function DnsRecordsPage() {
name: selectedRecord.name, name: selectedRecord.name,
type: selectedRecord.type as any, type: selectedRecord.type as any,
content: selectedRecord.content, content: selectedRecord.content,
ttl: selectedRecord.ttl, ttl: selectedRecord.ttl ?? 3600,
proxied: selectedRecord.proxied, proxied: selectedRecord.proxied ?? false,
isActive: selectedRecord.isActive, isActive: selectedRecord.isActive,
isAutoIP: selectedRecord.isAutoIP ?? false,
notes: selectedRecord.notes ?? "",
}); });
} else { } else {
form.reset({ form.reset({
@@ -135,6 +147,8 @@ export default function DnsRecordsPage() {
ttl: 3600, ttl: 3600,
proxied: false, proxied: false,
isActive: true, isActive: true,
isAutoIP: false,
notes: "",
}); });
} }
}, [selectedRecord, form]); }, [selectedRecord, form]);
@@ -289,7 +303,7 @@ export default function DnsRecordsPage() {
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink>{domain?.name || 'Loading...'}</BreadcrumbLink> <BreadcrumbLink href="#">{domain?.name || 'Loading...'}</BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
</Breadcrumb> </Breadcrumb>
</div> </div>
@@ -345,6 +359,11 @@ export default function DnsRecordsPage() {
<TableCell className="font-medium">{record.name}</TableCell> <TableCell className="font-medium">{record.name}</TableCell>
<TableCell> <TableCell>
<Badge variant="outline">{record.type}</Badge> <Badge variant="outline">{record.type}</Badge>
{record.isAutoIP && (record.type === 'A' || record.type === 'AAAA') && (
<Badge variant="outline" className="ml-1 bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300">
Auto IP
</Badge>
)}
</TableCell> </TableCell>
<TableCell className="font-mono text-sm"> <TableCell className="font-mono text-sm">
{record.content.length > 30 {record.content.length > 30
@@ -369,21 +388,37 @@ export default function DnsRecordsPage() {
: "Never"} : "Never"}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button <div className="flex items-center justify-end">
variant="ghost" {record.notes && (
size="icon" <TooltipProvider>
onClick={() => handleEditRecord(record)} <Tooltip>
> <TooltipTrigger asChild>
<Pencil className="h-4 w-4" /> <Button variant="ghost" size="icon">
</Button> <FileText className="h-4 w-4 text-muted-foreground" />
<Button </Button>
variant="ghost" </TooltipTrigger>
size="icon" <TooltipContent side="left">
onClick={() => handleDeleteRecord(record)} <p className="font-normal">{record.notes}</p>
className="text-destructive" </TooltipContent>
> </Tooltip>
<Trash2 className="h-4 w-4" /> </TooltipProvider>
</Button> )}
<Button
variant="ghost"
size="icon"
onClick={() => handleEditRecord(record)}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteRecord(record)}
className="text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
@@ -559,6 +594,46 @@ export default function DnsRecordsPage() {
)} )}
/> />
{(form.watch("type") === "A" || form.watch("type") === "AAAA") && (
<FormField
control={form.control}
name="isAutoIP"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Auto IP Address</FormLabel>
<FormDescription>
Automatically determine IP address using STUN
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<Input placeholder="Optional notes about this record" {...field} />
</FormControl>
<FormDescription>
Add any additional information about this record
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter> <DialogFooter>
<Button <Button
type="submit" type="submit"
@@ -724,6 +799,46 @@ export default function DnsRecordsPage() {
)} )}
/> />
{(form.watch("type") === "A" || form.watch("type") === "AAAA") && (
<FormField
control={form.control}
name="isAutoIP"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Auto IP Address</FormLabel>
<FormDescription>
Automatically determine IP address using STUN
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<Input placeholder="Optional notes about this record" {...field} />
</FormControl>
<FormDescription>
Add any additional information about this record
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter> <DialogFooter>
<Button <Button
type="submit" type="submit"
+36 -6
View File
@@ -271,7 +271,20 @@ export class MemStorage implements IStorage {
const id = this.recordIdCounter++; const id = this.recordIdCounter++;
const createdAt = new Date(); const createdAt = new Date();
const lastUpdated = new Date(); const lastUpdated = new Date();
const newRecord: DnsRecord = { id, ...record, lastUpdated, createdAt }; const newRecord: DnsRecord = {
id,
domainId: record.domainId,
name: record.name,
type: record.type,
content: record.content,
ttl: record.ttl ?? 3600,
proxied: record.proxied ?? false,
isActive: record.isActive ?? true,
isAutoIP: record.isAutoIP ?? false,
notes: record.notes ?? null,
lastUpdated,
createdAt
};
this.recordsMap.set(id, newRecord); this.recordsMap.set(id, newRecord);
return newRecord; return newRecord;
} }
@@ -305,7 +318,14 @@ export class MemStorage implements IStorage {
async createProvider(provider: InsertProvider): Promise<Provider> { async createProvider(provider: InsertProvider): Promise<Provider> {
const id = this.providerIdCounter++; const id = this.providerIdCounter++;
const createdAt = new Date(); const createdAt = new Date();
const newProvider: Provider = { id, ...provider, createdAt }; const newProvider: Provider = {
id,
name: provider.name,
type: provider.type,
credentials: provider.credentials ?? null,
isActive: provider.isActive ?? true,
createdAt
};
this.providersMap.set(id, newProvider); this.providersMap.set(id, newProvider);
return newProvider; return newProvider;
} }
@@ -341,7 +361,17 @@ export class MemStorage implements IStorage {
async createApiToken(token: InsertApiToken): Promise<ApiToken> { async createApiToken(token: InsertApiToken): Promise<ApiToken> {
const id = this.apiTokenIdCounter++; const id = this.apiTokenIdCounter++;
const createdAt = new Date(); const createdAt = new Date();
const newToken: ApiToken = { id, ...token, createdAt }; const newToken: ApiToken = {
id,
name: token.name,
token: token.token,
organizationId: token.organizationId,
permissions: token.permissions ?? null,
createdBy: token.createdBy,
isActive: token.isActive ?? true,
expiresAt: token.expiresAt ?? null,
createdAt
};
this.apiTokensMap.set(id, newToken); this.apiTokensMap.set(id, newToken);
return newToken; return newToken;
} }
@@ -374,9 +404,9 @@ export class MemStorage implements IStorage {
id, id,
recordId, recordId,
action, action,
previousValue, previousValue: previousValue ?? null,
newValue, newValue: newValue ?? null,
userId, userId: userId ?? null,
timestamp timestamp
}; };
+4
View File
@@ -60,6 +60,8 @@ export const dnsRecords = pgTable("dns_records", {
ttl: integer("ttl").default(3600), ttl: integer("ttl").default(3600),
proxied: boolean("proxied").default(false), proxied: boolean("proxied").default(false),
isActive: boolean("is_active").default(true).notNull(), isActive: boolean("is_active").default(true).notNull(),
isAutoIP: boolean("is_auto_ip").default(false).notNull(),
notes: text("notes"),
lastUpdated: timestamp("last_updated"), lastUpdated: timestamp("last_updated"),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
}); });
@@ -117,6 +119,8 @@ export const insertDnsRecordSchema = createInsertSchema(dnsRecords).pick({
ttl: true, ttl: true,
proxied: true, proxied: true,
isActive: true, isActive: true,
isAutoIP: true,
notes: true,
}); });
export const insertProviderSchema = createInsertSchema(providers).pick({ export const insertProviderSchema = createInsertSchema(providers).pick({