details improvement and unnecessary files deletion
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user