diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx index 589db1f2..caff1447 100644 --- a/ui/src/components/certimate/DeployEditDialog.tsx +++ b/ui/src/components/certimate/DeployEditDialog.tsx @@ -27,7 +27,7 @@ import DeployToLocal from "./DeployToLocal"; import DeployToSSH from "./DeployToSSH"; import DeployToWebhook from "./DeployToWebhook"; import DeployToKubernetesSecret from "./DeployToKubernetesSecret"; -import DeployToVolcengineLive from "./DeployToVolcengineLive" +import DeployToVolcengineLive from "./DeployToVolcengineLive"; import { deployTargetsMap, type DeployConfig } from "@/domain/domain"; import { accessProvidersMap } from "@/domain/access"; import { useConfigContext } from "@/providers/config"; diff --git a/ui/src/components/notify/Mail.tsx b/ui/src/components/notify/Mail.tsx new file mode 100644 index 00000000..79ea0f17 --- /dev/null +++ b/ui/src/components/notify/Mail.tsx @@ -0,0 +1,327 @@ +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 MailSetting = { + id: string; + name: string; + data: NotifyChannelEmail; +}; + +const Mail = () => { + const { config, setChannels } = useNotifyContext(); + const { t } = useTranslation(); + + const [changed, setChanged] = useState(false); + + const [mail, setmail] = useState({ + id: config.id ?? "", + name: "notifyChannels", + data: { + senderAddress: "", + receiverAddresses: "", + smtpHostAddr: "", + smtpHostPort: "25", + username: "", + password: "", + enabled: false, + }, + }); + + const [originMail, setoriginMail] = useState({ + id: config.id ?? "", + name: "notifyChannels", + data: { + senderAddress: "", + receiverAddresses: "", + smtpHostAddr: "", + smtpHostPort: "25", + username: "", + password: "", + enabled: false, + }, + }); + + useEffect(() => { + setChanged(false); + }, [config]); + + useEffect(() => { + const data = getDetailMail(); + setoriginMail({ + id: config.id ?? "", + name: "mail", + data, + }); + }, [config]); + + useEffect(() => { + const data = getDetailMail(); + setmail({ + id: config.id ?? "", + name: "mail", + data, + }); + }, [config]); + + const { toast } = useToast(); + + const getDetailMail = () => { + const df: NotifyChannelMail = { + senderAddress: "", + receiverAddresses: "", + smtpHostAddr: "", + smtpHostPort: "25", + username: "", + password: "", + enabled: false, + }; + if (!config.content) { + return df; + } + const chanels = config.content as NotifyChannels; + if (!chanels.mail) { + return df; + } + + return chanels.mail as NotifyChannelMail; + }; + + const checkChanged = (data: NotifyChannelMail) => { + if ( + data.senderAddress !== originMail.data.senderAddress || + data.receiverAddresses !== originMail.data.receiverAddresses || + data.smtpHostAddr !== originMail.data.smtpHostAddr || + data.smtpHostPort !== originMail.data.smtpHostPort || + data.username !== originMail.data.username || + data.password !== originMail.data.password + ) { + setChanged(true); + } else { + setChanged(false); + } + }; + + const handleSaveClick = async () => { + try { + const resp = await update({ + ...config, + name: "notifyChannels", + content: { + ...config.content, + mail: { + ...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 handlePushTestClick = async () => { + try { + await notifyTest("mail"); + + toast({ + title: t("settings.notification.config.push.test.message.success.message"), + description: t("settings.notification.config.push.test.message.success.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}`, + variant: "destructive", + }); + } + }; + + 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, + mail: { + ...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 ( +
+ { + const newData = { + ...mail, + data: { + ...mail.data, + senderAddress: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + { + const newData = { + ...mail, + data: { + ...mail.data, + receiverAddresses: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + { + const newData = { + ...mail, + data: { + ...mail.data, + smtpHostAddr: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + { + const newData = { + ...mail, + data: { + ...mail.data, + smtpHostPort: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + { + const newData = { + ...mail, + data: { + ...mail.data, + username: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + { + const newData = { + ...mail, + data: { + ...mail.data, + password: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> +
+ + +
+ +
+ + + + + + + +
+
+ ); +}; + +export default Mail; + diff --git a/ui/src/components/workflow/CustomAlertDialog.tsx b/ui/src/components/workflow/CustomAlertDialog.tsx index 32b28e49..74c79721 100644 --- a/ui/src/components/workflow/CustomAlertDialog.tsx +++ b/ui/src/components/workflow/CustomAlertDialog.tsx @@ -24,12 +24,12 @@ const CustomAlertDialog = ({ open, title, description, confirm, onOpenChange }: return ( - + {title} {description} - {t("common.cancel")} + {t("common.cancel")} { confirm && confirm(); diff --git a/ui/src/components/workflow/DataTable.tsx b/ui/src/components/workflow/DataTable.tsx index c789e520..3e4194db 100644 --- a/ui/src/components/workflow/DataTable.tsx +++ b/ui/src/components/workflow/DataTable.tsx @@ -3,20 +3,23 @@ import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, Paginati import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Button } from "../ui/button"; import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; -interface DataTableProps { +interface DataTableProps { columns: ColumnDef[]; data: TData[]; pageCount: number; onPageChange?: (pageIndex: number, pageSize?: number) => Promise; } -export function DataTable({ columns, data, onPageChange, pageCount }: DataTableProps) { +export function DataTable({ columns, data, onPageChange, pageCount }: DataTableProps) { const [{ pageIndex, pageSize }, setPagination] = useState({ pageIndex: 0, pageSize: 10, }); + const navigate = useNavigate(); + const pagination = { pageIndex, pageSize, @@ -39,13 +42,17 @@ export function DataTable({ columns, data, onPageChange, pageCoun onPageChange?.(pageIndex, pageSize); }, [pageIndex]); + const handleRowClick = (id: string) => { + navigate(`/workflow/detail?id=${id}`); + }; + return ( <>
{table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => { return {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}; })} @@ -55,7 +62,15 @@ export function DataTable({ columns, data, onPageChange, pageCoun {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - + { + e.stopPropagation(); + handleRowClick(row.original.id); + }} + > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} @@ -84,4 +99,3 @@ export function DataTable({ columns, data, onPageChange, pageCoun ); } - diff --git a/ui/src/components/workflow/WorkflowBaseInfoEditDialog.tsx b/ui/src/components/workflow/WorkflowBaseInfoEditDialog.tsx index d3881c47..228d87ef 100644 --- a/ui/src/components/workflow/WorkflowBaseInfoEditDialog.tsx +++ b/ui/src/components/workflow/WorkflowBaseInfoEditDialog.tsx @@ -8,7 +8,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from " import { Input } from "../ui/input"; import { Button } from "../ui/button"; import { useTranslation } from "react-i18next"; -import { useState } from "react"; +import { memo, useState } from "react"; type WorkflowNameEditDialogProps = { trigger: React.ReactNode; @@ -28,10 +28,6 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) => const form = useForm>({ resolver: zodResolver(formSchema), - defaultValues: { - name: workflow.name, - description: workflow.description, - }, }); const { t } = useTranslation(); @@ -72,7 +68,14 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) => 名称 - + { + form.setValue("name", e.target.value); + }} + /> @@ -87,7 +90,14 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) => 说明 - + { + form.setValue("description", e.target.value); + }} + /> @@ -107,4 +117,4 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) => ); }; -export default WorkflowNameBaseInfoDialog; +export default memo(WorkflowNameBaseInfoDialog); diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 109cb476..4c0848e1 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -4,7 +4,7 @@ import i18n from "@/i18n"; import { deployTargets, KVType } from "./domain"; export type Workflow = { - id?: string; + id: string; name: string; description?: string; type: string; @@ -125,6 +125,7 @@ export const initWorkflow = (): Workflow => { root.next = newWorkflowNode(WorkflowNodeType.Notify, {}); return { + id: "", name: i18n.t("workflow.default.name"), type: "auto", crontab: "0 0 * * *", diff --git a/ui/src/pages/workflow/index.tsx b/ui/src/pages/workflow/index.tsx index f96da3b3..26519d4b 100644 --- a/ui/src/pages/workflow/index.tsx +++ b/ui/src/pages/workflow/index.tsx @@ -90,6 +90,9 @@ const Workflow = () => { onCheckedChange={() => { handleCheckedChange(row.original.id ?? ""); }} + onClick={(e) => { + e.stopPropagation(); + }} /> ); @@ -127,7 +130,8 @@ const Workflow = () => { 操作 { + onClick={(e) => { + e.stopPropagation(); navigate(`/workflow/detail?id=${workflow.id}`); }} > @@ -135,7 +139,8 @@ const Workflow = () => { { + onClick={(e) => { + e.stopPropagation(); handleDeleteClick(workflow.id ?? ""); }} > diff --git a/ui/src/providers/workflow/index.ts b/ui/src/providers/workflow/index.ts index 6e64356b..84d6bc60 100644 --- a/ui/src/providers/workflow/index.ts +++ b/ui/src/providers/workflow/index.ts @@ -31,20 +31,20 @@ export type WorkflowState = { export const useWorkflowStore = create((set, get) => ({ workflow: { - id: "root", + id: "", name: "placeholder", type: WorkflowNodeType.Start, }, initialized: false, init: async (id?: string) => { let data = { + id: "", name: "placeholder", type: "auto", }; if (!id) { data = initWorkflow(); - data = await save(data); } else { data = await getWrokflow(id); } @@ -55,11 +55,15 @@ export const useWorkflowStore = create((set, get) => ({ }); }, setBaseInfo: async (name: string, description: string) => { - const resp = await save({ + const data: Record = { id: (get().workflow.id as string) ?? "", name: name, description: description, - }); + }; + if (!data.id) { + data.draft = get().workflow.draft as WorkflowNode; + } + const resp = await save(data); set((state: WorkflowState) => { return { workflow: { @@ -201,3 +205,4 @@ export const useWorkflowStore = create((set, get) => ({ return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type); }, })); +