This commit is contained in:
yoan
2024-10-13 08:15:21 +08:00
parent 19f5348802
commit 1928a47961
37 changed files with 1854 additions and 734 deletions

View File

@@ -1,10 +1,4 @@
import {
Access,
accessFormType,
getUsageByConfigType,
LocalConfig,
SSHConfig,
} from "@/domain/access";
import { Access, accessFormType, getUsageByConfigType } from "@/domain/access";
import { useConfig } from "@/providers/config";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
@@ -20,7 +14,7 @@ import {
} from "../ui/form";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import { save } from "@/repository/access";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
@@ -39,30 +33,19 @@ const AccessLocalForm = ({
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
name: z
.string()
.min(1, "access.form.name.not.empty")
.max(64, t("zod.rule.string.max", { max: 64 })),
configType: accessFormType,
command: z.string().min(1, 'access.form.ssh.command.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
certPath: z.string().min(0, 'access.form.ssh.cert.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
keyPath: z.string().min(0, 'access.form.ssh.key.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
});
let config: LocalConfig = {
command: "sudo service nginx restart",
certPath: "/etc/nginx/ssl/certificate.crt",
keyPath: "/etc/nginx/ssl/private.key",
};
if (data) config = data.config as SSHConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name || '',
name: data?.name || "",
configType: "local",
certPath: config.certPath,
keyPath: config.keyPath,
command: config.command,
},
});
@@ -73,15 +56,11 @@ const AccessLocalForm = ({
configType: data.configType,
usage: getUsageByConfigType(data.configType),
config: {
command: data.command,
certPath: data.certPath,
keyPath: data.keyPath,
},
config: {},
};
try {
req.id = op == "copy" ? "" : req.id;
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -128,9 +107,12 @@ const AccessLocalForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('name')}</FormLabel>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input placeholder={t('access.form.name.not.empty')} {...field} />
<Input
placeholder={t("access.form.name.not.empty")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -143,7 +125,7 @@ const AccessLocalForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormLabel>{t("access.form.config.field")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -158,7 +140,7 @@ const AccessLocalForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormLabel>{t("access.form.config.field")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -168,55 +150,10 @@ const AccessLocalForm = ({
)}
/>
<FormField
control={form.control}
name="certPath"
render={({ field }) => (
<FormItem>
<FormLabel>{t('access.form.ssh.cert.path')}</FormLabel>
<FormControl>
<Input placeholder={t('access.form.ssh.cert.path.not.empty')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="keyPath"
render={({ field }) => (
<FormItem>
<FormLabel>{t('access.form.ssh.key.path')}</FormLabel>
<FormControl>
<Input placeholder={t('access.form.ssh.key.path.not.empty')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="command"
render={({ field }) => (
<FormItem>
<FormLabel>{t('access.form.ssh.command')}</FormLabel>
<FormControl>
<Textarea placeholder={t('access.form.ssh.command.not.empty')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t('save')}</Button>
<Button type="submit">{t("save")}</Button>
</div>
</form>
</Form>

View File

@@ -18,7 +18,6 @@ import {
} from "../ui/form";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import { save } from "@/repository/access";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
@@ -66,7 +65,10 @@ const AccessSSHForm = ({
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
name: z
.string()
.min(1, "access.form.name.not.empty")
.max(64, t("zod.rule.string.max", { max: 64 })),
configType: accessFormType,
host: z.string().refine(
(str) => {
@@ -77,16 +79,23 @@ const AccessSSHForm = ({
}
),
group: z.string().optional(),
port: z.string().min(1, 'access.form.ssh.port.not.empty').max(5, t('zod.rule.string.max', { max: 5 })),
username: z.string().min(1, 'username.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
password: z.string().min(0, 'password.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
key: z.string().min(0, 'access.form.ssh.key.not.empty').max(20480, t('zod.rule.string.max', { max: 20480 })),
port: z
.string()
.min(1, "access.form.ssh.port.not.empty")
.max(5, t("zod.rule.string.max", { max: 5 })),
username: z
.string()
.min(1, "username.not.empty")
.max(64, t("zod.rule.string.max", { max: 64 })),
password: z
.string()
.min(0, "password.not.empty")
.max(64, t("zod.rule.string.max", { max: 64 })),
key: z
.string()
.min(0, "access.form.ssh.key.not.empty")
.max(20480, t("zod.rule.string.max", { max: 20480 })),
keyFile: z.any().optional(),
preCommand: z.string().min(0).max(2048, t('zod.rule.string.max', { max: 2048 })).optional(),
command: z.string().min(1, 'access.form.ssh.command.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
certPath: z.string().min(0, 'access.form.ssh.cert.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
keyPath: z.string().min(0, 'access.form.ssh.key.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
});
let config: SSHConfig = {
@@ -96,10 +105,6 @@ const AccessSSHForm = ({
password: "",
key: "",
keyFile: "",
preCommand: "",
command: "sudo service nginx restart",
certPath: "/etc/nginx/ssl/certificate.crt",
keyPath: "/etc/nginx/ssl/private.key",
};
if (data) config = data.config as SSHConfig;
@@ -107,7 +112,7 @@ const AccessSSHForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name || '',
name: data?.name || "",
configType: "ssh",
group: data?.group,
host: config.host,
@@ -116,10 +121,6 @@ const AccessSSHForm = ({
password: config.password,
key: config.key,
keyFile: config.keyFile,
certPath: config.certPath,
keyPath: config.keyPath,
command: config.command,
preCommand: config.preCommand,
},
});
@@ -139,15 +140,11 @@ const AccessSSHForm = ({
username: data.username,
password: data.password,
key: data.key,
command: data.command,
preCommand: data.preCommand,
certPath: data.certPath,
keyPath: data.keyPath,
},
};
try {
req.id = op == "copy" ? "" : req.id;
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -228,9 +225,12 @@ const AccessSSHForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('name')}</FormLabel>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input placeholder={t('access.form.name.not.empty')} {...field} />
<Input
placeholder={t("access.form.name.not.empty")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -244,12 +244,12 @@ const AccessSSHForm = ({
render={({ field }) => (
<FormItem>
<FormLabel className="w-full flex justify-between">
<div>{t('access.form.ssh.group.label')}</div>
<div>{t("access.form.ssh.group.label")}</div>
<AccessGroupEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t('add')}
{t("add")}
</div>
}
/>
@@ -264,7 +264,9 @@ const AccessSSHForm = ({
}}
>
<SelectTrigger>
<SelectValue placeholder={t('access.group.not.empty')} />
<SelectValue
placeholder={t("access.group.not.empty")}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="emptyId">
@@ -304,7 +306,7 @@ const AccessSSHForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormLabel>{t("access.form.config.field")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -319,7 +321,7 @@ const AccessSSHForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormLabel>{t("access.form.config.field")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -334,9 +336,12 @@ const AccessSSHForm = ({
name="host"
render={({ field }) => (
<FormItem className="grow">
<FormLabel>{t('access.form.ssh.host')}</FormLabel>
<FormLabel>{t("access.form.ssh.host")}</FormLabel>
<FormControl>
<Input placeholder={t('access.form.ssh.host.not.empty')} {...field} />
<Input
placeholder={t("access.form.ssh.host.not.empty")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -349,10 +354,10 @@ const AccessSSHForm = ({
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>{t('access.form.ssh.port')}</FormLabel>
<FormLabel>{t("access.form.ssh.port")}</FormLabel>
<FormControl>
<Input
placeholder={t('access.form.ssh.port.not.empty')}
placeholder={t("access.form.ssh.port.not.empty")}
{...field}
type="number"
/>
@@ -369,9 +374,9 @@ const AccessSSHForm = ({
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>{t('username')}</FormLabel>
<FormLabel>{t("username")}</FormLabel>
<FormControl>
<Input placeholder={t('username.not.empty')} {...field} />
<Input placeholder={t("username.not.empty")} {...field} />
</FormControl>
<FormMessage />
@@ -384,10 +389,10 @@ const AccessSSHForm = ({
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormLabel>{t("password")}</FormLabel>
<FormControl>
<Input
placeholder={t('password.not.empty')}
placeholder={t("password.not.empty")}
{...field}
type="password"
/>
@@ -403,9 +408,12 @@ const AccessSSHForm = ({
name="key"
render={({ field }) => (
<FormItem hidden>
<FormLabel>{t('access.form.ssh.key')}</FormLabel>
<FormLabel>{t("access.form.ssh.key")}</FormLabel>
<FormControl>
<Input placeholder={t('access.form.ssh.key.not.empty')} {...field} />
<Input
placeholder={t("access.form.ssh.key.not.empty")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -418,7 +426,7 @@ const AccessSSHForm = ({
name="keyFile"
render={({ field }) => (
<FormItem>
<FormLabel>{t('access.form.ssh.key')}</FormLabel>
<FormLabel>{t("access.form.ssh.key")}</FormLabel>
<FormControl>
<div>
<Button
@@ -428,10 +436,12 @@ const AccessSSHForm = ({
className="w-48"
onClick={handleSelectFileClick}
>
{fileName ? fileName : t('access.form.ssh.key.file.not.empty')}
{fileName
? fileName
: t("access.form.ssh.key.file.not.empty")}
</Button>
<Input
placeholder={t('access.form.ssh.key.not.empty')}
placeholder={t("access.form.ssh.key.not.empty")}
{...field}
ref={fileInputRef}
className="hidden"
@@ -447,70 +457,10 @@ const AccessSSHForm = ({
)}
/>
<FormField
control={form.control}
name="certPath"
render={({ field }) => (
<FormItem>
<FormLabel>{t('access.form.ssh.cert.path')}</FormLabel>
<FormControl>
<Input placeholder={t('access.form.ssh.cert.path.not.empty')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="keyPath"
render={({ field }) => (
<FormItem>
<FormLabel>{t('access.form.ssh.key.path')}</FormLabel>
<FormControl>
<Input placeholder={t('access.form.ssh.key.path.not.empty')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="preCommand"
render={({ field }) => (
<FormItem>
<FormLabel>{t('access.form.ssh.pre.command')}</FormLabel>
<FormControl>
<Textarea placeholder={t('access.form.ssh.pre.command.not.empty')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="command"
render={({ field }) => (
<FormItem>
<FormLabel>{t('access.form.ssh.command')}</FormLabel>
<FormControl>
<Textarea placeholder={t('access.form.ssh.command.not.empty')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t('save')}</Button>
<Button type="submit">{t("save")}</Button>
</div>
</form>
</Form>

View File

@@ -43,10 +43,14 @@ import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import KVList from "./KVList";
import { produce } from "immer";
import { nanoid } from "nanoid";
import { z } from "zod";
type DeployEditContextProps = {
deploy: DeployConfig;
error: Record<string, string>;
setDeploy: (deploy: DeployConfig) => void;
setError: (error: Record<string, string>) => void;
};
const DeployEditContext = createContext<DeployEditContextProps>(
@@ -59,53 +63,92 @@ export const useDeployEditContext = () => {
type DeployListProps = {
deploys: DeployConfig[];
onChange: (deploys: DeployConfig[]) => void;
};
const DeployList = ({ deploys }: DeployListProps) => {
const DeployList = ({ deploys, onChange }: DeployListProps) => {
const [list, setList] = useState<DeployConfig[]>([]);
const { t } = useTranslation();
useEffect(() => {
setList(deploys);
}, [deploys]);
const handleAdd = (deploy: DeployConfig) => {
deploy.id = nanoid();
const newList = [...list, deploy];
setList(newList);
onChange(newList);
};
const handleDelete = (id: string) => {
const newList = list.filter((item) => item.id !== id);
setList(newList);
onChange(newList);
};
const handleSave = (deploy: DeployConfig) => {
const newList = list.map((item) => {
if (item.id === deploy.id) {
return { ...deploy };
}
return item;
});
setList(newList);
onChange(newList);
};
return (
<>
<Show
when={list.length > 0}
fallback={
<Alert className="w-full">
<Alert className="w-full border dark:border-stone-400">
<AlertDescription className="flex flex-col items-center">
<div></div>
<div>{t("deployment.not.added")}</div>
<div className="flex justify-end mt-2">
<DeployEditDialog
trigger={<Button size={"sm"}></Button>}
onSave={(config: DeployConfig) => {
handleAdd(config);
}}
trigger={<Button size={"sm"}>{t("add")}</Button>}
/>
</div>
</AlertDescription>
</Alert>
}
>
<div className="flex justify-end py-2 border-b">
<DeployEditDialog trigger={<Button size={"sm"}></Button>} />
<div className="flex justify-end py-2 border-b dark:border-stone-400">
<DeployEditDialog
trigger={<Button size={"sm"}>{t("add")}</Button>}
onSave={(config: DeployConfig) => {
handleAdd(config);
}}
/>
</div>
<div className="w-full md:w-[35em] rounded mt-5 border">
<div className="w-full md:w-[35em] rounded mt-5 border dark:border-stone-400">
<div className="">
<div className="flex justify-between text-sm p-3 items-center text-stone-700">
<div className="flex space-x-2 items-center">
<div>
<img src="/imgs/providers/ssh.svg" className="w-9"></img>
</div>
<div className="text-stone-600 flex-col flex space-y-0">
<div>ssh部署</div>
<div></div>
</div>
</div>
<div className="flex space-x-2">
<EditIcon size={16} className="cursor-pointer" />
<Trash2 size={16} className="cursor-pointer" />
</div>
</div>
{list.map((item) => (
<DeployItem
key={item.id}
item={item}
onDelete={() => {
handleDelete(item.id ?? "");
}}
onSave={(deploy: DeployConfig) => {
handleSave(deploy);
}}
/>
))}
</div>
</div>
</Show>
@@ -113,11 +156,87 @@ const DeployList = ({ deploys }: DeployListProps) => {
);
};
type DeployItemProps = {
item: DeployConfig;
onDelete: () => void;
onSave: (deploy: DeployConfig) => void;
};
const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => {
const {
config: { accesses },
} = useConfig();
const { t } = useTranslation();
const access = accesses.find((access) => access.id === item.access);
const getImg = () => {
if (!access) {
return "";
}
const accessType = accessTypeMap.get(access.configType);
if (accessType) {
return accessType[1];
}
return "";
};
const getTypeName = () => {
if (!access) {
return "";
}
const accessType = targetTypeMap.get(item.type);
if (accessType) {
return t(accessType[0]);
}
return "";
};
return (
<div className="flex justify-between text-sm p-3 items-center text-stone-700">
<div className="flex space-x-2 items-center">
<div>
<img src={getImg()} className="w-9"></img>
</div>
<div className="text-stone-600 flex-col flex space-y-0">
<div>{getTypeName()}</div>
<div>{access?.name}</div>
</div>
</div>
<div className="flex space-x-2">
<DeployEditDialog
trigger={<EditIcon size={16} className="cursor-pointer" />}
deployConfig={item}
onSave={(deploy: DeployConfig) => {
onSave(deploy);
}}
/>
<Trash2
size={16}
className="cursor-pointer"
onClick={() => {
onDelete();
}}
/>
</div>
</div>
);
};
type DeployEditDialogProps = {
trigger: React.ReactNode;
deployConfig?: DeployConfig;
onSave: (deploy: DeployConfig) => void;
};
const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
const DeployEditDialog = ({
trigger,
deployConfig,
onSave,
}: DeployEditDialogProps) => {
const {
config: { accesses },
} = useConfig();
@@ -129,6 +248,10 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
type: "",
});
const [error, setError] = useState<Record<string, string>>({});
const [open, setOpen] = useState(false);
useEffect(() => {
if (deployConfig) {
setLocDeployConfig({ ...deployConfig });
@@ -150,6 +273,7 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
t = locDeployConfig.type;
}
setDeployType(t as TargetType);
setError({});
}, [locDeployConfig.type]);
const setDeploy = useCallback(
@@ -177,23 +301,62 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
return item.configType === types[0];
});
const handleSaveClick = () => {
// 验证数据
// 保存数据
// 清理数据
// 关闭弹框
const newError = { ...error };
if (locDeployConfig.type === "") {
newError.type = t("domain.management.edit.access.not.empty.message");
} else {
newError.type = "";
}
if (locDeployConfig.access === "") {
newError.access = t("domain.management.edit.access.not.empty.message");
} else {
newError.access = "";
}
setError(newError);
for (const key in newError) {
if (newError[key] !== "") {
return;
}
}
onSave(locDeployConfig);
setLocDeployConfig({
access: "",
type: "",
});
setError({});
setOpen(false);
};
return (
<DeployEditContext.Provider
value={{
deploy: locDeployConfig,
setDeploy: setDeploy,
error: error,
setError: setError,
}}
>
<Dialog>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent>
<DialogContent className="dark:text-stone-200">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t("deployment")}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{/* 授权类型 */}
<div>
<Label></Label>
<Label>{t("deployment.access.type")}</Label>
<Select
value={locDeployConfig.type}
@@ -227,11 +390,13 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{error.type}</div>
</div>
{/* 授权 */}
<div>
<Label className="flex justify-between">
<div></div>
<div>{t("deployment.access.config")}</div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
@@ -275,12 +440,21 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{error.access}</div>
</div>
<DeployEdit type={deployType!} />
<DialogFooter>
<Button></Button>
<Button
onClick={(e) => {
e.stopPropagation();
handleSaveClick();
}}
>
{t("save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -317,8 +491,27 @@ const DeployEdit = ({ type }: DeployEditProps) => {
const DeploySSH = () => {
const { t } = useTranslation();
const { setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
const { deploy: data, setDeploy } = useDeployEditContext();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
certPath: "/etc/nginx/ssl/nginx.crt",
keyPath: "/etc/nginx/ssl/nginx.key",
preCommand: "",
command: "sudo service nginx reload",
},
});
}
}, []);
return (
<>
<div className="flex flex-col space-y-2">
@@ -358,10 +551,11 @@ const DeploySSH = () => {
</div>
<div>
<Label></Label>
<Label>{t("access.form.ssh.pre.command")}</Label>
<Textarea
className="mt-1"
value={data?.config?.preCommand}
placeholder={t("access.form.ssh.pre.command.not.empty")}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
@@ -375,10 +569,11 @@ const DeploySSH = () => {
</div>
<div>
<Label></Label>
<Label>{t("access.form.ssh.command")}</Label>
<Textarea
className="mt-1"
value={data?.config?.command}
placeholder={t("access.form.ssh.command.not.empty")}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
@@ -396,25 +591,69 @@ const DeploySSH = () => {
};
const DeployCDN = () => {
const { deploy: data, setDeploy } = useDeployEditContext();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { t } = useTranslation();
useEffect(() => {
setError({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
const domainSchema = z
.string()
.regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("domain.not.empty.verify.message"),
});
return (
<div className="flex flex-col space-y-2">
<div>
<Label></Label>
<Label>{t("deployment.access.cdn.deploy.to.domain")}</Label>
<Input
placeholder="部署至域名"
placeholder={t("deployment.access.cdn.deploy.to.domain")}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = e.target.value;
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
</div>
);
@@ -423,6 +662,12 @@ const DeployCDN = () => {
const DeployWebhook = () => {
const { deploy: data, setDeploy } = useDeployEditContext();
const { setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
return (
<>
<KVList

View File

@@ -70,8 +70,8 @@ const KVList = ({ variables, onValueChange }: KVListProps) => {
return (
<>
<div className="flex justify-between">
<Label></Label>
<div className="flex justify-between dark:text-stone-200">
<Label>{t("variable")}</Label>
<Show when={!!locVariables?.length}>
<KVEdit
variable={{
@@ -97,7 +97,7 @@ const KVList = ({ variables, onValueChange }: KVListProps) => {
fallback={
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
<div className="text-muted-foreground">
{t("not.added.yet.variable")}
{t("variable.not.added")}
</div>
<KVEdit
@@ -119,7 +119,7 @@ const KVList = ({ variables, onValueChange }: KVListProps) => {
</div>
}
>
<div className="border p-3 rounded-md text-stone-700 text-sm">
<div className="border p-3 rounded-md text-stone-700 text-sm dark:text-stone-200">
{locVariables?.map((item, index) => (
<div key={index} className="flex justify-between items-center">
<div>
@@ -175,14 +175,14 @@ const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
const handleSaveClick = () => {
if (!locVariable.key) {
setErr({
key: t("name.required"),
key: t("variable.name.required"),
});
return;
}
if (!locVariable.value) {
setErr({
value: t("value.required"),
value: t("variable.value.required"),
});
return;
}
@@ -202,14 +202,14 @@ const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
}}
>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent>
<DialogContent className="dark:text-stone-200">
<DialogHeader className="flex flex-col">
<DialogTitle></DialogTitle>
<DialogTitle>{t("variable")}</DialogTitle>
<div className="pt-5 flex flex-col items-start">
<Label></Label>
<Label>{t("variable.name")}</Label>
<Input
placeholder="请输入变量名"
placeholder={t("variable.name.placeholder")}
value={locVariable?.key}
onChange={(e) => {
setLocVariable({ ...locVariable, key: e.target.value });
@@ -220,9 +220,9 @@ const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
</div>
<div className="pt-2 flex flex-col items-start">
<Label></Label>
<Label>{t("variable.value")}</Label>
<Input
placeholder="请输入变量值"
placeholder={t("variable.value.placeholder")}
value={locVariable?.value}
onChange={(e) => {
setLocVariable({ ...locVariable, value: e.target.value });

View File

@@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}