fix conflict
This commit is contained in:
24
ui/src/api/certificates.ts
Normal file
24
ui/src/api/certificates.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { type CertificateFormatType } from "@/domain/certificate";
|
||||
import { getPocketBase } from "@/repository/_pocketbase";
|
||||
|
||||
export const archive = async (id: string, format?: CertificateFormatType) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send<BaseResponse>(`/api/certificates/${encodeURIComponent(id)}/archive`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: {
|
||||
format: format,
|
||||
},
|
||||
});
|
||||
|
||||
if (resp.code != 0) {
|
||||
throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
|
||||
}
|
||||
|
||||
return resp;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
import { getPocketBase } from "@/repository/_pocketbase";
|
||||
|
||||
export const notifyTest = async (channel: string) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { type Statistics } from "@/domain/statistics";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
import { getPocketBase } from "@/repository/_pocketbase";
|
||||
|
||||
export const get = async () => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
import { getPocketBase } from "@/repository/_pocketbase";
|
||||
|
||||
export const run = async (id: string) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send<BaseResponse>("/api/workflow/run", {
|
||||
const resp = await pb.send<BaseResponse>(`/api/workflows/${encodeURIComponent(id)}/run`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: {
|
||||
workflowId: id,
|
||||
trigger: WORKFLOW_TRIGGERS.MANUAL,
|
||||
},
|
||||
});
|
||||
@@ -52,7 +52,7 @@ const MultipleInput = ({
|
||||
draft.push("");
|
||||
});
|
||||
setValue(newValue);
|
||||
setTimeout(() => itemRefs.current[newValue.length - 1]?.focus(), 0);
|
||||
setTimeout(() => itemRefs.current[newValue.length - 1]?.focus(), 1);
|
||||
|
||||
onValueCreate?.(newValue.length - 1);
|
||||
};
|
||||
@@ -110,7 +110,7 @@ const MultipleInput = ({
|
||||
draft.splice(index + 1, 0, "");
|
||||
});
|
||||
setValue(newValue);
|
||||
setTimeout(() => itemRefs.current[index + 1]?.focus(), 0);
|
||||
setTimeout(() => itemRefs.current[index + 1]?.focus(), 1);
|
||||
|
||||
onValueCreate?.(index + 1);
|
||||
};
|
||||
|
||||
@@ -3,9 +3,10 @@ import { useTranslation } from "react-i18next";
|
||||
import { CopyOutlined as CopyOutlinedIcon, DownOutlined as DownOutlinedIcon, LikeOutlined as LikeOutlinedIcon } from "@ant-design/icons";
|
||||
import { Button, Dropdown, Form, Input, Space, Tooltip, message } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
import { type CertificateModel } from "@/domain/certificate";
|
||||
import { saveFiles2Zip } from "@/utils/file";
|
||||
import { archive as archiveCertificate } from "@/api/certificates";
|
||||
import { CERTIFICATE_FORMATS, type CertificateFormatType, type CertificateModel } from "@/domain/certificate";
|
||||
|
||||
export type CertificateDetailProps = {
|
||||
className?: string;
|
||||
@@ -18,20 +19,17 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
|
||||
const [messageApi, MessageContextHolder] = message.useMessage();
|
||||
|
||||
const handleDownloadPEMClick = async () => {
|
||||
const zipName = `${data.id}-${data.subjectAltNames}.zip`;
|
||||
const files = [
|
||||
{
|
||||
name: `${data.subjectAltNames}.pem`,
|
||||
content: data.certificate ?? "",
|
||||
},
|
||||
{
|
||||
name: `${data.subjectAltNames}.key`,
|
||||
content: data.privateKey ?? "",
|
||||
},
|
||||
];
|
||||
|
||||
await saveFiles2Zip(zipName, files);
|
||||
const handleDownloadClick = async (format: CertificateFormatType) => {
|
||||
try {
|
||||
const res = await archiveCertificate(data.id, format);
|
||||
const bstr = atob(res.data);
|
||||
const u8arr = Uint8Array.from(bstr, (ch) => ch.charCodeAt(0));
|
||||
const blob = new Blob([u8arr], { type: "application/zip" });
|
||||
saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
messageApi.warning(t("common.text.operation_failed"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -90,21 +88,17 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
key: "PEM",
|
||||
label: "PEM",
|
||||
extra: <LikeOutlinedIcon />,
|
||||
onClick: () => handleDownloadPEMClick(),
|
||||
onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.PEM),
|
||||
},
|
||||
{
|
||||
key: "PFX",
|
||||
label: "PFX",
|
||||
onClick: () => {
|
||||
alert("TODO: 暂时不支持下载 PFX 证书");
|
||||
},
|
||||
onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.PFX),
|
||||
},
|
||||
{
|
||||
key: "JKS",
|
||||
label: "JKS",
|
||||
onClick: () => {
|
||||
alert("TODO: 暂时不支持下载 JKS 证书");
|
||||
},
|
||||
onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.JKS),
|
||||
},
|
||||
],
|
||||
}}
|
||||
|
||||
@@ -42,14 +42,14 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
|
||||
|
||||
<div className="mt-4 rounded-md bg-black p-4 text-stone-200">
|
||||
<div className="flex flex-col space-y-3">
|
||||
{data!.logs.map((item, i) => {
|
||||
{data!.logs?.map((item, i) => {
|
||||
return (
|
||||
<div key={i} className="flex flex-col space-y-2">
|
||||
<div>{item.nodeName}</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
{item.outputs.map((output, j) => {
|
||||
{item.outputs?.map((output, j) => {
|
||||
return (
|
||||
<div key={j} className="flex space-x-2 text-sm">
|
||||
<div key={j} className="flex space-x-2 text-sm" style={{ wordBreak: "break-word" }}>
|
||||
<div className="whitespace-nowrap">[{dayjs(output.time).format("YYYY-MM-DD HH:mm:ss")}]</div>
|
||||
{output.error ? <div className="text-red-500">{output.error}</div> : <div>{output.content}</div>}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,22 @@ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } f
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormOutlined as FormOutlinedIcon, PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { AutoComplete, type AutoCompleteProps, Button, Divider, Form, type FormInstance, Input, Select, Space, Switch, Tooltip, Typography } from "antd";
|
||||
import {
|
||||
AutoComplete,
|
||||
type AutoCompleteProps,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Form,
|
||||
type FormInstance,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -42,8 +57,8 @@ const MULTIPLE_INPUT_DELIMITER = ";";
|
||||
const initFormModel = (): ApplyNodeConfigFormFieldValues => {
|
||||
return {
|
||||
keyAlgorithm: "RSA2048",
|
||||
propagationTimeout: 60,
|
||||
disableFollowCNAME: true,
|
||||
skipBeforeExpiryDays: 20,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -77,13 +92,24 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
.split(MULTIPLE_INPUT_DELIMITER)
|
||||
.every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
|
||||
}, t("common.errmsg.host_invalid")),
|
||||
propagationTimeout: z
|
||||
dnsPropagationTimeout: z
|
||||
.union([
|
||||
z.number().int().gte(1, t("workflow_node.apply.form.propagation_timeout.placeholder")),
|
||||
z.string().refine((v) => !v || /^[1-9]\d*$/.test(v), t("workflow_node.apply.form.propagation_timeout.placeholder")),
|
||||
z.number().int().gte(1, t("workflow_node.apply.form.dns_propagation_timeout.placeholder")),
|
||||
z.string().refine((v) => !v || /^[1-9]\d*$/.test(v), t("workflow_node.apply.form.dns_propagation_timeout.placeholder")),
|
||||
])
|
||||
.nullish(),
|
||||
dnsTTL: z
|
||||
.union([
|
||||
z.number().int().gte(1, t("workflow_node.apply.form.dns_ttl.placeholder")),
|
||||
z.string().refine((v) => !v || /^[1-9]\d*$/.test(v), t("workflow_node.apply.form.dns_ttl.placeholder")),
|
||||
])
|
||||
.nullish(),
|
||||
disableFollowCNAME: z.boolean().nullish(),
|
||||
skipBeforeExpiryDays: z
|
||||
.number({ message: t("workflow_node.apply.form.skip_before_expiry_days.placeholder") })
|
||||
.int(t("workflow_node.apply.form.skip_before_expiry_days.placeholder"))
|
||||
.gte(1, t("workflow_node.apply.form.skip_before_expiry_days.placeholder"))
|
||||
.lte(60, t("workflow_node.apply.form.skip_before_expiry_days.placeholder")),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const { form: formInst, formProps } = useAntdForm({
|
||||
@@ -313,18 +339,34 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="propagationTimeout"
|
||||
label={t("workflow_node.apply.form.propagation_timeout.label")}
|
||||
name="dnsPropagationTimeout"
|
||||
label={t("workflow_node.apply.form.dns_propagation_timeout.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.propagation_timeout.tooltip") }}></span>}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.dns_propagation_timeout.tooltip") }}></span>}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
allowClear
|
||||
min={0}
|
||||
max={3600}
|
||||
placeholder={t("workflow_node.apply.form.propagation_timeout.placeholder")}
|
||||
addonAfter={t("workflow_node.apply.form.propagation_timeout.suffix")}
|
||||
placeholder={t("workflow_node.apply.form.dns_propagation_timeout.placeholder")}
|
||||
addonAfter={t("workflow_node.apply.form.dns_propagation_timeout.unit")}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="dnsTTL"
|
||||
label={t("workflow_node.apply.form.dns_ttl.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.dns_ttl.tooltip") }}></span>}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
allowClear
|
||||
min={0}
|
||||
max={86400}
|
||||
placeholder={t("workflow_node.apply.form.dns_ttl.placeholder")}
|
||||
addonAfter={t("workflow_node.apply.form.dns_ttl.unit")}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -337,6 +379,33 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Divider className="my-1">
|
||||
<Typography.Text className="text-xs font-normal" type="secondary">
|
||||
{t("workflow_node.apply.form.strategy_config.label")}
|
||||
</Typography.Text>
|
||||
</Divider>
|
||||
|
||||
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
||||
<Form.Item
|
||||
label={t("workflow_node.apply.form.skip_before_expiry_days.label")}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.skip_before_expiry_days.tooltip") }}></span>}
|
||||
>
|
||||
<Flex align="center" gap={8} wrap="wrap">
|
||||
<div>{t("workflow_node.apply.form.skip_before_expiry_days.prefix")}</div>
|
||||
<Form.Item name="skipBeforeExpiryDays" noStyle rules={[formRule]}>
|
||||
<InputNumber
|
||||
className="w-36"
|
||||
min={1}
|
||||
max={60}
|
||||
placeholder={t("workflow_node.apply.form.skip_before_expiry_days.placeholder")}
|
||||
addonAfter={t("workflow_node.apply.form.skip_before_expiry_days.unit")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div>{t("workflow_node.apply.form.skip_before_expiry_days.suffix")}</div>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Form.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
|
||||
import { Button, Divider, Form, type FormInstance, Select, Tooltip, Typography } from "antd";
|
||||
import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -65,7 +65,9 @@ export type DeployNodeConfigFormInstance = {
|
||||
};
|
||||
|
||||
const initFormModel = (): DeployNodeConfigFormFieldValues => {
|
||||
return {};
|
||||
return {
|
||||
skipOnLastSucceeded: true,
|
||||
};
|
||||
};
|
||||
|
||||
const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNodeConfigFormProps>(
|
||||
@@ -91,6 +93,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
.nonempty(t("workflow_node.deploy.form.provider_access.placeholder"))
|
||||
.refine(() => !!formInst.getFieldValue("provider"), t("workflow_node.deploy.form.provider.placeholder")),
|
||||
providerConfig: z.any(),
|
||||
skipOnLastSucceeded: z.boolean(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const { form: formInst, formProps } = useAntdForm({
|
||||
@@ -340,6 +343,27 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
|
||||
{nestedFormEl}
|
||||
</Show>
|
||||
|
||||
<Divider className="my-1">
|
||||
<Typography.Text className="text-xs font-normal" type="secondary">
|
||||
{t("workflow_node.deploy.form.strategy_config.label")}
|
||||
</Typography.Text>
|
||||
</Divider>
|
||||
|
||||
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
||||
<Form.Item label={t("workflow_node.deploy.form.skip_on_last_succeeded.label")}>
|
||||
<Flex align="center" gap={8} wrap="wrap">
|
||||
<div>{t("workflow_node.deploy.form.skip_on_last_succeeded.prefix")}</div>
|
||||
<Form.Item name="skipOnLastSucceeded" noStyle rules={[formRule]}>
|
||||
<Switch
|
||||
checkedChildren={t("workflow_node.deploy.form.skip_on_last_succeeded.enabled.on")}
|
||||
unCheckedChildren={t("workflow_node.deploy.form.skip_on_last_succeeded.enabled.off")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div>{t("workflow_node.deploy.form.skip_on_last_succeeded.suffix")}</div>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Form.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
import { validDomainName } from "@/utils/validators";
|
||||
|
||||
type DeployNodeConfigFormAliyunOSSConfigFieldValues = Nullish<{
|
||||
endpoint: string;
|
||||
region: string;
|
||||
bucket: string;
|
||||
domain: string;
|
||||
}>;
|
||||
@@ -33,9 +33,9 @@ const DeployNodeConfigFormAliyunOSSConfig = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
endpoint: z
|
||||
.string({ message: t("workflow_node.deploy.form.aliyun_oss_endpoint.placeholder") })
|
||||
.url(t("common.errmsg.url_invalid"))
|
||||
region: z
|
||||
.string({ message: t("workflow_node.deploy.form.aliyun_oss_region.placeholder") })
|
||||
.nonempty(t("workflow_node.deploy.form.aliyun_oss_region.placeholder"))
|
||||
.trim(),
|
||||
bucket: z
|
||||
.string({ message: t("workflow_node.deploy.form.aliyun_oss_bucket.placeholder") })
|
||||
@@ -61,12 +61,12 @@ const DeployNodeConfigFormAliyunOSSConfig = ({
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="endpoint"
|
||||
label={t("workflow_node.deploy.form.aliyun_oss_endpoint.label")}
|
||||
name="region"
|
||||
label={t("workflow_node.deploy.form.aliyun_oss_region.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_oss_endpoint.tooltip") }}></span>}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aliyun_oss_region.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.aliyun_oss_endpoint.placeholder")} />
|
||||
<Input placeholder={t("workflow_node.deploy.form.aliyun_oss_region.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { CERTIFICATE_FORMATS } from "@/domain/certificate";
|
||||
|
||||
type DeployNodeConfigFormLocalConfigFieldValues = Nullish<{
|
||||
format: string;
|
||||
@@ -27,9 +28,9 @@ export type DeployNodeConfigFormLocalConfigProps = {
|
||||
onValuesChange?: (values: DeployNodeConfigFormLocalConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const FORMAT_PEM = "PEM" as const;
|
||||
const FORMAT_PFX = "PFX" as const;
|
||||
const FORMAT_JKS = "JKS" as const;
|
||||
const FORMAT_PEM = CERTIFICATE_FORMATS.PEM;
|
||||
const FORMAT_PFX = CERTIFICATE_FORMATS.PFX;
|
||||
const FORMAT_JKS = CERTIFICATE_FORMATS.JKS;
|
||||
|
||||
const SHELLENV_SH = "sh" as const;
|
||||
const SHELLENV_CMD = "cmd" as const;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { CERTIFICATE_FORMATS } from "@/domain/certificate";
|
||||
|
||||
type DeployNodeConfigFormSSHConfigFieldValues = Nullish<{
|
||||
format: string;
|
||||
@@ -26,9 +27,9 @@ export type DeployNodeConfigFormSSHConfigProps = {
|
||||
onValuesChange?: (values: DeployNodeConfigFormSSHConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const FORMAT_PEM = "PEM" as const;
|
||||
const FORMAT_PFX = "PFX" as const;
|
||||
const FORMAT_JKS = "JKS" as const;
|
||||
const FORMAT_PEM = CERTIFICATE_FORMATS.PEM;
|
||||
const FORMAT_PFX = CERTIFICATE_FORMATS.PFX;
|
||||
const FORMAT_JKS = CERTIFICATE_FORMATS.JKS;
|
||||
|
||||
const initFormModel = (): DeployNodeConfigFormSSHConfigFieldValues => {
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
MoreOutlined as MoreOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Button, Card, Drawer, Dropdown, Input, Modal, Popover, Space } from "antd";
|
||||
import { Button, Card, Drawer, Dropdown, Input, type InputRef, Modal, Popover, Space } from "antd";
|
||||
import { produce } from "immer";
|
||||
import { isEqual } from "radash";
|
||||
|
||||
@@ -71,9 +71,10 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU
|
||||
|
||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||
|
||||
const nameInputRef = useRef<InputRef>(null);
|
||||
const nameRef = useRef<string>();
|
||||
|
||||
const handleRenameClick = async () => {
|
||||
const handleRenameConfirm = async () => {
|
||||
const oldName = node.name;
|
||||
const newName = nameRef.current?.trim()?.substring(0, 64) || oldName;
|
||||
if (oldName === newName) {
|
||||
@@ -131,11 +132,12 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU
|
||||
content: (
|
||||
<div className="pb-2 pt-4">
|
||||
<Input
|
||||
ref={(ref) => setTimeout(() => ref?.focus({ cursor: "end" }), 0)}
|
||||
ref={nameInputRef}
|
||||
autoFocus
|
||||
defaultValue={node.name}
|
||||
onChange={(e) => (nameRef.current = e.target.value)}
|
||||
onPressEnter={async () => {
|
||||
await handleRenameClick();
|
||||
await handleRenameConfirm();
|
||||
dialog.destroy();
|
||||
}}
|
||||
/>
|
||||
@@ -143,8 +145,9 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU
|
||||
),
|
||||
icon: null,
|
||||
okText: t("common.button.save"),
|
||||
onOk: handleRenameClick,
|
||||
onOk: handleRenameConfirm,
|
||||
});
|
||||
setTimeout(() => nameInputRef.current?.focus(), 1);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,3 +19,11 @@ export const CERTIFICATE_SOURCES = Object.freeze({
|
||||
} as const);
|
||||
|
||||
export type CertificateSourceType = (typeof CERTIFICATE_SOURCES)[keyof typeof CERTIFICATE_SOURCES];
|
||||
|
||||
export const CERTIFICATE_FORMATS = Object.freeze({
|
||||
PEM: "PEM",
|
||||
PFX: "PFX",
|
||||
JKS: "JKS",
|
||||
} as const);
|
||||
|
||||
export type CertificateFormatType = (typeof CERTIFICATE_FORMATS)[keyof typeof CERTIFICATE_FORMATS];
|
||||
|
||||
@@ -114,8 +114,10 @@ export type WorkflowNodeConfigForApply = {
|
||||
providerConfig?: Record<string, unknown>;
|
||||
keyAlgorithm: string;
|
||||
nameservers?: string;
|
||||
propagationTimeout?: number;
|
||||
dnsPropagationTimeout?: number;
|
||||
dnsTTL?: number;
|
||||
disableFollowCNAME?: boolean;
|
||||
skipBeforeExpiryDays: number;
|
||||
};
|
||||
|
||||
export type WorkflowNodeConfigForDeploy = {
|
||||
@@ -123,6 +125,7 @@ export type WorkflowNodeConfigForDeploy = {
|
||||
provider: string;
|
||||
providerAccessId: string;
|
||||
providerConfig: Record<string, unknown>;
|
||||
skipOnLastSucceeded: boolean;
|
||||
};
|
||||
|
||||
export type WorkflowNodeConfigForNotify = {
|
||||
|
||||
@@ -6,8 +6,8 @@ export interface WorkflowRunModel extends BaseModel {
|
||||
trigger: string;
|
||||
startedAt: ISO8601String;
|
||||
endedAt: ISO8601String;
|
||||
logs: WorkflowRunLog[];
|
||||
error: string;
|
||||
logs?: WorkflowRunLog[];
|
||||
error?: string;
|
||||
expand?: {
|
||||
workflowId?: WorkflowModel;
|
||||
};
|
||||
@@ -16,15 +16,15 @@ export interface WorkflowRunModel extends BaseModel {
|
||||
export type WorkflowRunLog = {
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
outputs: WorkflowRunLogOutput[];
|
||||
error: string;
|
||||
outputs?: WorkflowRunLogOutput[];
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type WorkflowRunLogOutput = {
|
||||
time: ISO8601String;
|
||||
title: string;
|
||||
content: string;
|
||||
error: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export const WORKFLOW_RUN_STATUSES = Object.freeze({
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
"settings.sslprovider.form.provider.option.letsencrypt_staging.label": "Let's Encrypt Staging Environment",
|
||||
"settings.sslprovider.form.provider.option.zerossl.label": "ZeroSSL",
|
||||
"settings.sslprovider.form.provider.option.gts.label": "Google Trust Services",
|
||||
"settings.sslprovider.form.provider.alert": "Attention: The certificate validity lifetime, certificate algorithm, domain names count, and support for wildcard domain names are allowed may vary among different providers. After switching service providers, please check whether the configuration of the workflows needs to be adjusted.",
|
||||
"settings.sslprovider.form.letsencrypt_staging_alert": "The staging environment can reduce the chance of your running up against rate limits.<br><br>Learn more:<br><a href=\"https://letsencrypt.org/docs/staging-environment/\" target=\"_blank\">https://letsencrypt.org/docs/staging-environment/</a>",
|
||||
"settings.sslprovider.form.letsencrypt_staging_warning": "Attention: Certificates from the staging environment are only for testing purposes.",
|
||||
"settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID",
|
||||
"settings.sslprovider.form.zerossl_eab_kid.placeholder": "Please enter EAB KID",
|
||||
"settings.sslprovider.form.zerossl_eab_kid.tooltip": "For more information, see <a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>",
|
||||
|
||||
@@ -51,15 +51,26 @@
|
||||
"workflow_node.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm",
|
||||
"workflow_node.apply.form.nameservers.label": "DNS recursive nameservers (Optional)",
|
||||
"workflow_node.apply.form.nameservers.placeholder": "Please enter DNS recursive nameservers (separated by semicolons)",
|
||||
"workflow_node.apply.form.nameservers.tooltip": "It determines whether to custom DNS recursive nameservers during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.<br><a href=\"https://go-acme.github.io/lego/usage/cli/options/index.html#dns-resolvers-and-challenge-verification\" target=\"_blank\">Learn more</a>.",
|
||||
"workflow_node.apply.form.nameservers.tooltip": "It determines whether to custom DNS recursive nameservers during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.<a href=\"https://go-acme.github.io/lego/usage/cli/options/index.html#dns-resolvers-and-challenge-verification\" target=\"_blank\">Learn more</a>.",
|
||||
"workflow_node.apply.form.nameservers.multiple_input_modal.title": "Change DNS rcursive nameservers",
|
||||
"workflow_node.apply.form.nameservers.multiple_input_modal.placeholder": "Please enter DNS recursive nameserver",
|
||||
"workflow_node.apply.form.propagation_timeout.label": "DNS propagation timeout (Optional)",
|
||||
"workflow_node.apply.form.propagation_timeout.placeholder": "Please enter DNS propagation timeout",
|
||||
"workflow_node.apply.form.propagation_timeout.suffix": "seconds",
|
||||
"workflow_node.apply.form.propagation_timeout.tooltip": "It determines the maximum waiting time for DNS propagation checks during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.",
|
||||
"workflow_node.apply.form.dns_propagation_timeout.label": "DNS propagation timeout (Optional)",
|
||||
"workflow_node.apply.form.dns_propagation_timeout.placeholder": "Please enter DNS propagation timeout",
|
||||
"workflow_node.apply.form.dns_propagation_timeout.unit": "seconds",
|
||||
"workflow_node.apply.form.dns_propagation_timeout.tooltip": "It determines the maximum waiting time for DNS propagation checks during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.<br><br>Leave blank to use the default value provided by the provider.",
|
||||
"workflow_node.apply.form.dns_ttl.label": "DNS TTL (Optional)",
|
||||
"workflow_node.apply.form.dns_ttl.placeholder": "Please enter DNS TTL",
|
||||
"workflow_node.apply.form.dns_ttl.unit": "seconds",
|
||||
"workflow_node.apply.form.dns_ttl.tooltip": "It determines the time to live for DNS record during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.<br><br>Leave blank to use the default value provided by the provider.",
|
||||
"workflow_node.apply.form.disable_follow_cname.label": "Disable CNAME following",
|
||||
"workflow_node.apply.form.disable_follow_cname.tooltip": "It determines whether to disable CNAME following during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.<br><a href=\"https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname\" target=\"_blank\">Learn more</a>.",
|
||||
"workflow_node.apply.form.disable_follow_cname.tooltip": "It determines whether to disable CNAME following during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.<a href=\"https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname\" target=\"_blank\">Learn more</a>.",
|
||||
"workflow_node.apply.form.strategy_config.label": "Strategy settings",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.label": "Renewal interval",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.placeholder": "Please enter renewal interval",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.prefix": "If the certificate expiration time exceeds",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.suffix": ", skip to re-apply.",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.unit": "days",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the CA, otherwise the certificate may never be renewed.",
|
||||
|
||||
"workflow_node.deploy.label": "Deployment",
|
||||
"workflow_node.deploy.search.provider.placeholder": "Search deploy target ...",
|
||||
@@ -130,9 +141,9 @@
|
||||
"workflow_node.deploy.form.aliyun_nlb_listener_id.label": "Alibaba Cloud NLB listener ID",
|
||||
"workflow_node.deploy.form.aliyun_nlb_listener_id.placeholder": "Please enter Alibaba Cloud NLB listener ID",
|
||||
"workflow_node.deploy.form.aliyun_nlb_listener_id.tooltip": "For more information, see <a href=\"https://slb.console.aliyun.com/nlb\" target=\"_blank\">https://slb.console.aliyun.com/nlb</a>",
|
||||
"workflow_node.deploy.form.aliyun_oss_endpoint.label": "Alibaba Cloud OSS endpoint",
|
||||
"workflow_node.deploy.form.aliyun_oss_endpoint.placeholder": "Please enter Alibaba Cloud OSS endpoint",
|
||||
"workflow_node.deploy.form.aliyun_oss_endpoint.tooltip": "For more information, see <a href=\"https://www.alibabacloud.com/help/en/oss/user-guide/regions-and-endpoints\" target=\"_blank\">https://www.alibabacloud.com/help/en/oss/user-guide/regions-and-endpoints</a>",
|
||||
"workflow_node.deploy.form.aliyun_oss_region.label": "Alibaba Cloud region",
|
||||
"workflow_node.deploy.form.aliyun_oss_region.placeholder": "Please enter Alibaba Cloud region (e.g. cn-hangzhou)",
|
||||
"workflow_node.deploy.form.aliyun_oss_region.tooltip": "For more information, see <a href=\"https://www.alibabacloud.com/help/en/oss/user-guide/regions-and-endpoints\" target=\"_blank\">https://www.alibabacloud.com/help/en/oss/user-guide/regions-and-endpoints</a>",
|
||||
"workflow_node.deploy.form.aliyun_oss_bucket.label": "Alibaba Cloud OSS bucket",
|
||||
"workflow_node.deploy.form.aliyun_oss_bucket.placeholder": "Please enter Alibaba Cloud OSS bucket name",
|
||||
"workflow_node.deploy.form.aliyun_oss_bucket.tooltip": "For more information, see <a href=\"https://oss.console.aliyun.com\" target=\"_blank\">https://oss.console.aliyun.com</a>",
|
||||
@@ -348,6 +359,12 @@
|
||||
"workflow_node.deploy.form.webhook_data.guide": "Tips: The Webhook data should be a key-value pair in JSON format. The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL. <br><br>Supported variables: <br><strong>${DOMAIN}</strong>: The primary domain of the certificate (<i>CommonName</i>).<br><strong>${DOMAINS}</strong>: The domain list of the certificate (<i>SubjectAltNames</i>).<br><strong>${CERTIFICATE}</strong>: The PEM format content of the certificate file.<br><strong>${PRIVATE_KEY}</strong>: The PEM format content of the private key file.",
|
||||
"workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
||||
"workflow_node.deploy.form.webhook_data_preset.button": "Use preset template",
|
||||
"workflow_node.deploy.form.strategy_config.label": "Strategy settings",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.label": "Repeated deployment",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.prefix": "If the last deployment was successful, ",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.suffix": " to re-deploy.",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.enabled.on": "skip",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.enabled.off": "not skip",
|
||||
|
||||
"workflow_node.notify.label": "Notification",
|
||||
"workflow_node.notify.form.subject.label": "Subject",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"settings.notification.template.form.message.extra": "支持的变量(${COUNT}: 即将过期张数;${DOMAINS}: 域名列表)",
|
||||
"settings.notification.channels.card.title": "通知渠道",
|
||||
"settings.notification.channel.enabled.on": "启用",
|
||||
"settings.notification.channel.enabled.off": "未启用",
|
||||
"settings.notification.channel.enabled.off": "停用",
|
||||
"settings.notification.push_test.button": "推送测试消息",
|
||||
"settings.notification.push_test.pushed": "已推送",
|
||||
"settings.notification.channel.form.bark_server_url.label": "服务器地址",
|
||||
@@ -77,8 +77,8 @@
|
||||
"settings.sslprovider.form.provider.option.letsencrypt_staging.label": "Let's Encrypt 测试环境",
|
||||
"settings.sslprovider.form.provider.option.zerossl.label": "ZeroSSL",
|
||||
"settings.sslprovider.form.provider.option.gts.label": "Google Trust Services",
|
||||
"settings.sslprovider.form.provider.alert": "注意:不同服务商所支持的证书有效期、证书算法、多域名数量上限、是否允许泛域名等可能不同,切换服务商后请注意检查已有工作流的配置是否需要调整。",
|
||||
"settings.sslprovider.form.letsencrypt_staging_alert": "测试环境比生产环境有更宽松的速率限制,可进行测试性部署。<br><br>点击下方链接了解更多:<br><a href=\"https://letsencrypt.org/zh-cn/docs/staging-environment/\" target=\"_blank\">https://letsencrypt.org/zh-cn/docs/staging-environment/</a>",
|
||||
"settings.sslprovider.form.letsencrypt_staging_warning": "警告:测试环境证书仅能用于测试目的。",
|
||||
"settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID",
|
||||
"settings.sslprovider.form.zerossl_eab_kid.placeholder": "请输入 EAB KID",
|
||||
"settings.sslprovider.form.zerossl_eab_kid.tooltip": "这是什么?请参阅 <a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"workflow_node.apply.form.domains.multiple_input_modal.placeholder": "请输入域名",
|
||||
"workflow_node.apply.form.contact_email.label": "联系邮箱",
|
||||
"workflow_node.apply.form.contact_email.placeholder": "请输入联系邮箱",
|
||||
"workflow_node.apply.form.contact_email.tooltip": "申请签发 SSL 证书时所需的联系方式。请注意 Let's Encrypt 账户注册的速率限制。<br><a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">点此了解更多</a>。",
|
||||
"workflow_node.apply.form.contact_email.tooltip": "申请签发 SSL 证书时所需的联系方式。请注意 Let's Encrypt 账户注册的速率限制。<a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">点此了解更多</a>。",
|
||||
"workflow_node.apply.form.provider.label": "DNS 提供商",
|
||||
"workflow_node.apply.form.provider.placeholder": "请选择 DNS 提供商",
|
||||
"workflow_node.apply.form.provider_access.label": "DNS 提供商授权",
|
||||
@@ -51,15 +51,26 @@
|
||||
"workflow_node.apply.form.key_algorithm.placeholder": "请选择数字证书算法",
|
||||
"workflow_node.apply.form.nameservers.label": "DNS 递归服务器(可选)",
|
||||
"workflow_node.apply.form.nameservers.placeholder": "请输入 DNS 递归服务器(多个值请用半角分号隔开)",
|
||||
"workflow_node.apply.form.nameservers.tooltip": "在 ACME DNS-01 认证时使用自定义的 DNS 递归服务器。如果你不了解该选项的用途,保持默认即可。<br><a href=\"https://go-acme.github.io/lego/usage/cli/options/index.html#dns-resolvers-and-challenge-verification\" target=\"_blank\">点此了解更多</a>。",
|
||||
"workflow_node.apply.form.nameservers.tooltip": "在 ACME DNS-01 认证时使用自定义的 DNS 递归服务器。如果你不了解该选项的用途,保持默认即可。<a href=\"https://go-acme.github.io/lego/usage/cli/options/index.html#dns-resolvers-and-challenge-verification\" target=\"_blank\">点此了解更多</a>。",
|
||||
"workflow_node.apply.form.nameservers.multiple_input_modal.title": "修改 DNS 递归服务器",
|
||||
"workflow_node.apply.form.nameservers.multiple_input_modal.placeholder": "请输入 DNS 递归服务器",
|
||||
"workflow_node.apply.form.propagation_timeout.label": "DNS 传播检查超时时间(可选)",
|
||||
"workflow_node.apply.form.propagation_timeout.placeholder": "请输入 DNS 传播检查超时时间",
|
||||
"workflow_node.apply.form.propagation_timeout.suffix": "秒",
|
||||
"workflow_node.apply.form.propagation_timeout.tooltip": "在 ACME DNS-01 认证时等待 DNS 传播检查的最长时间。如果你不了解此选项的用途,保持默认即可。",
|
||||
"workflow_node.apply.form.dns_propagation_timeout.label": "DNS 传播检查超时时间(可选)",
|
||||
"workflow_node.apply.form.dns_propagation_timeout.placeholder": "请输入 DNS 传播检查超时时间",
|
||||
"workflow_node.apply.form.dns_propagation_timeout.unit": "秒",
|
||||
"workflow_node.apply.form.dns_propagation_timeout.tooltip": "在 ACME DNS-01 认证时等待 DNS 传播检查的最长时间。如果你不了解此选项的用途,保持默认即可。<br><br>为空时,将使用提供商提供的默认值。",
|
||||
"workflow_node.apply.form.dns_ttl.label": "DNS 解析 TTL(可选)",
|
||||
"workflow_node.apply.form.dns_ttl.placeholder": "请输入 DNS 解析 TTL",
|
||||
"workflow_node.apply.form.dns_ttl.unit": "秒",
|
||||
"workflow_node.apply.form.dns_ttl.tooltip": "在 ACME DNS-01 认证时 DNS 解析记录的 TTL。如果你不了解此选项的用途,保持默认即可。<br><br>为空时,将使用提供商提供的默认值。",
|
||||
"workflow_node.apply.form.disable_follow_cname.label": "禁止 CNAME 跟随",
|
||||
"workflow_node.apply.form.disable_follow_cname.tooltip": "在 ACME DNS-01 认证时是否禁止 CNAME 跟随。如果你不了解该选项的用途,保持默认即可。<br><a href=\"https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname\" target=\"_blank\">点此了解更多</a>。",
|
||||
"workflow_node.apply.form.disable_follow_cname.tooltip": "在 ACME DNS-01 认证时是否禁止 CNAME 跟随。如果你不了解该选项的用途,保持默认即可。<a href=\"https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname\" target=\"_blank\">点此了解更多</a>。",
|
||||
"workflow_node.apply.form.strategy_config.label": "执行策略",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.label": "续期间隔",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.placeholder": "请输入续期间隔",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.prefix": "当上次签发的证书到期时间超过",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.suffix": "时,跳过重新申请。",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.unit": "天",
|
||||
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过 CA 的证书有效期限制,否则证书可能永远不会续期。",
|
||||
|
||||
"workflow_node.deploy.label": "部署",
|
||||
"workflow_node.deploy.search.provider.placeholder": "搜索部署目标……",
|
||||
@@ -130,9 +141,9 @@
|
||||
"workflow_node.deploy.form.aliyun_nlb_listener_id.label": "阿里云 NLB 监听器 ID",
|
||||
"workflow_node.deploy.form.aliyun_nlb_listener_id.placeholder": "请输入阿里云 NLB 监听器 ID",
|
||||
"workflow_node.deploy.form.aliyun_nlb_listener_id.tooltip": "这是什么?请参阅 <a href=\"https://slb.console.aliyun.com/nlb\" target=\"_blank\">https://slb.console.aliyun.com/nlb</a>",
|
||||
"workflow_node.deploy.form.aliyun_oss_endpoint.label": "阿里云 OSS Endpoint",
|
||||
"workflow_node.deploy.form.aliyun_oss_endpoint.placeholder": "请输入阿里云 OSS Endpoint",
|
||||
"workflow_node.deploy.form.aliyun_oss_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints\" target=\"_blank\">https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints</a>",
|
||||
"workflow_node.deploy.form.aliyun_oss_region.label": "阿里云地域",
|
||||
"workflow_node.deploy.form.aliyun_oss_region.placeholder": "请输入阿里云地域(例如:cn-hangzhou)",
|
||||
"workflow_node.deploy.form.aliyun_oss_region.tooltip": "这是什么?请参阅 <a href=\"https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints\" target=\"_blank\">https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints</a>",
|
||||
"workflow_node.deploy.form.aliyun_oss_bucket.label": "阿里云 OSS 存储桶名",
|
||||
"workflow_node.deploy.form.aliyun_oss_bucket.placeholder": "请输入阿里云 OSS 存储桶名",
|
||||
"workflow_node.deploy.form.aliyun_oss_bucket.tooltip": "这是什么?请参阅 <a href=\"https://oss.console.aliyun.com\" target=\"_blank\">https://oss.console.aliyun.com</a>",
|
||||
@@ -348,6 +359,12 @@
|
||||
"workflow_node.deploy.form.webhook_data.guide": "小贴士:回调数据是一个 JSON 格式的键值对。其中值支持模板变量,将在被发送到指定的 Webhook URL 时被替换为实际值;其他内容将保持原样。<br><br>支持的变量:<br><strong>${DOMAIN}</strong>:证书的主域名(即 <i>CommonName</i>)<br><strong>${DOMAINS}</strong>:证书的多域名列表(即 <i>SubjectAltNames</i>)<br><strong>${CERTIFICATE}</strong>:证书文件 PEM 格式内容<br><strong>${PRIVATE_KEY}</strong>:私钥文件 PEM 格式内容",
|
||||
"workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
||||
"workflow_node.deploy.form.webhook_data_preset.button": "使用预设模板",
|
||||
"workflow_node.deploy.form.strategy_config.label": "执行策略",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.label": "重复部署",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.prefix": "当上次部署已成功时",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.suffix": "重新部署。",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.enabled.on": "跳过",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.enabled.off": "不跳过",
|
||||
|
||||
"workflow_node.notify.label": "通知",
|
||||
"workflow_node.notify.form.subject.label": "通知主题",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
|
||||
import Version from "@/components/Version";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
import { getAuthStore } from "@/repository/admin";
|
||||
|
||||
const AuthLayout = () => {
|
||||
const auth = getPocketBase().authStore;
|
||||
if (auth.isValid && auth.isAdmin) {
|
||||
const auth = getAuthStore();
|
||||
if (auth.isValid && auth.isSuperuser) {
|
||||
return <Navigate to="/" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Button, type ButtonProps, Drawer, Dropdown, Layout, Menu, type MenuProp
|
||||
|
||||
import Version from "@/components/Version";
|
||||
import { useBrowserTheme, useTriggerElement } from "@/hooks";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
import { getAuthStore } from "@/repository/admin";
|
||||
|
||||
const ConsoleLayout = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -35,8 +35,8 @@ const ConsoleLayout = () => {
|
||||
navigate("/settings/account");
|
||||
};
|
||||
|
||||
const auth = getPocketBase().authStore;
|
||||
if (!auth.isValid || !auth.isAdmin) {
|
||||
const auth = getAuthStore();
|
||||
if (!auth.isValid || !auth.isSuperuser) {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useAntdForm } from "@/hooks";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
import { authWithPassword } from "@/repository/admin";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
const Login = () => {
|
||||
@@ -27,7 +27,7 @@ const Login = () => {
|
||||
} = useAntdForm<z.infer<typeof formSchema>>({
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
await getPocketBase().admins.authWithPassword(values.username, values.password);
|
||||
await authWithPassword(values.username, values.password);
|
||||
await navigage("/");
|
||||
} catch (err) {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useAntdForm } from "@/hooks";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
import { getAuthStore, save as saveAdmin } from "@/repository/admin";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
const SettingsAccount = () => {
|
||||
@@ -27,18 +27,16 @@ const SettingsAccount = () => {
|
||||
formProps,
|
||||
} = useAntdForm<z.infer<typeof formSchema>>({
|
||||
initialValues: {
|
||||
username: getPocketBase().authStore.model?.email,
|
||||
username: getAuthStore().record?.email,
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
await getPocketBase().admins.update(getPocketBase().authStore.model?.id, {
|
||||
email: values.username,
|
||||
});
|
||||
await saveAdmin({ email: values.username });
|
||||
|
||||
messageApi.success(t("common.text.operation_succeeded"));
|
||||
|
||||
setTimeout(() => {
|
||||
getPocketBase().authStore.clear();
|
||||
getAuthStore().clear();
|
||||
navigate("/login");
|
||||
}, 500);
|
||||
} catch (err) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useAntdForm } from "@/hooks";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
import { authWithPassword, getAuthStore, save as saveAdmin } from "@/repository/admin";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
const SettingsPassword = () => {
|
||||
@@ -33,16 +33,13 @@ const SettingsPassword = () => {
|
||||
} = useAntdForm({
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
await getPocketBase().admins.authWithPassword(getPocketBase().authStore.model?.email, values.oldPassword);
|
||||
await getPocketBase().admins.update(getPocketBase().authStore.model?.id, {
|
||||
password: values.newPassword,
|
||||
passwordConfirm: values.confirmPassword,
|
||||
});
|
||||
await authWithPassword(getAuthStore().record!.email, values.oldPassword);
|
||||
await saveAdmin({ password: values.newPassword });
|
||||
|
||||
messageApi.success(t("common.text.operation_succeeded"));
|
||||
|
||||
setTimeout(() => {
|
||||
getPocketBase().authStore.clear();
|
||||
getAuthStore().clear();
|
||||
navigate("/login");
|
||||
}, 500);
|
||||
} catch (err) {
|
||||
|
||||
@@ -97,10 +97,6 @@ const SSLProviderEditFormLetsEncryptStagingConfig = () => {
|
||||
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("settings.sslprovider.form.letsencrypt_staging_alert") }}></span>} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Alert type="warning" message={<span dangerouslySetInnerHTML={{ __html: t("settings.sslprovider.form.letsencrypt_staging_warning") }}></span>} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled={!formChanged} loading={pending}>
|
||||
{t("common.button.save")}
|
||||
@@ -324,28 +320,36 @@ const SettingsSSLProvider = () => {
|
||||
avatar={<img src={"/imgs/acme/letsencrypt.svg"} className="size-8" />}
|
||||
size="small"
|
||||
title={t("settings.sslprovider.form.provider.option.letsencrypt.label")}
|
||||
description="letsencrypt.org"
|
||||
value={SSLPROVIDERS.LETS_ENCRYPT}
|
||||
/>
|
||||
<CheckCard
|
||||
avatar={<img src={"/imgs/acme/letsencrypt.svg"} className="size-8" />}
|
||||
size="small"
|
||||
title={t("settings.sslprovider.form.provider.option.letsencrypt_staging.label")}
|
||||
description="letsencrypt.org"
|
||||
value={SSLPROVIDERS.LETS_ENCRYPT_STAGING}
|
||||
/>
|
||||
<CheckCard
|
||||
avatar={<img src={"/imgs/acme/zerossl.svg"} className="size-8" />}
|
||||
size="small"
|
||||
title={t("settings.sslprovider.form.provider.option.zerossl.label")}
|
||||
description="zerossl.com"
|
||||
value={SSLPROVIDERS.ZERO_SSL}
|
||||
/>
|
||||
<CheckCard
|
||||
avatar={<img src={"/imgs/acme/google.svg"} className="size-8" />}
|
||||
size="small"
|
||||
title={t("settings.sslprovider.form.provider.option.gts.label")}
|
||||
description="pki.goog"
|
||||
value={SSLPROVIDERS.GOOGLE_TRUST_SERVICES}
|
||||
/>
|
||||
</CheckCard.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Alert type="warning" message={<span dangerouslySetInnerHTML={{ __html: t("settings.sslprovider.form.provider.alert") }}></span>} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className="md:max-w-[40rem]">{providerFormEl}</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { isEqual } from "radash";
|
||||
import { z } from "zod";
|
||||
|
||||
import { run as runWorkflow } from "@/api/workflow";
|
||||
import { run as runWorkflow } from "@/api/workflows";
|
||||
import ModalForm from "@/components/ModalForm";
|
||||
import Show from "@/components/Show";
|
||||
import WorkflowElements from "@/components/workflow/WorkflowElements";
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DeleteOutlined as DeleteOutlinedIcon,
|
||||
EditOutlined as EditOutlinedIcon,
|
||||
PlusOutlined as PlusOutlinedIcon,
|
||||
SyncOutlined as SyncOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
@@ -159,7 +160,14 @@ const WorkflowList = () => {
|
||||
title: t("workflow.props.last_run_at"),
|
||||
render: (_, record) => {
|
||||
if (record.lastRunId) {
|
||||
if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
|
||||
if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING) {
|
||||
return (
|
||||
<Space>
|
||||
<Badge status="processing" count={<SyncOutlinedIcon style={{ color: themeToken.colorInfo }} />} />
|
||||
<Typography.Text>{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
|
||||
return (
|
||||
<Space>
|
||||
<Badge status="success" count={<CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />} />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { Card, Col, Form, Input, Row, Spin, Typography, notification } from "antd";
|
||||
import { Card, Col, Form, Input, type InputRef, Row, Spin, Typography, notification } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -81,6 +81,17 @@ const WorkflowNew = () => {
|
||||
});
|
||||
const [formModalOpen, setFormModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (formModalOpen) {
|
||||
setTimeout(() => inputRef.current?.focus({ cursor: "end" }), 1);
|
||||
} else {
|
||||
setTemplateSelectKey(undefined);
|
||||
formInst.resetFields();
|
||||
}
|
||||
}, [formModalOpen]);
|
||||
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
|
||||
const handleTemplateClick = (key: TemplateKeys) => {
|
||||
setTemplateSelectKey(key);
|
||||
setFormModalOpen(true);
|
||||
@@ -88,11 +99,6 @@ const WorkflowNew = () => {
|
||||
|
||||
const handleModalOpenChange = (open: boolean) => {
|
||||
setFormModalOpen(open);
|
||||
|
||||
if (!open) {
|
||||
setTemplateSelectKey(undefined);
|
||||
formInst.resetFields();
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalFormFinish = () => {
|
||||
@@ -155,6 +161,7 @@ const WorkflowNew = () => {
|
||||
|
||||
<ModalForm
|
||||
{...formProps}
|
||||
autoFocus
|
||||
disabled={formPending}
|
||||
layout="vertical"
|
||||
form={formInst}
|
||||
@@ -167,7 +174,7 @@ const WorkflowNew = () => {
|
||||
onOpenChange={handleModalOpenChange}
|
||||
>
|
||||
<Form.Item name="name" label={t("workflow.new.modal.form.name.label")} rules={[formRule]}>
|
||||
<Input ref={(ref) => setTimeout(() => ref?.focus({ cursor: "end" }), 0)} placeholder={t("workflow.new.modal.form.name.placeholder")} />
|
||||
<Input ref={inputRef} autoFocus placeholder={t("workflow.new.modal.form.name.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label={t("workflow.new.modal.form.description.label")} rules={[formRule]}>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import PocketBase from "pocketbase";
|
||||
|
||||
const apiDomain = import.meta.env.VITE_API_DOMAIN;
|
||||
console.log("VITE_API_DOMAIN:", apiDomain);
|
||||
|
||||
let pb: PocketBase;
|
||||
export const getPocketBase = () => {
|
||||
if (pb) return pb;
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
import { getPocketBase } from "./pocketbase";
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "access";
|
||||
|
||||
@@ -22,13 +22,8 @@ export const save = async (record: MaybeModelRecord<AccessModel>) => {
|
||||
};
|
||||
|
||||
export const remove = async (record: MaybeModelRecordWithId<AccessModel>) => {
|
||||
record = { ...record, deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") };
|
||||
|
||||
// TODO: 仅为兼容旧版本,后续迭代时删除
|
||||
if ("provider" in record && record.provider === "httpreq") record.provider = "acmehttpreq";
|
||||
if ("provider" in record && record.provider === "tencent") record.provider = "tencentcloud";
|
||||
if ("provider" in record && record.provider === "pdns") record.provider = "powerdns";
|
||||
|
||||
await getPocketBase().collection(COLLECTION_NAME).update<AccessModel>(record.id!, record);
|
||||
await getPocketBase()
|
||||
.collection(COLLECTION_NAME)
|
||||
.update<AccessModel>(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") });
|
||||
return true;
|
||||
};
|
||||
|
||||
17
ui/src/repository/admin.ts
Normal file
17
ui/src/repository/admin.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "_superusers";
|
||||
|
||||
export const authWithPassword = (username: string, password: string) => {
|
||||
return getPocketBase().collection(COLLECTION_NAME).authWithPassword(username, password);
|
||||
};
|
||||
|
||||
export const getAuthStore = () => {
|
||||
return getPocketBase().authStore;
|
||||
};
|
||||
|
||||
export const save = (data: { email: string } | { password: string }) => {
|
||||
return getPocketBase()
|
||||
.collection(COLLECTION_NAME)
|
||||
.update(getAuthStore().record?.id || "", data);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
|
||||
import { type RecordListOptions } from "pocketbase";
|
||||
|
||||
import { type CertificateModel } from "@/domain/certificate";
|
||||
import { getPocketBase } from "./pocketbase";
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "certificate";
|
||||
|
||||
@@ -39,8 +39,8 @@ export const list = async (request: ListCertificateRequest) => {
|
||||
};
|
||||
|
||||
export const remove = async (record: MaybeModelRecordWithId<CertificateModel>) => {
|
||||
record = { ...record, deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") };
|
||||
|
||||
await getPocketBase().collection(COLLECTION_NAME).update<CertificateModel>(record.id!, record);
|
||||
await getPocketBase()
|
||||
.collection(COLLECTION_NAME)
|
||||
.update<CertificateModel>(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") });
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { type SettingsModel, type SettingsNames } from "@/domain/settings";
|
||||
import { getPocketBase } from "./pocketbase";
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "settings";
|
||||
|
||||
export const get = async <T extends NonNullable<unknown>>(name: SettingsNames) => {
|
||||
try {
|
||||
const resp = await getPocketBase().collection("settings").getFirstListItem<SettingsModel<T>>(`name='${name}'`, {
|
||||
const resp = await getPocketBase().collection(COLLECTION_NAME).getFirstListItem<SettingsModel<T>>(`name='${name}'`, {
|
||||
requestKey: null,
|
||||
});
|
||||
return resp;
|
||||
@@ -23,8 +25,8 @@ export const get = async <T extends NonNullable<unknown>>(name: SettingsNames) =
|
||||
|
||||
export const save = async <T extends NonNullable<unknown>>(record: MaybeModelRecordWithId<SettingsModel<T>>) => {
|
||||
if (record.id) {
|
||||
return await getPocketBase().collection("settings").update<SettingsModel<T>>(record.id, record);
|
||||
return await getPocketBase().collection(COLLECTION_NAME).update<SettingsModel<T>>(record.id, record);
|
||||
}
|
||||
|
||||
return await getPocketBase().collection("settings").create<SettingsModel<T>>(record);
|
||||
return await getPocketBase().collection(COLLECTION_NAME).create<SettingsModel<T>>(record);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type RecordListOptions, type RecordSubscription } from "pocketbase";
|
||||
|
||||
import { type WorkflowModel } from "@/domain/workflow";
|
||||
import { getPocketBase } from "./pocketbase";
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "workflow";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type WorkflowRunModel } from "@/domain/workflowRun";
|
||||
|
||||
import { getPocketBase } from "./pocketbase";
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "workflow_run";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import JSZip from "jszip";
|
||||
|
||||
export function readFileContent(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -17,19 +15,3 @@ export function readFileContent(file: File): Promise<string> {
|
||||
reader.readAsText(file, "utf-8");
|
||||
});
|
||||
}
|
||||
|
||||
export const saveFiles2Zip = async (zipName: string, files: { name: string; content: string }[]) => {
|
||||
const zip = new JSZip();
|
||||
|
||||
files.forEach((file) => {
|
||||
zip.file(file.name, file.content);
|
||||
});
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
// Save the zip file to the local system
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(content);
|
||||
link.download = zipName;
|
||||
link.click();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user