details improvement and unnecessary files deletion

This commit is contained in:
yoan
2024-11-24 13:36:17 +08:00
parent 37df882ed3
commit 9ff3e22c80
57 changed files with 978 additions and 5047 deletions

View File

@@ -15,10 +15,7 @@ import {
} from "@/components/ui/alert-dialog.tsx";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import AccessEditDialog from "@/components/certimate/AccessEditDialog";
import AccessGroupEdit from "@/components/certimate/AccessGroupEdit";
import AccessGroupList from "@/components/certimate/AccessGroupList";
import XPagination from "@/components/certimate/XPagination";
import { convertZulu2Beijing } from "@/lib/time";
import { Access as AccessType, accessProvidersMap } from "@/domain/access";
@@ -40,10 +37,6 @@ const Access = () => {
const page = query.get("page");
const pageNumber = page ? Number(page) : 1;
const tab = query.get("tab");
const accessGroupId = query.get("accessGroupId");
const startIndex = (pageNumber - 1) * perPage;
const endIndex = startIndex + perPage;
@@ -52,143 +45,104 @@ const Access = () => {
deleteAccess(rs.id);
};
const handleTabItemClick = (tab: string) => {
query.set("tab", tab);
navigate({ search: query.toString() });
};
return (
<div className="">
<div className="flex justify-between items-center">
<div className="text-muted-foreground">{t("access.page.title")}</div>
{tab != "access_group" ? (
<AccessEditDialog trigger={<Button>{t("access.authorization.add")}</Button>} op="add" />
) : (
<AccessGroupEdit trigger={<Button>{t("access.group.add")}</Button>} />
)}
<AccessEditDialog trigger={<Button>{t("access.authorization.add")}</Button>} op="add" />
</div>
<Tabs defaultValue={tab ? tab : "access"} value={tab ? tab : "access"} className="w-full mt-5">
<TabsList className="space-x-5 px-3">
<TabsTrigger
value="access"
onClick={() => {
handleTabItemClick("access");
}}
>
{t("access.authorization.tab")}
</TabsTrigger>
<TabsTrigger
value="access_group"
onClick={() => {
handleTabItemClick("access_group");
}}
>
{t("access.group.tab")}
</TabsTrigger>
</TabsList>
<TabsContent value="access">
{accesses.length === 0 ? (
<div className="flex flex-col items-center mt-10">
<span className="bg-orange-100 p-5 rounded-full">
<Key size={40} className="text-primary" />
</span>
{accesses.length === 0 ? (
<div className="flex flex-col items-center mt-10">
<span className="bg-orange-100 p-5 rounded-full">
<Key size={40} className="text-primary" />
</span>
<div className="text-center text-sm text-muted-foreground mt-3">{t("access.authorization.nodata")}</div>
<AccessEditDialog trigger={<Button>{t("access.authorization.add")}</Button>} op="add" className="mt-3" />
</div>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48">{t("common.text.name")}</div>
<div className="w-48">{t("common.text.provider")}</div>
<div className="text-center text-sm text-muted-foreground mt-3">{t("access.authorization.nodata")}</div>
<AccessEditDialog trigger={<Button>{t("access.authorization.add")}</Button>} op="add" className="mt-3" />
</div>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48">{t("common.text.name")}</div>
<div className="w-48">{t("common.text.provider")}</div>
<div className="w-60">{t("common.text.created_at")}</div>
<div className="w-60">{t("common.text.updated_at")}</div>
<div className="grow">{t("common.text.operations")}</div>
<div className="w-60">{t("common.text.created_at")}</div>
<div className="w-60">{t("common.text.updated_at")}</div>
<div className="grow">{t("common.text.operations")}</div>
</div>
{accesses.slice(startIndex, endIndex).map((access) => (
<div
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
key={access.id}
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-start">
<div className="pr-3 truncate">{access.name}</div>
</div>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center space-x-2">
<img src={accessProvidersMap.get(access.configType)?.icon} className="w-6" />
<div>{t(accessProvidersMap.get(access.configType)?.name || "")}</div>
</div>
{accesses
.filter((item) => {
return accessGroupId ? item.group == accessGroupId : true;
})
.slice(startIndex, endIndex)
.map((access) => (
<div
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
key={access.id}
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-start">
<div className="pr-3 truncate">{access.name}</div>
</div>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center space-x-2">
<img src={accessProvidersMap.get(access.configType)?.icon} className="w-6" />
<div>{t(accessProvidersMap.get(access.configType)?.name || "")}</div>
</div>
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">{access.created && convertZulu2Beijing(access.created)}</div>
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">{access.updated && convertZulu2Beijing(access.updated)}</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0">
<AccessEditDialog
trigger={
<Button variant={"link"} className="p-0">
{t("common.edit")}
</Button>
}
op="edit"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<AccessEditDialog
trigger={
<Button variant={"link"} className="p-0">
{t("common.copy")}
</Button>
}
op="copy"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
{t("common.delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="dark:text-gray-200">{t("access.authorization.delete")}</AlertDialogTitle>
<AlertDialogDescription>{t("access.authorization.delete.confirm")}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="dark:text-gray-200">{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDelete(access);
}}
>
{t("common.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
))}
<XPagination
totalPages={totalPages}
currentPage={pageNumber}
onPageChange={(page) => {
query.set("page", page.toString());
navigate({ search: query.toString() });
}}
/>
</>
)}
</TabsContent>
<TabsContent value="access_group">
<AccessGroupList />
</TabsContent>
</Tabs>
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">{access.created && convertZulu2Beijing(access.created)}</div>
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">{access.updated && convertZulu2Beijing(access.updated)}</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0">
<AccessEditDialog
trigger={
<Button variant={"link"} className="p-0">
{t("common.edit")}
</Button>
}
op="edit"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<AccessEditDialog
trigger={
<Button variant={"link"} className="p-0">
{t("common.copy")}
</Button>
}
op="copy"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
{t("common.delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="dark:text-gray-200">{t("access.authorization.delete")}</AlertDialogTitle>
<AlertDialogDescription>{t("access.authorization.delete.confirm")}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="dark:text-gray-200">{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDelete(access);
}}
>
{t("common.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
))}
<XPagination
totalPages={totalPages}
currentPage={pageNumber}
onPageChange={(page) => {
query.set("page", page.toString());
navigate({ search: query.toString() });
}}
/>
</>
)}
</div>
);
};

View File

@@ -1,503 +0,0 @@
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { ChevronsUpDown, Plus, CircleHelp } from "lucide-react";
import { ClientResponseError } from "pocketbase";
import { Button } from "@/components/ui/button";
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Toaster } from "@/components/ui/toaster";
import { useToast } from "@/components/ui/use-toast";
import AccessEditDialog from "@/components/certimate/AccessEditDialog";
import DeployList from "@/components/certimate/DeployList";
import EmailsEdit from "@/components/certimate/EmailsEdit";
import StringList from "@/components/certimate/StringList";
import { cn } from "@/lib/utils";
import { PbErrorData } from "@/domain/base";
import { accessProvidersMap } from "@/domain/access";
import { EmailsSetting } from "@/domain/settings";
import { DeployConfig, Domain } from "@/domain/domain";
import { save, get } from "@/repository/domains";
import { useConfigContext } from "@/providers/config";
import { Switch } from "@/components/ui/switch";
import { TooltipFast } from "@/components/ui/tooltip";
const Edit = () => {
const {
config: { accesses, emails },
} = useConfigContext();
const [domain, setDomain] = useState<Domain>({} as Domain);
const location = useLocation();
const { t } = useTranslation();
const [tab, setTab] = useState<"apply" | "deploy">("apply");
useEffect(() => {
// Parsing query parameters
const queryParams = new URLSearchParams(location.search);
const id = queryParams.get("id");
if (id) {
const fetchData = async () => {
const data = await get(id);
setDomain(data);
};
fetchData();
}
}, [location.search]);
const formSchema = z.object({
id: z.string().optional(),
domain: z.string().min(1, {
message: "common.errmsg.domain_invalid",
}),
email: z.string().email("common.errmsg.email_invalid").optional(),
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "domain.application.form.access.placeholder",
}),
keyAlgorithm: z.string().optional(),
nameservers: z.string().optional(),
timeout: z.number().optional(),
disableFollowCNAME: z.boolean().optional(),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: "",
domain: "",
email: "",
access: "",
keyAlgorithm: "RSA2048",
nameservers: "",
timeout: 60,
disableFollowCNAME: true,
},
});
useEffect(() => {
if (domain) {
form.reset({
id: domain.id,
domain: domain.domain,
email: domain.applyConfig?.email,
access: domain.applyConfig?.access,
keyAlgorithm: domain.applyConfig?.keyAlgorithm,
nameservers: domain.applyConfig?.nameservers,
timeout: domain.applyConfig?.timeout,
disableFollowCNAME: domain.applyConfig?.disableFollowCNAME,
});
}
}, [domain, form]);
const { toast } = useToast();
const onSubmit = async (data: z.infer<typeof formSchema>) => {
const req: Domain = {
id: data.id as string,
crontab: "0 0 * * *",
domain: data.domain,
email: data.email,
access: data.access,
applyConfig: {
email: data.email ?? "",
access: data.access,
keyAlgorithm: data.keyAlgorithm,
nameservers: data.nameservers,
timeout: data.timeout,
disableFollowCNAME: data.disableFollowCNAME,
},
};
try {
const resp = await save(req);
let description = t("domain.application.form.domain.changed.message");
if (req.id == "") {
description = t("domain.application.form.domain.added.message");
}
toast({
title: t("common.save.succeeded.message"),
description,
});
if (!domain?.id) setTab("deploy");
setDomain({ ...resp });
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
});
return;
}
};
const handelOnDeployListChange = async (list: DeployConfig[]) => {
const req = {
...domain,
deployConfig: list,
};
try {
const resp = await save(req);
let description = t("domain.application.form.domain.changed.message");
if (req.id == "") {
description = t("domain.application.form.domain.added.message");
}
toast({
title: t("common.save.succeeded.message"),
description,
});
if (!domain?.id) setTab("deploy");
setDomain({ ...resp });
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
});
return;
}
};
return (
<>
<div className="">
<Toaster />
<div className="h-5 text-muted-foreground">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#/domains">{t("domain.page.title")}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{domain?.id ? t("domain.edit") : t("domain.add")}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="flex flex-col justify-center w-full mt-5 md:space-x-10 md:flex-row">
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex md:mt-5">
<div
className={cn("cursor-pointer text-right", tab === "apply" ? "text-primary" : "")}
onClick={() => {
setTab("apply");
}}
>
{t("domain.application.tab")}
</div>
<div
className={cn("cursor-pointer text-right", tab === "deploy" ? "text-primary" : "")}
onClick={() => {
if (!domain?.id) {
toast({
title: t("domain.application.unsaved.message"),
description: t("domain.application.unsaved.message"),
variant: "destructive",
});
return;
}
setTab("deploy");
}}
>
{t("domain.deployment.tab")}
</div>
</div>
<div className="flex flex-col">
<div className={cn("w-full md:w-[35em] p-5 rounded mt-3 md:mt-0", tab == "deploy" && "hidden")}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 dark:text-stone-200">
{/* 域名 */}
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<>
<StringList
value={field.value}
valueType="domain"
onValueChange={(domain: string) => {
form.setValue("domain", domain);
}}
/>
</>
<FormMessage />
</FormItem>
)}
/>
{/* 邮箱 */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="flex justify-between w-full">
<div>{t("domain.application.form.email.label") + " " + t("domain.application.form.email.tips")}</div>
<EmailsEdit
trigger={
<div className="flex items-center font-normal cursor-pointer text-primary hover:underline">
<Plus size={14} />
{t("common.add")}
</div>
}
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("email", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.application.form.email.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.application.form.email.list")}</SelectLabel>
{(emails.content as EmailsSetting).emails.map((item) => (
<SelectItem key={item} value={item}>
<div>{item}</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* DNS 服务商授权 */}
<FormField
control={form.control}
name="access"
render={({ field }) => (
<FormItem>
<FormLabel className="flex justify-between w-full">
<div>{t("domain.application.form.access.label")}</div>
<AccessEditDialog
trigger={
<div className="flex items-center font-normal cursor-pointer text-primary hover:underline">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("access", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.application.form.access.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.application.form.access.list")}</SelectLabel>
{accesses
.filter((item) => item.usage != "deploy")
.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img className="w-6" src={accessProvidersMap.get(item.configType)?.icon} />
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div>
<hr />
<Collapsible>
<CollapsibleTrigger className="w-full my-4">
<div className="flex items-center justify-between space-x-4">
<span className="flex-1 text-sm text-left text-gray-600">{t("domain.application.form.advanced_settings.label")}</span>
<ChevronsUpDown className="w-4 h-4" />
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col space-y-8">
{/* 证书算法 */}
<FormField
control={form.control}
name="keyAlgorithm"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain.application.form.key_algorithm.label")}</FormLabel>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("keyAlgorithm", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.application.form.key_algorithm.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="RSA2048">RSA2048</SelectItem>
<SelectItem value="RSA3072">RSA3072</SelectItem>
<SelectItem value="RSA4096">RSA4096</SelectItem>
<SelectItem value="RSA8192">RSA8192</SelectItem>
<SelectItem value="EC256">EC256</SelectItem>
<SelectItem value="EC384">EC384</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
{/* DNS */}
<FormField
control={form.control}
name="nameservers"
render={({ field }) => (
<FormItem>
<StringList
value={field.value ?? ""}
onValueChange={(val: string) => {
form.setValue("nameservers", val);
}}
valueType="dns"
></StringList>
<FormMessage />
</FormItem>
)}
/>
{/* DNS 超时时间 */}
<FormField
control={form.control}
name="timeout"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain.application.form.timeout.label")}</FormLabel>
<FormControl>
<Input
type="number"
placeholder={t("domain.application.form.timeout.placeholder")}
{...field}
value={field.value}
onChange={(e) => {
form.setValue("timeout", parseInt(e.target.value));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 禁用 CNAME 跟随 */}
<FormField
control={form.control}
name="disableFollowCNAME"
render={({ field }) => (
<FormItem>
<FormLabel>
<div className="flex">
<span className="mr-1">{t("domain.application.form.disable_follow_cname.label")} </span>
<TooltipFast
className="max-w-[20rem]"
contentView={
<p>
{t("domain.application.form.disable_follow_cname.tips")}
<a
className="text-primary"
target="_blank"
href="https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname"
>
{t("domain.application.form.disable_follow_cname.tips_link")}
</a>
</p>
}
>
<CircleHelp size={14} />
</TooltipFast>
</div>
</FormLabel>
<FormControl>
<div>
<Switch
defaultChecked={field.value}
onCheckedChange={(value) => {
form.setValue(field.name, value);
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CollapsibleContent>
</Collapsible>
</div>
<div className="flex justify-end">
<Button type="submit">{domain?.id ? t("common.save") : t("common.next")}</Button>
</div>
</form>
</Form>
</div>
<div className={cn("flex flex-col space-y-5 w-full md:w-[35em]", tab == "apply" && "hidden")}>
<DeployList
deploys={domain?.deployConfig ?? []}
onChange={(list: DeployConfig[]) => {
handelOnDeployListChange(list);
}}
/>
</div>
</div>
</div>
</div>
</>
);
};
export default Edit;

View File

@@ -1,339 +0,0 @@
import { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useTranslation, Trans } from "react-i18next";
import { TooltipContent, TooltipProvider } from "@radix-ui/react-tooltip";
import { Earth } from "lucide-react";
import Show from "@/components/Show";
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import XPagination from "@/components/certimate/XPagination";
import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialog,
AlertDialogContent,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Toaster } from "@/components/ui/toaster";
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
import { useToast } from "@/components/ui/use-toast";
import { CustomFile, saveFiles2ZIP } from "@/lib/file";
import { convertZulu2Beijing, getDate, getLeftDays } from "@/lib/time";
import { Domain } from "@/domain/domain";
import { list, remove, save, subscribeId, unsubscribeId } from "@/repository/domains";
const Home = () => {
const toast = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const location = useLocation();
const query = new URLSearchParams(location.search);
const page = query.get("page");
const state = query.get("state");
const [totalPage, setTotalPage] = useState(0);
const handleCreateClick = () => {
navigate("/edit");
};
const setPage = (newPage: number) => {
query.set("page", newPage.toString());
navigate(`?${query.toString()}`);
};
const handleEditClick = (id: string) => {
navigate(`/edit?id=${id}`);
};
const handleHistoryClick = (id: string) => {
navigate(`/history?domain=${id}`);
};
const handleDeleteClick = async (id: string) => {
try {
await remove(id);
setDomains(domains.filter((domain) => domain.id !== id));
} catch (error) {
console.error("Error deleting domain:", error);
}
};
const [domains, setDomains] = useState<Domain[]>([]);
useEffect(() => {
const fetchData = async () => {
const data = await list({
page: page ? Number(page) : 1,
perPage: 10,
state: state ? state : "",
});
setDomains(data.items);
setTotalPage(data.totalPages);
};
fetchData();
}, [page, state]);
const handelCheckedChange = async (id: string) => {
const checkedDomains = domains.filter((domain) => domain.id === id);
const isChecked = checkedDomains[0].enabled;
const data = checkedDomains[0];
data.enabled = !isChecked;
await save(data);
const updatedDomains = domains.map((domain) => {
if (domain.id === id) {
return { ...domain, checked: !isChecked };
}
return domain;
});
setDomains(updatedDomains);
};
const handleRightNowClick = async (domain: Domain) => {
try {
unsubscribeId(domain.id ?? "");
subscribeId(domain.id ?? "", (resp) => {
const updatedDomains = domains.map((domain) => {
if (domain.id === resp.id) {
return { ...resp };
}
return domain;
});
setDomains(updatedDomains);
});
domain.rightnow = true;
await save(domain);
toast.toast({
title: t("domain.deploy.started.message"),
description: t("domain.deploy.started.tips"),
});
} catch (e) {
toast.toast({
title: t("domain.deploy.failed.message"),
description: (
// 这里的 text 只是占位作用,实际文案在 src/i18n/locales/[lang].json
<Trans i18nKey="domain.deploy.failed.tips">
text1
<Link to={`/history?domain=${domain.id}`} className="underline text-blue-500">
text2
</Link>
text3
</Trans>
),
variant: "destructive",
});
}
};
const handleForceClick = async (domain: Domain) => {
await handleRightNowClick({ ...domain, deployed: false });
};
const handleDownloadClick = async (domain: Domain) => {
const zipName = `${domain.id}-${domain.domain}.zip`;
const files: CustomFile[] = [
{
name: `${domain.domain}.pem`,
content: domain.certificate ? domain.certificate : "",
},
{
name: `${domain.domain}.key`,
content: domain.privateKey ? domain.privateKey : "",
},
];
await saveFiles2ZIP(zipName, files);
};
return (
<>
<div className="">
<Toaster />
<div className="flex justify-between items-center">
<div className="text-muted-foreground">{t("domain.page.title")}</div>
<Button onClick={handleCreateClick}>{t("domain.add")}</Button>
</div>
{!domains.length ? (
<>
<div className="flex flex-col items-center mt-10">
<span className="bg-orange-100 p-5 rounded-full">
<Earth size={40} className="text-primary" />
</span>
<div className="text-center text-sm text-muted-foreground mt-3">{t("domain.nodata")}</div>
<Button onClick={handleCreateClick} className="mt-3">
{t("domain.add")}
</Button>
</div>
</>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-36">{t("common.text.domain")}</div>
<div className="w-40">{t("domain.props.expiry")}</div>
<div className="w-32">{t("domain.props.last_execution_status")}</div>
<div className="w-64">{t("domain.props.last_execution_stage")}</div>
<div className="w-40 sm:ml-2">{t("domain.props.last_execution_time")}</div>
<div className="w-24">{t("domain.props.enable")}</div>
<div className="grow">{t("common.text.operations")}</div>
</div>
{domains.map((domain) => (
<div
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
key={domain.id}
>
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex flex-col items-start">
{domain.domain.split(";").map((domain: string) => (
<div className="pr-3 truncate w-full">{domain}</div>
))}
</div>
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
<div>
{domain.expiredAt ? (
<>
<div>{t("domain.props.expiry.date1", { date: `${getLeftDays(domain.expiredAt)}/90` })}</div>
<div>
{t("domain.props.expiry.date2", {
date: getDate(domain.expiredAt),
})}
</div>
</>
) : (
"---"
)}
</div>
</div>
<div className="sm:w-32 w-full pt-1 sm:pt-0 flex items-center">
{domain.lastDeployedAt && domain.expand?.lastDeployment ? (
<>
<DeployState deployment={domain.expand.lastDeployment} />
</>
) : (
"---"
)}
</div>
<div className="sm:w-64 w-full pt-1 sm:pt-0 flex items-center">
{domain.lastDeployedAt && domain.expand?.lastDeployment ? (
<DeployProgress phase={domain.expand.lastDeployment?.phase} phaseSuccess={domain.expand.lastDeployment?.phaseSuccess} />
) : (
"---"
)}
</div>
<div className="sm:w-40 pt-1 sm:pt-0 sm:ml-2 flex items-center">
{domain.lastDeployedAt ? convertZulu2Beijing(domain.lastDeployedAt) : "---"}
</div>
<div className="sm:w-24 flex items-center">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Switch
checked={domain.enabled}
onCheckedChange={() => {
handelCheckedChange(domain.id ?? "");
}}
></Switch>
</TooltipTrigger>
<TooltipContent>
<div className="border rounded-sm px-3 bg-background text-muted-foreground text-xs">
{domain.enabled ? t("domain.props.enable.disabled") : t("domain.props.enable.enabled")}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0">
<Button variant={"link"} className="p-0" onClick={() => handleHistoryClick(domain.id ?? "")}>
{t("domain.history")}
</Button>
<Show when={domain.enabled ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button variant={"link"} className="p-0" onClick={() => handleRightNowClick(domain)}>
{t("domain.deploy")}
</Button>
</Show>
<Show when={(domain.enabled ? true : false) && domain.deployed ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button variant={"link"} className="p-0" onClick={() => handleForceClick(domain)}>
{t("domain.deploy_forced")}
</Button>
</Show>
<Show when={domain.expiredAt ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button variant={"link"} className="p-0" onClick={() => handleDownloadClick(domain)}>
{t("common.download")}
</Button>
</Show>
{!domain.enabled && (
<>
<Separator orientation="vertical" className="h-4 mx-2" />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
{t("common.delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("domain.delete")}</AlertDialogTitle>
<AlertDialogDescription>{t("domain.delete.confirm")}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDeleteClick(domain.id ?? "");
}}
>
{t("common.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button variant={"link"} className="p-0" onClick={() => handleEditClick(domain.id ?? "")}>
{t("common.edit")}
</Button>
</>
)}
</div>
</div>
))}
<XPagination
totalPages={totalPage}
currentPage={page ? Number(page) : 1}
onPageChange={(page) => {
setPage(page);
}}
/>
</>
)}
</div>
</>
);
};
export default Home;

View File

@@ -1,170 +0,0 @@
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Smile } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import { convertZulu2Beijing } from "@/lib/time";
import { Deployment, DeploymentListReq, Log } from "@/domain/deployment";
import { list } from "@/repository/deployment";
const History = () => {
const navigate = useNavigate();
const [deployments, setDeployments] = useState<Deployment[]>();
const [searchParams] = useSearchParams();
const { t } = useTranslation();
const domain = searchParams.get("domain");
useEffect(() => {
const fetchData = async () => {
const param: DeploymentListReq = {};
if (domain) {
param.domain = domain;
}
const data = await list(param);
setDeployments(data.items);
};
fetchData();
}, [domain]);
return (
<ScrollArea className="h-[80vh] overflow-hidden">
<div className="text-muted-foreground">{t("history.page.title")}</div>
{!deployments?.length ? (
<>
<Alert className="max-w-[40em] mx-auto mt-20">
<AlertTitle>{t("common.text.nodata")}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
<Smile className="text-yellow-400" size={36} />
</div>
<div className="ml-2"> {t("history.nodata")}</div>
</div>
<div className="mt-2 flex justify-end">
<Button
onClick={() => {
navigate("/");
}}
>
{t("domain.add")}
</Button>
</div>
</AlertDescription>
</Alert>
</>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48">{t("history.props.domain")}</div>
<div className="w-24">{t("history.props.status")}</div>
<div className="w-56">{t("history.props.stage")}</div>
<div className="w-56 sm:ml-2 text-center">{t("history.props.last_execution_time")}</div>
<div className="grow">{t("common.text.operations")}</div>
</div>
{deployments?.map((deployment) => (
<div
key={deployment.id}
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-start flex-col">
{deployment.expand.domain?.domain.split(";").map((domain: string) => <div className="pr-3 truncate w-full">{domain}</div>)}
</div>
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
<DeployState deployment={deployment} />
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
<DeployProgress phase={deployment.phase} phaseSuccess={deployment.phaseSuccess} />
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center sm:justify-center">{convertZulu2Beijing(deployment.deployedAt)}</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0 sm:ml-2">
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
{t("history.log")}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
{t("history.log")}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh] overflow-y-auto">
{deployment.log.check && (
<>
{deployment.log.check.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.error && <div className="mt-1 text-red-600">{item.error}</div>}
</div>
);
})}
</>
)}
{deployment.log.apply && (
<>
{deployment.log.apply.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.info &&
item.info.map((info: string) => {
return <div className="mt-1 text-green-600">{info}</div>;
})}
{item.error && <div className="mt-1 text-red-600">{item.error}</div>}
</div>
);
})}
</>
)}
{deployment.log.deploy && (
<>
{deployment.log.deploy.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.info &&
item.info.map((info: string) => {
return <div className="mt-1 text-green-600 break-words">{info}</div>;
})}
{item.error && <div className="mt-1 text-red-600">{item.error}</div>}
</div>
);
})}
</>
)}
</div>
</SheetContent>
</Sheet>
</div>
</div>
))}
</>
)}
</ScrollArea>
);
};
export default History;

View File

@@ -43,7 +43,6 @@ const WorkflowDetail = () => {
const [running, setRunning] = useState(false);
useEffect(() => {
console.log(id);
init(id ?? "");
if (id) {
setLocId(id);
@@ -87,6 +86,9 @@ const WorkflowDetail = () => {
return;
}
switchEnable();
if (!locId) {
navigate(`/workflow/detail?id=${workflow.id}`);
}
};
const handleWorkflowSaveClick = () => {
@@ -99,6 +101,9 @@ const WorkflowDetail = () => {
return;
}
save();
if (!locId) {
navigate(`/workflow/detail?id=${workflow.id}`);
}
};
const getTabCls = (tabName: string) => {