2025-09-28 22:42:48 +01:00

653 lines
22 KiB
TypeScript

"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ArrowLeft, Copy, Key, Loader, Save, Trash2 } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react"; // Import useEffect
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { apiClient } from "@/lib/api-client";
import {
KeygenGroup,
KeygenLicense,
KeygenPolicy,
KeygenProduct,
KeygenUser,
} from "@/lib/types";
import { parseMetadata } from "@/lib/utils";
import { toast } from "sonner";
const licenseSchema = z.object({
name: z.string().min(1, "License name is required"),
user: z.string().optional(),
policy: z.string(),
expiry: z.string().min(1, "Expiry date is required"),
group: z.string().optional(),
product: z.string().optional(),
metadata: z.string().optional(),
});
type LicenseFormData = z.infer<typeof licenseSchema>;
export default function LicenseDetailPage() {
const params = useParams();
const router = useRouter();
const queryClient = useQueryClient();
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const licenseId = params.id as string;
const isNew = licenseId === "new";
const {
data: license,
isLoading: isLicenseLoading,
isFetched: isLicenseFetched,
} = useQuery({
queryKey: ["license", licenseId],
queryFn: async () => await apiClient.getLicense(licenseId),
enabled: !isNew,
});
const { data: policies, isLoading: policiesLoading } = useQuery({
queryKey: ["policies"],
queryFn: async () => await apiClient.getPolicies(1, 100),
staleTime: 1000,
});
const { data: users, isLoading: usersLoading } = useQuery({
queryKey: ["users"],
queryFn: async () => await apiClient.getUsers(1, 100),
});
const { data: products, isLoading: productsLoading } = useQuery({
queryKey: ["products"],
queryFn: async () => await apiClient.getProducts(1, 100),
});
const { data: groups, isLoading: groupsLoading } = useQuery({
queryKey: ["groups"],
queryFn: async () => await apiClient.getGroups(1, 100),
});
const createMutation = useMutation({
mutationFn: async (licenseData: Partial<KeygenLicense>) =>
await apiClient.createLicense({
attributes: licenseData.attributes,
relationships: licenseData.relationships,
}),
onSuccess: async (data) => {
await queryClient.invalidateQueries({ queryKey: ["licenses"] });
toast.success("License created successfully");
router.push(`/dashboard/licenses/${data.data.id}`);
},
onError: (error) => {
toast.error(error.message || "Failed to create license");
},
});
const updateMutation = useMutation({
mutationFn: async (licenseData: Partial<KeygenLicense>) =>
await apiClient.updateLicense(licenseId, {
attributes: licenseData.attributes,
relationships: licenseData.relationships,
}),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["licenses"] });
await queryClient.invalidateQueries({ queryKey: ["license", licenseId] });
toast.success("License updated successfully");
},
onError: (error) => {
toast.error(error.message || "Failed to update license");
},
});
const deleteMutation = useMutation({
mutationFn: () => apiClient.deleteLicense(licenseId),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["licenses"] });
toast.success("License deleted successfully");
router.push("/dashboard/licenses");
},
onError: (error) => {
toast.error(error.message || "Failed to delete license");
},
});
// Function to create relationships object
const getRelationships = (data: LicenseFormData) => ({
...(data.user && {
owner: {
data: { type: "users", id: data.user },
},
}),
...(data.policy && {
policy: {
data: { type: "policies", id: data.policy },
},
}),
...(data.product && {
product: {
data: { type: "products", id: data.product },
},
}),
...(data.group && {
group: {
data: { type: "groups", id: data.group },
},
}),
});
const onSubmit = async (data: LicenseFormData) => {
const formattedExpiry = new Date(data.expiry).toISOString();
const licenseData = {
attributes: {
name: data.name,
expiry: formattedExpiry,
metadata: parseMetadata(data.metadata || ""),
},
relationships: isNew ? getRelationships(data) : undefined,
};
if (isNew) {
await createMutation.mutateAsync(licenseData as Partial<KeygenLicense>);
} else {
await updateMutation.mutateAsync(licenseData as Partial<KeygenLicense>);
}
};
// Memoized default values
const defaultValues = useMemo(() => {
if (isNew || !license?.data) {
return {
name: "",
user: undefined,
policy: undefined,
expiry: "",
group: undefined,
product: undefined,
};
}
if (!license) return undefined;
const expiry = license.data.attributes.expiry
? new Date(license.data.attributes.expiry).toISOString().slice(0, 16)
: "";
console.log({license})
return {
name: license.data.attributes.name ?? "",
user: license.data.relationships?.owner?.data?.id,
policy: license.data.relationships?.policy?.data?.id,
expiry,
group: license.data.relationships?.group?.data?.id,
product: license.data.relationships?.product?.data?.id,
metadata: license.data.attributes.metadata
? JSON.stringify(license.data.attributes.metadata)
: undefined,
};
}, [isNew, license]);
const form = useForm<LicenseFormData>({
resolver: zodResolver(licenseSchema),
mode: "onBlur",
defaultValues,
});
useEffect(() => {
if (
!isNew &&
isLicenseFetched &&
license?.data &&
!form.formState.isDirty
) {
form.reset(defaultValues);
} else if (isNew && !form.formState.isDirty) {
form.reset(defaultValues);
}
}, [form, isNew, isLicenseFetched, license?.data, defaultValues]);
const handleDelete = () => {
deleteMutation.mutate();
setIsDeleteDialogOpen(false);
};
const copyLicenseKey = (key: string) => {
navigator.clipboard.writeText(key);
toast("License key copied to clipboard");
};
const allLoading =
isLicenseLoading ||
policiesLoading ||
usersLoading ||
productsLoading ||
groupsLoading;
if (allLoading || !defaultValues) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<Loader className="animate-spin h-8 w-8 text-blue-600 mx-auto mb-4" />
<p>Loading license details...</p>
</div>
</div>
);
}
if (!isNew && !license?.data) {
return (
<div className="flex flex-col items-center justify-center h-64">
<p className="text-lg text-red-500 mb-4">License not found.</p>
<Button onClick={() => router.push("/dashboard/licenses")}>
Go to Licenses
</Button>
</div>
);
}
console.log({ defaultValues });
return (
<>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/dashboard/licenses")}
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Licenses
</Button>
<div>
<h1 className="text-3xl font-bold tracking-tight">
{isNew ? "Create License" : "Edit License"}
</h1>
<p className="text-muted-foreground">
{isNew
? "Generate a new license"
: "Update license information and settings"}
</p>
</div>
</div>
{!isNew && (
<Button
variant="destructive"
onClick={() => setIsDeleteDialogOpen(true)}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete License
</Button>
)}
</div>
{!isNew && license && (
<Card>
<CardHeader>
<CardTitle>License Key</CardTitle>
<CardDescription>
This is the generated license key for this license
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-2">
<code className="flex-1 bg-gray-100 px-3 py-2 rounded text-sm font-mono">
{/* It's safer to only show the key if it exists */}
{license.data.attributes.key
? "*********************************************************************************"
: "Not available"}
</code>
{license.data.attributes.key && (
<Button
variant="outline"
size="sm"
onClick={() => copyLicenseKey(license.data.attributes.key!)} // Assert non-null
>
<Copy className="h-4 w-4" />
</Button>
)}
</div>
<div className="mt-4 flex items-center space-x-4">
<Badge
variant={
license.data.attributes.status === "active"
? "default"
: "destructive"
}
>
{license.data.attributes.status}
</Badge>
<span className="text-sm text-muted-foreground">
Uses: {license.data.attributes.uses}
</span>
</div>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Key className="h-5 w-5" />
<span>License Information</span>
</CardTitle>
<CardDescription>
{isNew
? "Configure the new license settings"
: "Update the license information below"}
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
<div className="grid grid-cols-2 gap-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>License Name</FormLabel>
<FormControl>
<Input placeholder="Premium License" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiry"
render={({ field }) => (
<FormItem>
<FormLabel>Expiry Date</FormLabel>
<FormControl>
<Input type="datetime-local" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* USER SELECT */}
<FormField
control={form.control}
name="user"
render={({ field }) => (
<FormItem>
<FormLabel>Assign to User</FormLabel>
<Select
onValueChange={(value) =>
field.onChange(
value === "NO_USER" ? undefined : value
)
}
defaultValue={defaultValues.user || "NO_USER"}
>
<FormControl>
{/* Re-evaluate if this should be disabled for editing */}
<SelectTrigger disabled={!isNew} className="w-full">
<SelectValue placeholder="Select a user" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="NO_USER">
No user assigned
</SelectItem>
{users?.data.map((user: KeygenUser) => (
<SelectItem
className=""
key={user.id}
value={user.id}
>
<div className="flex items-center space-x-2">
<div className="flex flex-col">
<span className="font-medium">
{user.attributes?.firstName}{" "}
{user.attributes?.lastName}
</span>
<span className="text-xs text-muted-foreground">
{user.attributes?.email}
</span>
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* POLICY SELECT */}
<FormField
control={form.control}
name="policy"
render={({ field }) => (
<FormItem>
<FormLabel>Policy</FormLabel>
<Select
onValueChange={(value) =>
field.onChange(
value === "NO_POLICY" ? undefined : value
)
}
value={field.value || "NO_POLICY"} // Control the value for Select
>
<FormControl>
{/* Re-evaluate if this should be disabled for editing */}
<SelectTrigger disabled={!isNew} className="w-full">
<SelectValue placeholder="Select a policy" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="NO_POLICY">
No policy assigned
</SelectItem>
{policies?.data.map((policy: KeygenPolicy) => (
<SelectItem key={policy.id} value={policy.id}>
{policy.attributes.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* GROUP SELECT */}
<FormField
control={form.control}
name="group"
render={({ field }) => (
<FormItem>
<FormLabel>Assign to Group (Optional)</FormLabel>
<Select
onValueChange={(value) =>
field.onChange(
value === "NO_GROUP" ? undefined : value
)
}
value={field.value || "NO_GROUP"} // Control the value for Select
>
<FormControl>
{/* Re-evaluate if this should be disabled for editing */}
<SelectTrigger disabled={!isNew} className="w-full">
<SelectValue placeholder="Select a group" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="NO_GROUP">
No group assigned
</SelectItem>
{groups?.data.map((group: KeygenGroup) => (
<SelectItem
className=""
key={group.id}
value={group.id}
>
{group.attributes.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* PRODUCT SELECT */}
<FormField
control={form.control}
name="product"
render={({ field }) => (
<FormItem>
<FormLabel>Product (Optional)</FormLabel>
<Select
onValueChange={(value) =>
field.onChange(
value === "NO_PRODUCT" ? undefined : value
)
}
value={field.value || "NO_PRODUCT"} // Control the value for Select
>
<FormControl>
{/* Re-evaluate if this should be disabled for editing */}
<SelectTrigger disabled={!isNew} className="w-full">
<SelectValue placeholder="Select a product" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="NO_PRODUCT">
No product assigned
</SelectItem>
{products?.data.map((product: KeygenProduct) => (
<SelectItem key={product.id} value={product.id}>
{product.attributes.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metadata"
render={({ field }) => (
<FormItem>
<FormLabel>Metadata</FormLabel>
<FormControl>
<Textarea
placeholder='e.g. { "key":"value" }'
rows={4}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end space-x-4">
<Button
type="button"
variant="outline"
onClick={() => router.push("/dashboard/licenses")}
>
Cancel
</Button>
<Button
type="submit"
disabled={
createMutation.isPending || updateMutation.isPending
}
>
<Save className="h-4 w-4 mr-2" />
{createMutation.isPending || updateMutation.isPending
? "Saving..."
: isNew
? "Create License"
: "Update License"}
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
{/* Delete License Dialog */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete License</DialogTitle>
<DialogDescription>
Are you sure you want to delete this license? This action cannot
be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsDeleteDialogOpen(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? "Deleting..." : "Delete License"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</>
);
}