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: "/",
icon: <LayoutDashboard className="w-5 h-5 mr-3" />,
},
{
title: "Organizations",
href: "/organizations",
icon: <Users className="w-5 h-5 mr-3" />,
},
{
title: "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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
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 { RecentActivity } from "@/components/activity/recent-activity";
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 { Link } from "wouter";
@@ -69,6 +75,8 @@ const dnsRecordSchema = z.object({
ttl: z.number().int().min(1).default(3600),
proxied: z.boolean().default(false),
isActive: z.boolean().default(true),
isAutoIP: z.boolean().default(false),
notes: z.string().optional(),
});
export default function DnsRecordsPage() {
@@ -113,6 +121,8 @@ export default function DnsRecordsPage() {
ttl: 3600,
proxied: false,
isActive: true,
isAutoIP: false,
notes: "",
},
});
@@ -123,9 +133,11 @@ export default function DnsRecordsPage() {
name: selectedRecord.name,
type: selectedRecord.type as any,
content: selectedRecord.content,
ttl: selectedRecord.ttl,
proxied: selectedRecord.proxied,
ttl: selectedRecord.ttl ?? 3600,
proxied: selectedRecord.proxied ?? false,
isActive: selectedRecord.isActive,
isAutoIP: selectedRecord.isAutoIP ?? false,
notes: selectedRecord.notes ?? "",
});
} else {
form.reset({
@@ -135,6 +147,8 @@ export default function DnsRecordsPage() {
ttl: 3600,
proxied: false,
isActive: true,
isAutoIP: false,
notes: "",
});
}
}, [selectedRecord, form]);
@@ -289,7 +303,7 @@ export default function DnsRecordsPage() {
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink>{domain?.name || 'Loading...'}</BreadcrumbLink>
<BreadcrumbLink href="#">{domain?.name || 'Loading...'}</BreadcrumbLink>
</BreadcrumbItem>
</Breadcrumb>
</div>
@@ -345,6 +359,11 @@ export default function DnsRecordsPage() {
<TableCell className="font-medium">{record.name}</TableCell>
<TableCell>
<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 className="font-mono text-sm">
{record.content.length > 30
@@ -369,21 +388,37 @@ export default function DnsRecordsPage() {
: "Never"}
</TableCell>
<TableCell className="text-right">
<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 className="flex items-center justify-end">
{record.notes && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<FileText className="h-4 w-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">
<p className="font-normal">{record.notes}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<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>
</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>
<Button
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>
<Button
type="submit"
+36 -6
View File
@@ -271,7 +271,20 @@ export class MemStorage implements IStorage {
const id = this.recordIdCounter++;
const createdAt = 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);
return newRecord;
}
@@ -305,7 +318,14 @@ export class MemStorage implements IStorage {
async createProvider(provider: InsertProvider): Promise<Provider> {
const id = this.providerIdCounter++;
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);
return newProvider;
}
@@ -341,7 +361,17 @@ export class MemStorage implements IStorage {
async createApiToken(token: InsertApiToken): Promise<ApiToken> {
const id = this.apiTokenIdCounter++;
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);
return newToken;
}
@@ -374,9 +404,9 @@ export class MemStorage implements IStorage {
id,
recordId,
action,
previousValue,
newValue,
userId,
previousValue: previousValue ?? null,
newValue: newValue ?? null,
userId: userId ?? null,
timestamp
};
+4
View File
@@ -60,6 +60,8 @@ export const dnsRecords = pgTable("dns_records", {
ttl: integer("ttl").default(3600),
proxied: boolean("proxied").default(false),
isActive: boolean("is_active").default(true).notNull(),
isAutoIP: boolean("is_auto_ip").default(false).notNull(),
notes: text("notes"),
lastUpdated: timestamp("last_updated"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
@@ -117,6 +119,8 @@ export const insertDnsRecordSchema = createInsertSchema(dnsRecords).pick({
ttl: true,
proxied: true,
isActive: true,
isAutoIP: true,
notes: true,
});
export const insertProviderSchema = createInsertSchema(providers).pick({