workflow data save
This commit is contained in:
@@ -19,4 +19,3 @@ export const notifyTest = async (channel: string) => {
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
||||
|
||||
@@ -123,22 +123,29 @@ const Bark = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const [testing, setTesting] = useState<boolean>(false);
|
||||
const handlePushTestClick = async () => {
|
||||
if (testing) return;
|
||||
|
||||
try {
|
||||
setTesting(true);
|
||||
|
||||
await notifyTest("bark");
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.success.message"),
|
||||
description: t("settings.notification.config.push.test.message.success.message"),
|
||||
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = getErrMessage(e);
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
||||
title: t("settings.notification.push_test_message.failed.message"),
|
||||
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -177,67 +184,76 @@ const Bark = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
placeholder={t("settings.notification.bark.serverUrl.placeholder")}
|
||||
value={bark.data.serverUrl}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...bark,
|
||||
data: {
|
||||
...bark.data,
|
||||
serverUrl: e.target.value,
|
||||
},
|
||||
};
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>
|
||||
<Label>{t("settings.notification.bark.server_url.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.bark.server_url.placeholder")}
|
||||
value={bark.data.serverUrl}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...bark,
|
||||
data: {
|
||||
...bark.data,
|
||||
serverUrl: e.target.value,
|
||||
},
|
||||
};
|
||||
|
||||
checkChanged(newData.data);
|
||||
setBark(newData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder={t("settings.notification.bark.deviceKey.placeholder")}
|
||||
value={bark.data.deviceKey}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...bark,
|
||||
data: {
|
||||
...bark.data,
|
||||
deviceKey: e.target.value,
|
||||
},
|
||||
};
|
||||
|
||||
checkChanged(newData.data);
|
||||
setBark(newData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex items-center space-x-1 mt-2">
|
||||
<Switch id="airplane-mode" checked={bark.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
checkChanged(newData.data);
|
||||
setBark(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-2">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div>
|
||||
<Label>{t("settings.notification.bark.device_key.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.bark.device_key.placeholder")}
|
||||
value={bark.data.deviceKey}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...bark,
|
||||
data: {
|
||||
...bark.data,
|
||||
deviceKey: e.target.value,
|
||||
},
|
||||
};
|
||||
|
||||
<Show when={!changed && bark.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.config.push.test.message")}
|
||||
</Button>
|
||||
</Show>
|
||||
checkChanged(newData.data);
|
||||
setBark(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id="airplane-mode" checked={bark.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
<Show when={!changed && bark.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
loading={testing}
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.push_test_message")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -120,23 +120,30 @@ const DingTalk = () => {
|
||||
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const [testing, setTesting] = useState<boolean>(false);
|
||||
const handlePushTestClick = async () => {
|
||||
if (testing) return;
|
||||
|
||||
try {
|
||||
setTesting(true);
|
||||
|
||||
await notifyTest("dingtalk");
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.success.message"),
|
||||
description: t("settings.notification.config.push.test.message.success.message"),
|
||||
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = getErrMessage(e);
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
||||
title: t("settings.notification.push_test_message.failed.message"),
|
||||
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
@@ -177,64 +184,74 @@ const DingTalk = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
placeholder="AccessToken"
|
||||
value={dingtalk.data.accessToken}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...dingtalk,
|
||||
data: {
|
||||
...dingtalk.data,
|
||||
accessToken: e.target.value,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setDingtalk(newData);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("settings.notification.dingtalk.secret.placeholder")}
|
||||
className="mt-2"
|
||||
value={dingtalk.data.secret}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...dingtalk,
|
||||
data: {
|
||||
...dingtalk.data,
|
||||
secret: e.target.value,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setDingtalk(newData);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center space-x-1 mt-2">
|
||||
<Switch id="airplane-mode" checked={dingtalk.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>
|
||||
<Label>{t("settings.notification.dingtalk.access_token.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.dingtalk.access_token.placeholder")}
|
||||
value={dingtalk.data.accessToken}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...dingtalk,
|
||||
data: {
|
||||
...dingtalk.data,
|
||||
accessToken: e.target.value,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setDingtalk(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-2">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div>
|
||||
<Label>{t("settings.notification.dingtalk.secret.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.dingtalk.secret.placeholder")}
|
||||
value={dingtalk.data.secret}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...dingtalk,
|
||||
data: {
|
||||
...dingtalk.data,
|
||||
secret: e.target.value,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setDingtalk(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Show when={!changed && dingtalk.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.config.push.test.message")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div className="flex justify-between gap-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id="airplane-mode" checked={dingtalk.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
<Show when={!changed && dingtalk.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
loading={testing}
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.push_test_message")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
384
ui/src/components/notify/Email.tsx
Normal file
384
ui/src/components/notify/Email.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { getErrMessage } from "@/lib/error";
|
||||
import { NotifyChannelEmail, NotifyChannels } from "@/domain/settings";
|
||||
import { useNotifyContext } from "@/providers/notify";
|
||||
import { update } from "@/repository/settings";
|
||||
import Show from "@/components/Show";
|
||||
import { notifyTest } from "@/api/notify";
|
||||
|
||||
type EmailSetting = {
|
||||
id: string;
|
||||
name: string;
|
||||
data: NotifyChannelEmail;
|
||||
};
|
||||
|
||||
const Mail = () => {
|
||||
const { config, setChannels } = useNotifyContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [changed, setChanged] = useState<boolean>(false);
|
||||
|
||||
const [mail, setMail] = useState<EmailSetting>({
|
||||
id: config.id ?? "",
|
||||
name: "notifyChannels",
|
||||
data: {
|
||||
smtpHost: "",
|
||||
smtpPort: 465,
|
||||
smtpTLS: true,
|
||||
username: "",
|
||||
password: "",
|
||||
senderAddress: "",
|
||||
receiverAddress: "",
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
const [originMail, setOriginMail] = useState<EmailSetting>({
|
||||
id: config.id ?? "",
|
||||
name: "notifyChannels",
|
||||
data: {
|
||||
smtpHost: "",
|
||||
smtpPort: 465,
|
||||
smtpTLS: true,
|
||||
username: "",
|
||||
password: "",
|
||||
senderAddress: "",
|
||||
receiverAddress: "",
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setChanged(false);
|
||||
}, [config]);
|
||||
|
||||
useEffect(() => {
|
||||
const data = getDetailMail();
|
||||
setOriginMail({
|
||||
id: config.id ?? "",
|
||||
name: "email",
|
||||
data,
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
useEffect(() => {
|
||||
const data = getDetailMail();
|
||||
setMail({
|
||||
id: config.id ?? "",
|
||||
name: "email",
|
||||
data,
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const getDetailMail = () => {
|
||||
const df: NotifyChannelEmail = {
|
||||
smtpHost: "smtp.example.com",
|
||||
smtpPort: 465,
|
||||
smtpTLS: true,
|
||||
username: "",
|
||||
password: "",
|
||||
senderAddress: "",
|
||||
receiverAddress: "",
|
||||
enabled: false,
|
||||
};
|
||||
if (!config.content) {
|
||||
return df;
|
||||
}
|
||||
const chanels = config.content as NotifyChannels;
|
||||
if (!chanels.email) {
|
||||
return df;
|
||||
}
|
||||
|
||||
return chanels.email as NotifyChannelEmail;
|
||||
};
|
||||
|
||||
const checkChanged = (data: NotifyChannelEmail) => {
|
||||
if (
|
||||
data.smtpHost !== originMail.data.smtpHost ||
|
||||
data.smtpPort !== originMail.data.smtpPort ||
|
||||
data.smtpTLS !== originMail.data.smtpTLS ||
|
||||
data.username !== originMail.data.username ||
|
||||
data.password !== originMail.data.password ||
|
||||
data.senderAddress !== originMail.data.senderAddress ||
|
||||
data.receiverAddress !== originMail.data.receiverAddress
|
||||
) {
|
||||
setChanged(true);
|
||||
} else {
|
||||
setChanged(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveClick = async () => {
|
||||
try {
|
||||
const resp = await update({
|
||||
...config,
|
||||
name: "notifyChannels",
|
||||
content: {
|
||||
...config.content,
|
||||
email: {
|
||||
...mail.data,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setChannels(resp);
|
||||
toast({
|
||||
title: t("common.save.succeeded.message"),
|
||||
description: t("settings.notification.config.saved.message"),
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = getErrMessage(e);
|
||||
|
||||
toast({
|
||||
title: t("common.save.failed.message"),
|
||||
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [testing, setTesting] = useState<boolean>(false);
|
||||
const handlePushTestClick = async () => {
|
||||
if (testing) return;
|
||||
|
||||
try {
|
||||
setTesting(true);
|
||||
|
||||
await notifyTest("email");
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = getErrMessage(e);
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.push_test_message.failed.message"),
|
||||
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchChange = async () => {
|
||||
const newData = {
|
||||
...mail,
|
||||
data: {
|
||||
...mail.data,
|
||||
enabled: !mail.data.enabled,
|
||||
},
|
||||
};
|
||||
setMail(newData);
|
||||
|
||||
try {
|
||||
const resp = await update({
|
||||
...config,
|
||||
name: "notifyChannels",
|
||||
content: {
|
||||
...config.content,
|
||||
email: {
|
||||
...newData.data,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setChannels(resp);
|
||||
} catch (e) {
|
||||
const msg = getErrMessage(e);
|
||||
|
||||
toast({
|
||||
title: t("common.save.failed.message"),
|
||||
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex space-x-4">
|
||||
<div className="w-2/5">
|
||||
<Label>{t("settings.notification.email.smtp_host.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.email.smtp_host.placeholder")}
|
||||
value={mail.data.smtpHost}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...mail,
|
||||
data: {
|
||||
...mail.data,
|
||||
smtpHost: e.target.value,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setMail(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-2/5">
|
||||
<Label>{t("settings.notification.email.smtp_port.label")}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t("settings.notification.email.smtp_port.placeholder")}
|
||||
value={mail.data.smtpPort}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...mail,
|
||||
data: {
|
||||
...mail.data,
|
||||
smtpPort: +e.target.value || 0,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setMail(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-1/5">
|
||||
<Label>{t("settings.notification.email.smtp_tls.label")}</Label>
|
||||
<Switch
|
||||
className="block mt-2"
|
||||
checked={mail.data.smtpTLS}
|
||||
onCheckedChange={(e) => {
|
||||
const newData = {
|
||||
...mail,
|
||||
data: {
|
||||
...mail.data,
|
||||
smtpPort: e && mail.data.smtpPort === 25 ? 465 : !e && mail.data.smtpPort === 465 ? 25 : mail.data.smtpPort,
|
||||
smtpTLS: e,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setMail(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<div className="w-1/2">
|
||||
<Label>{t("settings.notification.email.username.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.email.username.placeholder")}
|
||||
value={mail.data.username}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...mail,
|
||||
data: {
|
||||
...mail.data,
|
||||
username: e.target.value,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setMail(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-1/2">
|
||||
<Label>{t("settings.notification.email.password.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.email.password.placeholder")}
|
||||
value={mail.data.password}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...mail,
|
||||
data: {
|
||||
...mail.data,
|
||||
password: e.target.value,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setMail(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("settings.notification.email.sender_address.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.email.sender_address.placeholder")}
|
||||
value={mail.data.senderAddress}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...mail,
|
||||
data: {
|
||||
...mail.data,
|
||||
senderAddress: e.target.value,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setMail(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("settings.notification.email.receiver_address.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.email.receiver_address.placeholder")}
|
||||
value={mail.data.receiverAddress}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...mail,
|
||||
data: {
|
||||
...mail.data,
|
||||
receiverAddress: e.target.value,
|
||||
},
|
||||
};
|
||||
checkChanged(newData.data);
|
||||
setMail(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id="airplane-mode" checked={mail.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
<Show when={!changed && mail.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
loading={testing}
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.push_test_message")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mail;
|
||||
@@ -116,23 +116,30 @@ const Lark = () => {
|
||||
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const [testing, setTesting] = useState<boolean>(false);
|
||||
const handlePushTestClick = async () => {
|
||||
if (testing) return;
|
||||
|
||||
try {
|
||||
setTesting(true);
|
||||
|
||||
await notifyTest("lark");
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.success.message"),
|
||||
description: t("settings.notification.config.push.test.message.success.message"),
|
||||
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = getErrMessage(e);
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
||||
title: t("settings.notification.push_test_message.failed.message"),
|
||||
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
@@ -173,49 +180,56 @@ const Lark = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Webhook Url"
|
||||
value={lark.data.webhookUrl}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...lark,
|
||||
data: {
|
||||
...lark.data,
|
||||
webhookUrl: e.target.value,
|
||||
},
|
||||
};
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>
|
||||
<Label>{t("settings.notification.lark.webhook_url.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.lark.webhook_url.placeholder")}
|
||||
value={lark.data.webhookUrl}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...lark,
|
||||
data: {
|
||||
...lark.data,
|
||||
webhookUrl: e.target.value,
|
||||
},
|
||||
};
|
||||
|
||||
checkChanged(newData.data);
|
||||
setLark(newData);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center space-x-1 mt-2">
|
||||
<Switch id="airplane-mode" checked={lark.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
checkChanged(newData.data);
|
||||
setLark(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-2">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div className="flex justify-between gap-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id="airplane-mode" checked={lark.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
</div>
|
||||
|
||||
<Show when={!changed && lark.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.config.push.test.message")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
<Show when={!changed && lark.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
loading={testing}
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.push_test_message")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -97,7 +97,7 @@ const ServerChan = () => {
|
||||
if (!isValidURL(serverchan.data.url)) {
|
||||
toast({
|
||||
title: t("common.save.failed.message"),
|
||||
description: t("settings.notification.url.errmsg.invalid"),
|
||||
description: t("common.errmsg.url_invalid"),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -130,22 +130,29 @@ const ServerChan = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const [testing, setTesting] = useState<boolean>(false);
|
||||
const handlePushTestClick = async () => {
|
||||
if (testing) return;
|
||||
|
||||
try {
|
||||
setTesting(true);
|
||||
|
||||
await notifyTest("serverchan");
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.success.message"),
|
||||
description: t("settings.notification.config.push.test.message.success.message"),
|
||||
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = getErrMessage(e);
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
||||
title: t("settings.notification.push_test_message.failed.message"),
|
||||
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,50 +191,56 @@ const ServerChan = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
placeholder={t("settings.notification.serverchan.url.placeholder")}
|
||||
value={serverchan.data.url}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...serverchan,
|
||||
data: {
|
||||
...serverchan.data,
|
||||
url: e.target.value,
|
||||
},
|
||||
};
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>
|
||||
<Label>{t("settings.notification.serverchan.url.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.serverchan.url.placeholder")}
|
||||
value={serverchan.data.url}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...serverchan,
|
||||
data: {
|
||||
...serverchan.data,
|
||||
url: e.target.value,
|
||||
},
|
||||
};
|
||||
|
||||
checkChanged(newData.data);
|
||||
setServerChan(newData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex items-center space-x-1 mt-2">
|
||||
<Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
checkChanged(newData.data);
|
||||
setServerChan(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-2">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div className="flex justify-between gap-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
</div>
|
||||
|
||||
<Show when={!changed && serverchan.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.config.push.test.message")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
<Show when={!changed && serverchan.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
loading={testing}
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.push_test_message")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -123,22 +123,29 @@ const Telegram = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const [testing, setTesting] = useState<boolean>(false);
|
||||
const handlePushTestClick = async () => {
|
||||
if (testing) return;
|
||||
|
||||
try {
|
||||
setTesting(true);
|
||||
|
||||
await notifyTest("telegram");
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.success.message"),
|
||||
description: t("settings.notification.config.push.test.message.success.message"),
|
||||
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = getErrMessage(e);
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
||||
title: t("settings.notification.push_test_message.failed.message"),
|
||||
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -177,67 +184,76 @@ const Telegram = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
placeholder="ApiToken"
|
||||
value={telegram.data.apiToken}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...telegram,
|
||||
data: {
|
||||
...telegram.data,
|
||||
apiToken: e.target.value,
|
||||
},
|
||||
};
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>
|
||||
<Label>{t("settings.notification.telegram.api_token.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.telegram.api_token.placeholder")}
|
||||
value={telegram.data.apiToken}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...telegram,
|
||||
data: {
|
||||
...telegram.data,
|
||||
apiToken: e.target.value,
|
||||
},
|
||||
};
|
||||
|
||||
checkChanged(newData.data);
|
||||
setTelegram(newData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="ChatId"
|
||||
value={telegram.data.chatId}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...telegram,
|
||||
data: {
|
||||
...telegram.data,
|
||||
chatId: e.target.value,
|
||||
},
|
||||
};
|
||||
|
||||
checkChanged(newData.data);
|
||||
setTelegram(newData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex items-center space-x-1 mt-2">
|
||||
<Switch id="airplane-mode" checked={telegram.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
checkChanged(newData.data);
|
||||
setTelegram(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-2">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div>
|
||||
<Label>{t("settings.notification.telegram.chat_id.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.telegram.chat_id.placeholder")}
|
||||
value={telegram.data.chatId}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...telegram,
|
||||
data: {
|
||||
...telegram.data,
|
||||
chatId: e.target.value,
|
||||
},
|
||||
};
|
||||
|
||||
<Show when={!changed && telegram.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.config.push.test.message")}
|
||||
</Button>
|
||||
</Show>
|
||||
checkChanged(newData.data);
|
||||
setTelegram(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id="airplane-mode" checked={telegram.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
<Show when={!changed && telegram.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
loading={testing}
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.push_test_message")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -97,7 +97,7 @@ const Webhook = () => {
|
||||
if (!isValidURL(webhook.data.url)) {
|
||||
toast({
|
||||
title: t("common.save.failed.message"),
|
||||
description: t("settings.notification.url.errmsg.invalid"),
|
||||
description: t("common.errmsg.url_invalid"),
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -130,22 +130,29 @@ const Webhook = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const [testing, setTesting] = useState<boolean>(false);
|
||||
const handlePushTestClick = async () => {
|
||||
if (testing) return;
|
||||
|
||||
try {
|
||||
setTesting(true);
|
||||
|
||||
await notifyTest("webhook");
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.success.message"),
|
||||
description: t("settings.notification.config.push.test.message.success.message"),
|
||||
title: t("settings.notification.push_test_message.succeeded.message"),
|
||||
description: t("settings.notification.push_test_message.succeeded.message"),
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = getErrMessage(e);
|
||||
|
||||
toast({
|
||||
title: t("settings.notification.config.push.test.message.failed.message"),
|
||||
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
|
||||
title: t("settings.notification.push_test_message.failed.message"),
|
||||
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,50 +191,56 @@ const Webhook = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Url"
|
||||
value={webhook.data.url}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...webhook,
|
||||
data: {
|
||||
...webhook.data,
|
||||
url: e.target.value,
|
||||
},
|
||||
};
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>
|
||||
<Label>{t("settings.notification.webhook.url.label")}</Label>
|
||||
<Input
|
||||
placeholder={t("settings.notification.webhook.url.placeholder")}
|
||||
value={webhook.data.url}
|
||||
onChange={(e) => {
|
||||
const newData = {
|
||||
...webhook,
|
||||
data: {
|
||||
...webhook.data,
|
||||
url: e.target.value,
|
||||
},
|
||||
};
|
||||
|
||||
checkChanged(newData.data);
|
||||
setWebhook(newData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex items-center space-x-1 mt-2">
|
||||
<Switch id="airplane-mode" checked={webhook.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
checkChanged(newData.data);
|
||||
setWebhook(newData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-2">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div className="flex justify-between gap-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id="airplane-mode" checked={webhook.data.enabled} onCheckedChange={handleSwitchChange} />
|
||||
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
|
||||
</div>
|
||||
|
||||
<Show when={!changed && webhook.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.config.push.test.message")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Show when={changed}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
<Show when={!changed && webhook.id != ""}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
loading={testing}
|
||||
onClick={() => {
|
||||
handlePushTestClick();
|
||||
}}
|
||||
>
|
||||
{t("settings.notification.push_test_message")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -32,11 +33,38 @@ const buttonVariants = cva(
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, loading, asChild = false, children, ...props }, ref) => {
|
||||
if (asChild) {
|
||||
return (
|
||||
<Slot ref={ref} {...props}>
|
||||
<>
|
||||
{React.Children.map(children as React.ReactElement, (child: React.ReactElement) => {
|
||||
return React.cloneElement(child, {
|
||||
className: cn(buttonVariants({ variant, size }), className),
|
||||
children: (
|
||||
<>
|
||||
{loading && <Loader2 className={cn("h-4 w-4 animate-spin", children && "mr-2")} />}
|
||||
{child.props.children}
|
||||
</>
|
||||
),
|
||||
});
|
||||
})}
|
||||
</>
|
||||
</Slot>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={cn(buttonVariants({ variant, size, className }))} disabled={loading} ref={ref} {...props}>
|
||||
<>
|
||||
{loading && <Loader2 className={cn("h-4 w-4 animate-spin", children && "mr-2")} />}
|
||||
{children}
|
||||
</>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
Button.displayName = "Button";
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ const BranchNode = memo(({ data }: BrandNodeProps) => {
|
||||
}}
|
||||
size={"sm"}
|
||||
variant={"outline"}
|
||||
className="text-xs px-2 h-6 rounded-full absolute -top-3 left-[50%] -translate-x-1/2 z-10"
|
||||
className="text-xs px-2 h-6 rounded-full absolute -top-3 left-[50%] -translate-x-1/2 z-10 dark:text-stone-200"
|
||||
>
|
||||
{t("workflow.node.addBranch.label")}
|
||||
</Button>
|
||||
|
||||
@@ -34,7 +34,7 @@ const ConditionNode = ({ data, branchId, branchIndex }: NodeProps) => {
|
||||
</DropdownMenu>
|
||||
|
||||
<div className="w-[261px] flex flex-col justify-center text-foreground rounded-md bg-white px-5 py-5">
|
||||
<div contentEditable suppressContentEditableWarning onBlur={handleNameBlur} className="text-center outline-slate-200">
|
||||
<div contentEditable suppressContentEditableWarning onBlur={handleNameBlur} className="text-center outline-slate-200 dark:text-stone-600">
|
||||
{data.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export type DeployFormProps = {
|
||||
defaultProivder?: string;
|
||||
};
|
||||
const DeployForm = ({ data, defaultProivder }: DeployFormProps) => {
|
||||
return getForm(data, defaultProivder);
|
||||
return <div className="dark:text-stone-200">{getForm(data, defaultProivder)}</div>;
|
||||
};
|
||||
|
||||
export default memo(DeployForm);
|
||||
|
||||
@@ -39,7 +39,6 @@ const Node = ({ data }: NodeProps) => {
|
||||
};
|
||||
|
||||
const getSetting = () => {
|
||||
console.log(data);
|
||||
if (!data.validated) {
|
||||
return <>{t(`${i18nPrefix}.setting.label`)}</>;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ const NotifyForm = ({ data }: NotifyFormProps) => {
|
||||
e.stopPropagation();
|
||||
form.handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
className="space-y-8"
|
||||
className="space-y-8 dark:text-stone-200"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -22,12 +22,12 @@ export type NotifyChannels = {
|
||||
};
|
||||
|
||||
export type NotifyChannel =
|
||||
| NotifyChannelEmail
|
||||
| NotifyChannelWebhook
|
||||
| NotifyChannelDingTalk
|
||||
| NotifyChannelLark
|
||||
| NotifyChannelTelegram
|
||||
| NotifyChannelWebhook
|
||||
| NotifyChannelServerChan
|
||||
| NotifyChannelMail
|
||||
| NotifyChannelBark;
|
||||
|
||||
type ChannelLabel = {
|
||||
@@ -66,6 +66,21 @@ export const channels: ChannelLabel[] = [
|
||||
];
|
||||
|
||||
export const channelLabelMap: Map<string, ChannelLabel> = new Map(channels.map((item) => [item.name, item]));
|
||||
export type NotifyChannelEmail = {
|
||||
smtpHost: string;
|
||||
smtpPort: number;
|
||||
smtpTLS: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
senderAddress: string;
|
||||
receiverAddress: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type NotifyChannelWebhook = {
|
||||
url: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type NotifyChannelDingTalk = {
|
||||
accessToken: string;
|
||||
@@ -84,26 +99,11 @@ export type NotifyChannelTelegram = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type NotifyChannelWebhook = {
|
||||
url: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type NotifyChannelServerChan = {
|
||||
url: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type NotifyChannelMail = {
|
||||
senderAddress: string;
|
||||
receiverAddresses: string;
|
||||
smtpHostAddr: string;
|
||||
smtpHostPort: string;
|
||||
username: string;
|
||||
password: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type NotifyChannelBark = {
|
||||
deviceKey: string;
|
||||
serverUrl: string;
|
||||
|
||||
@@ -3,6 +3,20 @@ import { nanoid } from "nanoid";
|
||||
import i18n from "@/i18n";
|
||||
import { deployTargets, KVType } from "./domain";
|
||||
|
||||
export type Workflow = {
|
||||
id?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
type: string;
|
||||
crontab?: string;
|
||||
content?: WorkflowNode;
|
||||
draft?: WorkflowNode;
|
||||
enabled?: boolean;
|
||||
hasDraft?: boolean;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
};
|
||||
|
||||
export enum WorkflowNodeType {
|
||||
Start = "start",
|
||||
End = "end",
|
||||
@@ -94,6 +108,31 @@ type NewWorkflowNodeOptions = {
|
||||
providerType?: string;
|
||||
};
|
||||
|
||||
export const initWorkflow = (): Workflow => {
|
||||
// 开始节点
|
||||
const rs = newWorkflowNode(WorkflowNodeType.Start, {});
|
||||
let root = rs;
|
||||
|
||||
// 申请节点
|
||||
root.next = newWorkflowNode(WorkflowNodeType.Apply, {});
|
||||
root = root.next;
|
||||
|
||||
// 部署节点
|
||||
root.next = newWorkflowNode(WorkflowNodeType.Deploy, {});
|
||||
root = root.next;
|
||||
|
||||
// 通知节点
|
||||
root.next = newWorkflowNode(WorkflowNodeType.Notify, {});
|
||||
|
||||
return {
|
||||
name: i18n.t("workflow.default.name"),
|
||||
type: "auto",
|
||||
crontab: "0 0 * * *",
|
||||
enabled: false,
|
||||
draft: rs,
|
||||
};
|
||||
};
|
||||
|
||||
export const newWorkflowNode = (type: WorkflowNodeType, options: NewWorkflowNodeOptions): WorkflowNode | WorkflowBranchNode => {
|
||||
const id = nanoid();
|
||||
const typeName = workflowNodeTypeDefaultName.get(type) || "";
|
||||
|
||||
@@ -83,12 +83,12 @@
|
||||
"common.provider.local": "Local Deployment",
|
||||
"common.provider.ssh": "SSH Deployment",
|
||||
"common.provider.webhook": "Webhook",
|
||||
"common.provider.serverchan": "ServerChan",
|
||||
"common.provider.kubernetes": "Kubernetes",
|
||||
"common.provider.kubernetes.secret": "Kubernetes - Secret",
|
||||
"common.provider.email": "Email",
|
||||
"common.provider.dingtalk": "DingTalk",
|
||||
"common.provider.telegram": "Telegram",
|
||||
"common.provider.lark": "Lark",
|
||||
"common.provider.mail": "Mail",
|
||||
"common.provider.telegram": "Telegram",
|
||||
"common.provider.serverchan": "ServerChan",
|
||||
"common.provider.bark": "Bark"
|
||||
}
|
||||
|
||||
@@ -30,20 +30,40 @@
|
||||
"settings.notification.config.enable": "Enable",
|
||||
"settings.notification.config.saved.message": "Configuration saved successfully",
|
||||
"settings.notification.config.failed.message": "Configuration save failed",
|
||||
"settings.notification.config.push.test.message": "Send test notification",
|
||||
"settings.notification.config.push.test.message.failed.message": "Send test notification failed",
|
||||
"settings.notification.config.push.test.message.success.message": "Send test notification successfully",
|
||||
"settings.notification.dingtalk.secret.placeholder": "Signature for signed addition",
|
||||
"settings.notification.url.errmsg.invalid": "Invalid Url format",
|
||||
"settings.notification.serverchan.url.placeholder": "Url, e.g. https://sctapi.ftqq.com/****************.send",
|
||||
"settings.notification.mail.sender_address.placeholder": "Sender email address",
|
||||
"settings.notification.mail.receiver_address.placeholder": "Receiver email address",
|
||||
"settings.notification.mail.smtp_host.placeholder": "SMTP server address",
|
||||
"settings.notification.mail.smtp_port.placeholder": "SMTP server port, if not set, default is 25",
|
||||
"settings.notification.mail.username.placeholder": "username",
|
||||
"settings.notification.mail.password.placeholder": "password",
|
||||
"settings.notification.bark.serverUrl.placeholder": "Server URL, e.g. https://your-bark-server.com, leave it blank to use the bark default server",
|
||||
"settings.notification.bark.deviceKey.placeholder": "Device Key,e.g. XXXXXXXXXXXXXXXXXXXX",
|
||||
"settings.notification.push_test_message": "Send test notification",
|
||||
"settings.notification.push_test_message.succeeded.message": "Send test notification successfully",
|
||||
"settings.notification.push_test_message.failed.message": "Send test notification failed",
|
||||
"settings.notification.email.smtp_host.label": "SMTP Host",
|
||||
"settings.notification.email.smtp_host.placeholder": "Please enter SMTP host",
|
||||
"settings.notification.email.smtp_port.label": "SMTP Port",
|
||||
"settings.notification.email.smtp_port.placeholder": "Please enter SMTP port",
|
||||
"settings.notification.email.smtp_tls.label": "Use TLS/SSL",
|
||||
"settings.notification.email.username.label": "Username",
|
||||
"settings.notification.email.username.placeholder": "please enter username",
|
||||
"settings.notification.email.password.label": "Password",
|
||||
"settings.notification.email.password.placeholder": "please enter password",
|
||||
"settings.notification.email.sender_address.label": "Sender Email Address",
|
||||
"settings.notification.email.sender_address.placeholder": "Please enter sender email address",
|
||||
"settings.notification.email.receiver_address.label": "Receiver Email Address",
|
||||
"settings.notification.email.receiver_address.placeholder": "Please enter receiver email address",
|
||||
"settings.notification.webhook.url.label": "Webhook URL",
|
||||
"settings.notification.webhook.url.placeholder": "Please enter Webhook URL",
|
||||
"settings.notification.dingtalk.access_token.label": "AccessToken",
|
||||
"settings.notification.dingtalk.access_token.placeholder": "Please enter access token",
|
||||
"settings.notification.dingtalk.secret.label": "Secret",
|
||||
"settings.notification.dingtalk.secret.placeholder": "Please enter secret",
|
||||
"settings.notification.lark.webhook_url.label": "Webhook URL",
|
||||
"settings.notification.lark.webhook_url.placeholder": "Please enter Webhook URL",
|
||||
"settings.notification.telegram.api_token.label": "API Token",
|
||||
"settings.notification.telegram.api_token.placeholder": "Please enter API token",
|
||||
"settings.notification.telegram.chat_id.label": "Chat ID",
|
||||
"settings.notification.telegram.chat_id.placeholder": "Please enter Telegram chat ID",
|
||||
"settings.notification.serverchan.url.label": "Server URL",
|
||||
"settings.notification.serverchan.url.placeholder": "Please enter server URL (e.g. https://sctapi.ftqq.com/*****.send)",
|
||||
"settings.notification.bark.server_url.label": "Server URL",
|
||||
"settings.notification.bark.server_url.placeholder": "Please enter server URL (e.g. https://your-bark-server.com. Leave it blank to use the bark default server)",
|
||||
"settings.notification.bark.device_key.label": "Device Key",
|
||||
"settings.notification.bark.device_key.placeholder": "Please enter device key",
|
||||
|
||||
"settings.ca.tab": "Certificate Authority",
|
||||
"settings.ca.provider.errmsg.empty": "Please select a Certificate Authority",
|
||||
|
||||
@@ -83,12 +83,12 @@
|
||||
"common.provider.local": "本地部署",
|
||||
"common.provider.ssh": "SSH 部署",
|
||||
"common.provider.webhook": "Webhook",
|
||||
"common.provider.serverchan": "Server酱",
|
||||
"common.provider.kubernetes": "Kubernetes",
|
||||
"common.provider.kubernetes.secret": "Kubernetes - Secret",
|
||||
"common.provider.email": "电子邮件",
|
||||
"common.provider.dingtalk": "钉钉",
|
||||
"common.provider.telegram": "Telegram",
|
||||
"common.provider.lark": "飞书",
|
||||
"common.provider.mail": "电子邮件",
|
||||
"common.provider.telegram": "Telegram",
|
||||
"common.provider.serverchan": "Server酱",
|
||||
"common.provider.bark": "Bark"
|
||||
}
|
||||
|
||||
@@ -30,20 +30,40 @@
|
||||
"settings.notification.config.enable": "是否启用",
|
||||
"settings.notification.config.saved.message": "配置保存成功",
|
||||
"settings.notification.config.failed.message": "配置保存失败",
|
||||
"settings.notification.config.push.test.message": "推送测试消息",
|
||||
"settings.notification.config.push.test.message.failed.message": "推送测试消息失败",
|
||||
"settings.notification.config.push.test.message.success.message": "推送测试消息成功",
|
||||
"settings.notification.dingtalk.secret.placeholder": "加签的签名",
|
||||
"settings.notification.url.errmsg.invalid": "URL 格式不正确",
|
||||
"settings.notification.serverchan.url.placeholder": "Url, 形如: https://sctapi.ftqq.com/****************.send",
|
||||
"settings.notification.mail.sender_address.placeholder": "发送邮箱地址",
|
||||
"settings.notification.mail.receiver_address.placeholder": "接收邮箱地址",
|
||||
"settings.notification.mail.smtp_host.placeholder": "SMTP服务器地址",
|
||||
"settings.notification.mail.smtp_port.placeholder": "SMTP服务器端口, 如果未设置, 默认为25",
|
||||
"settings.notification.mail.username.placeholder": "用于登录到邮件服务器的用户名",
|
||||
"settings.notification.mail.password.placeholder": "用于登录到邮件服务器的密码",
|
||||
"settings.notification.bark.serverUrl.placeholder": "服务器URL,形如: https://your-bark-server.com, 留空则使用 Bark 默认服务器",
|
||||
"settings.notification.bark.deviceKey.placeholder": "设备密钥,形如: XXXXXXXXXXXXXXXXXXXX",
|
||||
"settings.notification.push_test_message": "推送测试消息",
|
||||
"settings.notification.push_test_message.failed.message": "推送测试消息失败",
|
||||
"settings.notification.push_test_message.succeeded.message": "推送测试消息成功",
|
||||
"settings.notification.email.smtp_host.label": "SMTP 服务器地址",
|
||||
"settings.notification.email.smtp_host.placeholder": "请输入 SMTP 服务器地址",
|
||||
"settings.notification.email.smtp_port.label": "SMTP 服务器端口",
|
||||
"settings.notification.email.smtp_port.placeholder": "请输入 SMTP 服务器端口",
|
||||
"settings.notification.email.smtp_tls.label": "TLS/SSL 连接",
|
||||
"settings.notification.email.username.label": "用户名",
|
||||
"settings.notification.email.username.placeholder": "请输入用户名",
|
||||
"settings.notification.email.password.label": "密码",
|
||||
"settings.notification.email.password.placeholder": "请输入密码",
|
||||
"settings.notification.email.sender_address.label": "发送邮箱地址",
|
||||
"settings.notification.email.sender_address.placeholder": "请输入发送邮箱地址",
|
||||
"settings.notification.email.receiver_address.label": "接收邮箱地址",
|
||||
"settings.notification.email.receiver_address.placeholder": "请输入接收邮箱地址",
|
||||
"settings.notification.webhook.url.label": "Webhook 回调地址",
|
||||
"settings.notification.webhook.url.placeholder": "请输入 Webhook 回调地址",
|
||||
"settings.notification.dingtalk.access_token.label": "AccessToken",
|
||||
"settings.notification.dingtalk.access_token.placeholder": "请输入 AccessToken",
|
||||
"settings.notification.dingtalk.secret.label": "签名密钥",
|
||||
"settings.notification.dingtalk.secret.placeholder": "请输入签名密钥",
|
||||
"settings.notification.lark.webhook_url.label": "Webhook URL",
|
||||
"settings.notification.lark.webhook_url.placeholder": "请输入 Webhook URL",
|
||||
"settings.notification.telegram.api_token.label": "API Token",
|
||||
"settings.notification.telegram.api_token.placeholder": "请输入 API token",
|
||||
"settings.notification.telegram.chat_id.label": "会话 ID",
|
||||
"settings.notification.telegram.chat_id.placeholder": "请输入 Telegram 会话 ID",
|
||||
"settings.notification.serverchan.url.label": "服务器 URL",
|
||||
"settings.notification.serverchan.url.placeholder": "请输入服务器 URL(形如: https://sctapi.ftqq.com/*****.send)",
|
||||
"settings.notification.bark.server_url.label": "服务器 URL",
|
||||
"settings.notification.bark.server_url.placeholder": "请输入服务器 URL(形如: https://your-bark-server.com;留空则使用 Bark 默认服务器)",
|
||||
"settings.notification.bark.device_key.label": "设备密钥",
|
||||
"settings.notification.bark.device_key.placeholder": "请输入设备密钥",
|
||||
|
||||
"settings.ca.tab": "证书颁发机构(CA)",
|
||||
"settings.ca.provider.errmsg.empty": "请选择证书分发机构",
|
||||
|
||||
@@ -7,7 +7,7 @@ import NotifyTemplate from "@/components/notify/NotifyTemplate";
|
||||
import Telegram from "@/components/notify/Telegram";
|
||||
import Webhook from "@/components/notify/Webhook";
|
||||
import ServerChan from "@/components/notify/ServerChan";
|
||||
import Mail from "@/components/notify/Mail";
|
||||
import Email from "@/components/notify/Email";
|
||||
import Bark from "@/components/notify/Bark";
|
||||
import { NotifyProvider } from "@/providers/notify";
|
||||
|
||||
@@ -27,51 +27,52 @@ const Notify = () => {
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-md p-5 mt-7 shadow-lg">
|
||||
<Accordion type={"single"} className="dark:text-stone-200">
|
||||
<AccordionItem value="item-2" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.dingtalk")}</AccordionTrigger>
|
||||
<AccordionItem value="item-email" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.email")}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<DingTalk />
|
||||
<Email />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-3" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.lark")}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Lark />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-4" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.telegram")}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Telegram />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-5" className="dark:border-stone-200">
|
||||
<AccordionItem value="item-webhook" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.webhook")}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Webhook />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-6" className="dark:border-stone-200">
|
||||
<AccordionItem value="item-dingtalk" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.dingtalk")}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<DingTalk />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-lark" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.lark")}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Lark />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-telegram" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.telegram")}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Telegram />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-serverchan" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.serverchan")}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<ServerChan />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-7" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.mail")}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Mail />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-8" className="dark:border-stone-200">
|
||||
<AccordionItem value="item-bark" className="dark:border-stone-200">
|
||||
<AccordionTrigger>{t("common.provider.bark")}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Bark />
|
||||
|
||||
102
ui/src/pages/workflow/WorkflowDetail.tsx
Normal file
102
ui/src/pages/workflow/WorkflowDetail.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import Show from "@/components/Show";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import End from "@/components/workflow/End";
|
||||
import NodeRender from "@/components/workflow/NodeRender";
|
||||
|
||||
import WorkflowProvider from "@/components/workflow/WorkflowProvider";
|
||||
import { WorkflowNode } from "@/domain/workflow";
|
||||
import { useWorkflowStore, WorkflowState } from "@/providers/workflow";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { useShallow } from "zustand/shallow";
|
||||
|
||||
const selectState = (state: WorkflowState) => ({
|
||||
workflow: state.workflow,
|
||||
init: state.init,
|
||||
switchEnable: state.switchEnable,
|
||||
save: state.save,
|
||||
});
|
||||
|
||||
const WorkflowDetail = () => {
|
||||
// 3. 使用正确的选择器和 shallow 比较
|
||||
const { workflow, init, switchEnable, save } = useWorkflowStore(useShallow(selectState));
|
||||
|
||||
// 从 url 中获取 workflowId
|
||||
const [searchParams] = useSearchParams();
|
||||
const id = searchParams.get("id");
|
||||
|
||||
useEffect(() => {
|
||||
console.log(id);
|
||||
init(id ?? "");
|
||||
}, [id]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const elements = useMemo(() => {
|
||||
let current = workflow.draft as WorkflowNode;
|
||||
|
||||
const elements: JSX.Element[] = [];
|
||||
|
||||
while (current) {
|
||||
// 处理普通节点
|
||||
elements.push(<NodeRender data={current} key={current.id} />);
|
||||
current = current.next as WorkflowNode;
|
||||
}
|
||||
|
||||
elements.push(<End key="workflow-end" />);
|
||||
|
||||
return elements;
|
||||
}, [workflow]);
|
||||
|
||||
const handleBackClick = () => {
|
||||
navigate("/workflow");
|
||||
};
|
||||
|
||||
const handleEnableChange = () => {
|
||||
switchEnable();
|
||||
};
|
||||
|
||||
const handleWorkflowSaveClick = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowProvider>
|
||||
<ScrollArea className="h-[100vh] w-full relative bg-background">
|
||||
<div className="h-16 sticky top-0 left-0 z-20 shadow-md bg-muted/40 flex justify-between items-center">
|
||||
<div className="px-5 text-stone-700 dark:text-stone-200 flex items-center space-x-2">
|
||||
<ArrowLeft className="cursor-pointer" onClick={handleBackClick} />
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="">工作流</div>
|
||||
<div className="text-sm text-muted-foreground">工作流详情</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 flex items-center space-x-3">
|
||||
<Show when={!!workflow.enabled}>
|
||||
<Show when={!!workflow.hasDraft} fallback={<Button variant={"secondary"}>立即执行</Button>}>
|
||||
<Button variant={"secondary"} onClick={handleWorkflowSaveClick}>
|
||||
保存变更
|
||||
</Button>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<Switch className="dark:data-[state=unchecked]:bg-stone-400" checked={workflow.enabled ?? false} onCheckedChange={handleEnableChange} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className=" flex flex-col items-center mt-8">{elements}</div>
|
||||
|
||||
<ScrollBar orientation="vertical" />
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</WorkflowProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowDetail;
|
||||
@@ -1,50 +1,21 @@
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import End from "@/components/workflow/End";
|
||||
import NodeRender from "@/components/workflow/NodeRender";
|
||||
|
||||
import WorkflowProvider from "@/components/workflow/WorkflowProvider";
|
||||
import { WorkflowNode } from "@/domain/workflow";
|
||||
import { useWorkflowStore, WorkflowState } from "@/providers/workflow";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useShallow } from "zustand/shallow";
|
||||
|
||||
const selectState = (state: WorkflowState) => ({
|
||||
root: state.root,
|
||||
});
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Workflow = () => {
|
||||
// 3. 使用正确的选择器和 shallow 比较
|
||||
const { root } = useWorkflowStore(useShallow(selectState));
|
||||
|
||||
const elements = useMemo(() => {
|
||||
let current = root;
|
||||
|
||||
const elements: JSX.Element[] = [];
|
||||
|
||||
while (current) {
|
||||
// 处理普通节点
|
||||
elements.push(<NodeRender data={current} key={current.id} />);
|
||||
current = current.next as WorkflowNode;
|
||||
}
|
||||
|
||||
elements.push(<End key="workflow-end" />);
|
||||
|
||||
return elements;
|
||||
}, [root]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const handleCreateClick = () => {
|
||||
navigate("/workflow/detail");
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<WorkflowProvider>
|
||||
<ScrollArea className="h-[100vh] w-full relative bg-background">
|
||||
<div className="h-16 sticky top-0 left-0 z-20 shadow-md bg-muted/40"></div>
|
||||
|
||||
<div className=" flex flex-col items-center mt-8">{elements}</div>
|
||||
|
||||
<ScrollBar orientation="vertical" />
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</WorkflowProvider>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-muted-foreground">工作流</div>
|
||||
<Button onClick={handleCreateClick}>
|
||||
<Plus size={16} />
|
||||
新建工作流
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,99 +2,183 @@ import {
|
||||
addBranch,
|
||||
addNode,
|
||||
getWorkflowOutputBeforeId,
|
||||
initWorkflow,
|
||||
removeBranch,
|
||||
removeNode,
|
||||
updateNode,
|
||||
Workflow,
|
||||
WorkflowBranchNode,
|
||||
WorkflowNode,
|
||||
WorkflowNodeType,
|
||||
} from "@/domain/workflow";
|
||||
import { save, get as getWrokflow } from "@/repository/workflow";
|
||||
import { create } from "zustand";
|
||||
|
||||
export type WorkflowState = {
|
||||
root: WorkflowNode;
|
||||
workflow: Workflow;
|
||||
initialized: boolean;
|
||||
updateNode: (node: WorkflowNode) => void;
|
||||
addNode: (node: WorkflowNode, preId: string) => void;
|
||||
addBranch: (branchId: string) => void;
|
||||
removeNode: (nodeId: string) => void;
|
||||
removeBranch: (branchId: string, index: number) => void;
|
||||
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
|
||||
switchEnable(): void;
|
||||
save(): void;
|
||||
init(id?: string): void;
|
||||
};
|
||||
|
||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
root: {
|
||||
id: "1",
|
||||
name: "开始",
|
||||
workflow: {
|
||||
id: "root",
|
||||
name: "placeholder",
|
||||
type: WorkflowNodeType.Start,
|
||||
next: {
|
||||
id: "2",
|
||||
name: "分支",
|
||||
type: WorkflowNodeType.Branch,
|
||||
branches: [
|
||||
{
|
||||
id: "3",
|
||||
name: "条件1",
|
||||
type: WorkflowNodeType.Condition,
|
||||
next: {
|
||||
id: "4",
|
||||
name: "条件2",
|
||||
type: WorkflowNodeType.Apply,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "条件2",
|
||||
type: WorkflowNodeType.Condition,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
updateNode: (node: WorkflowNode | WorkflowBranchNode) => {
|
||||
initialized: false,
|
||||
init: async (id?: string) => {
|
||||
let data = {
|
||||
name: "placeholder",
|
||||
type: "auto",
|
||||
};
|
||||
|
||||
if (!id) {
|
||||
data = initWorkflow();
|
||||
} else {
|
||||
data = await getWrokflow(id);
|
||||
}
|
||||
|
||||
set({
|
||||
workflow: data,
|
||||
initialized: true,
|
||||
});
|
||||
},
|
||||
switchEnable: async () => {
|
||||
const resp = await save({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
content: get().workflow.draft as WorkflowNode,
|
||||
enabled: !get().workflow.enabled,
|
||||
hasDraft: false,
|
||||
});
|
||||
set((state: WorkflowState) => {
|
||||
const newRoot = updateNode(state.root, node);
|
||||
console.log(newRoot);
|
||||
return {
|
||||
root: newRoot,
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
id: resp.id,
|
||||
content: resp.content,
|
||||
enabled: resp.enabled,
|
||||
hasDraft: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
addNode: (node: WorkflowNode | WorkflowBranchNode, preId: string) =>
|
||||
save: async () => {
|
||||
const resp = await save({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
content: get().workflow.draft as WorkflowNode,
|
||||
hasDraft: false,
|
||||
});
|
||||
set((state: WorkflowState) => {
|
||||
const newRoot = addNode(state.root, preId, node);
|
||||
|
||||
return {
|
||||
root: newRoot,
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
id: resp.id,
|
||||
content: resp.content,
|
||||
hasDraft: false,
|
||||
},
|
||||
};
|
||||
}),
|
||||
addBranch: (branchId: string) =>
|
||||
});
|
||||
},
|
||||
updateNode: async (node: WorkflowNode | WorkflowBranchNode) => {
|
||||
const newRoot = updateNode(get().workflow.draft as WorkflowNode, node);
|
||||
const resp = await save({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
draft: newRoot,
|
||||
hasDraft: true,
|
||||
});
|
||||
set((state: WorkflowState) => {
|
||||
const newRoot = addBranch(state.root, branchId);
|
||||
|
||||
return {
|
||||
root: newRoot,
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
draft: newRoot,
|
||||
id: resp.id,
|
||||
hasDraft: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
removeBranch: (branchId: string, index: number) =>
|
||||
});
|
||||
},
|
||||
addNode: async (node: WorkflowNode | WorkflowBranchNode, preId: string) => {
|
||||
const newRoot = addNode(get().workflow.draft as WorkflowNode, preId, node);
|
||||
const resp = await save({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
draft: newRoot,
|
||||
hasDraft: true,
|
||||
});
|
||||
set((state: WorkflowState) => {
|
||||
const newRoot = removeBranch(state.root, branchId, index);
|
||||
|
||||
return {
|
||||
root: newRoot,
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
draft: newRoot,
|
||||
id: resp.id,
|
||||
hasDraft: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
removeNode: (nodeId: string) =>
|
||||
});
|
||||
},
|
||||
addBranch: async (branchId: string) => {
|
||||
const newRoot = addBranch(get().workflow.draft as WorkflowNode, branchId);
|
||||
const resp = await save({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
draft: newRoot,
|
||||
hasDraft: true,
|
||||
});
|
||||
set((state: WorkflowState) => {
|
||||
const newRoot = removeNode(state.root, nodeId);
|
||||
|
||||
return {
|
||||
root: newRoot,
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
draft: newRoot,
|
||||
id: resp.id,
|
||||
hasDraft: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
});
|
||||
},
|
||||
removeBranch: async (branchId: string, index: number) => {
|
||||
const newRoot = removeBranch(get().workflow.draft as WorkflowNode, branchId, index);
|
||||
const resp = await save({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
draft: newRoot,
|
||||
hasDraft: true,
|
||||
});
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
draft: newRoot,
|
||||
id: resp.id,
|
||||
hasDraft: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
removeNode: async (nodeId: string) => {
|
||||
const newRoot = removeNode(get().workflow.draft as WorkflowNode, nodeId);
|
||||
const resp = await save({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
draft: newRoot,
|
||||
hasDraft: true,
|
||||
});
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
workflow: {
|
||||
...state.workflow,
|
||||
draft: newRoot,
|
||||
id: resp.id,
|
||||
hasDraft: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
getWorkflowOuptutBeforeId: (id: string, type: string) => {
|
||||
return getWorkflowOutputBeforeId(get().root, id, type);
|
||||
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
16
ui/src/repository/workflow.ts
Normal file
16
ui/src/repository/workflow.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Workflow, WorkflowNode } from "@/domain/workflow";
|
||||
import { getPb } from "./api";
|
||||
|
||||
export const get = async (id: string) => {
|
||||
const response = await getPb().collection("workflow").getOne<Workflow>(id);
|
||||
return response;
|
||||
};
|
||||
|
||||
export const save = async (data: Record<string, string | boolean | WorkflowNode>) => {
|
||||
if (data.id) {
|
||||
return await getPb()
|
||||
.collection("workflow")
|
||||
.update<Workflow>(data.id as string, data);
|
||||
}
|
||||
return await getPb().collection("workflow").create<Workflow>(data);
|
||||
};
|
||||
@@ -14,6 +14,7 @@ import Account from "./pages/setting/Account";
|
||||
import Notify from "./pages/setting/Notify";
|
||||
import SSLProvider from "./pages/setting/SSLProvider";
|
||||
import Workflow from "./pages/workflow";
|
||||
import WorkflowDetail from "./pages/workflow/WorkflowDetail";
|
||||
|
||||
export const router = createHashRouter([
|
||||
{
|
||||
@@ -40,6 +41,10 @@ export const router = createHashRouter([
|
||||
path: "/history",
|
||||
element: <History />,
|
||||
},
|
||||
{
|
||||
path: "/workflow",
|
||||
element: <Workflow />,
|
||||
},
|
||||
{
|
||||
path: "/setting",
|
||||
element: <SettingLayout />,
|
||||
@@ -75,7 +80,7 @@ export const router = createHashRouter([
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
element: <Workflow />,
|
||||
path: "/workflow/detail",
|
||||
element: <WorkflowDetail />,
|
||||
},
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user