diff --git a/internal/certificate/service.go b/internal/certificate/service.go new file mode 100644 index 00000000..48c07928 --- /dev/null +++ b/internal/certificate/service.go @@ -0,0 +1,102 @@ +package certificate + +import ( + "context" + "encoding/json" + "strconv" + "strings" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/notify" + "github.com/usual2970/certimate/internal/repository" + "github.com/usual2970/certimate/internal/utils/app" +) + +const ( + defaultExpireSubject = "您有 {COUNT} 张证书即将过期" + defaultExpireMessage = "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!" +) + +type CertificateRepository interface { + GetExpireSoon(ctx context.Context) ([]domain.Certificate, error) +} + +type certificateService struct { + repo CertificateRepository +} + +func NewCertificateService(repo CertificateRepository) *certificateService { + return &certificateService{ + repo: repo, + } +} + +func (s *certificateService) InitSchedule(ctx context.Context) error { + scheduler := app.GetScheduler() + + err := scheduler.Add("certificate", "0 0 * * *", func() { + certs, err := s.repo.GetExpireSoon(context.Background()) + if err != nil { + app.GetApp().Logger().Error("failed to get expire soon certificate", "err", err) + return + } + msg := buildMsg(certs) + if err := notify.SendToAllChannels(msg.Subject, msg.Message); err != nil { + app.GetApp().Logger().Error("failed to send expire soon certificate", "err", err) + } + }) + if err != nil { + app.GetApp().Logger().Error("failed to add schedule", "err", err) + return err + } + scheduler.Start() + app.GetApp().Logger().Info("certificate schedule started") + return nil +} + +func buildMsg(records []domain.Certificate) *domain.NotifyMessage { + if len(records) == 0 { + return nil + } + + // 查询模板信息 + settingRepo := repository.NewSettingRepository() + setting, err := settingRepo.GetByName(context.Background(), "templates") + + subject := defaultExpireSubject + message := defaultExpireMessage + + if err == nil { + var templates *domain.NotifyTemplates + + json.Unmarshal([]byte(setting.Content), &templates) + + if templates != nil && len(templates.NotifyTemplates) > 0 { + subject = templates.NotifyTemplates[0].Title + message = templates.NotifyTemplates[0].Content + } + } + + // 替换变量 + count := len(records) + domains := make([]string, count) + + for i, record := range records { + domains[i] = record.SAN + } + + countStr := strconv.Itoa(count) + domainStr := strings.Join(domains, ";") + + subject = strings.ReplaceAll(subject, "{COUNT}", countStr) + subject = strings.ReplaceAll(subject, "{DOMAINS}", domainStr) + + message = strings.ReplaceAll(message, "{COUNT}", countStr) + message = strings.ReplaceAll(message, "{DOMAINS}", domainStr) + + // 返回消息 + return &domain.NotifyMessage{ + Subject: subject, + Message: message, + } +} diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index 894dd024..b5ee0e0e 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -6,16 +6,16 @@ var ValidityDuration = time.Hour * 24 * 10 type Certificate struct { Meta - SAN string `json:"san"` - Certificate string `json:"certificate"` - PrivateKey string `json:"privateKey"` - IssuerCertificate string `json:"issuerCertificate"` - CertUrl string `json:"certUrl"` - CertStableUrl string `json:"certStableUrl"` - Output string `json:"output"` - Workflow string `json:"workflow"` - ExpireAt time.Time `json:"ExpireAt"` - NodeId string `json:"nodeId"` + SAN string `json:"san" db:"san"` + Certificate string `json:"certificate" db:"certificate"` + PrivateKey string `json:"privateKey" db:"privateKey"` + IssuerCertificate string `json:"issuerCertificate" db:"issuerCertificate"` + CertUrl string `json:"certUrl" db:"certUrl"` + CertStableUrl string `json:"certStableUrl" db:"certStableUrl"` + Output string `json:"output" db:"output"` + Workflow string `json:"workflow" db:"workflow"` + ExpireAt time.Time `json:"ExpireAt" db:"expireAt"` + NodeId string `json:"nodeId" db:"nodeId"` } type MetaData struct { diff --git a/internal/domain/common.go b/internal/domain/common.go index 1b53b122..8ff8f221 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -3,7 +3,7 @@ package domain import "time" type Meta struct { - Id string `json:"id"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id string `json:"id" db:"id"` + Created time.Time `json:"created" db:"created"` + Updated time.Time `json:"updated" db:"updated"` } diff --git a/internal/domain/setting.go b/internal/domain/setting.go index ec36be8a..da368566 100644 --- a/internal/domain/setting.go +++ b/internal/domain/setting.go @@ -29,3 +29,17 @@ func (s *Setting) GetChannelContent(channel string) (map[string]any, error) { return v, nil } + +type NotifyTemplates struct { + NotifyTemplates []NotifyTemplate `json:"notifyTemplates"` +} + +type NotifyTemplate struct { + Title string `json:"title"` + Content string `json:"content"` +} + +type NotifyMessage struct { + Subject string + Message string +} diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 49487b2f..2190ba6d 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -15,11 +15,17 @@ const ( WorkflowNodeTypeCondition = "condition" ) +const ( + WorkflowTypeAuto = "auto" + WorkflowTypeManual = "manual" +) + type Workflow struct { Meta Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` + Crontab string `json:"crontab"` Content *WorkflowNode `json:"content"` Draft *WorkflowNode `json:"draft"` Enabled bool `json:"enabled"` diff --git a/internal/domains/init.go b/internal/domains/init.go deleted file mode 100644 index 6ca683a0..00000000 --- a/internal/domains/init.go +++ /dev/null @@ -1,38 +0,0 @@ -package domains - -import ( - "context" - - "github.com/usual2970/certimate/internal/notify" - "github.com/usual2970/certimate/internal/utils/app" -) - -func InitSchedule() { - // 查询所有启用的域名 - records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "enabled=true", "-id", 500, 0) - if err != nil { - app.GetApp().Logger().Error("查询所有启用的域名失败", "err", err) - return - } - - // 加入到定时任务 - for _, record := range records { - if err := app.GetScheduler().Add(record.Id, record.GetString("crontab"), func() { - if err := deploy(context.Background(), record); err != nil { - app.GetApp().Logger().Error("部署失败", "err", err) - return - } - }); err != nil { - app.GetApp().Logger().Error("加入到定时任务失败", "err", err) - } - } - - // 过期提醒 - app.GetScheduler().Add("expire", "0 0 * * *", func() { - notify.PushExpireMsg() - }) - - // 启动定时任务 - app.GetScheduler().Start() - app.GetApp().Logger().Info("定时任务启动成功", "total", app.GetScheduler().Total()) -} diff --git a/internal/notify/expire.go b/internal/notify/expire.go deleted file mode 100644 index 0f5251c9..00000000 --- a/internal/notify/expire.go +++ /dev/null @@ -1,96 +0,0 @@ -package notify - -import ( - "strconv" - "strings" - "time" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - - "github.com/usual2970/certimate/internal/utils/app" - "github.com/usual2970/certimate/internal/utils/xtime" -) - -const ( - defaultExpireSubject = "您有 {COUNT} 张证书即将过期" - defaultExpireMessage = "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!" -) - -func PushExpireMsg() { - // 查询即将过期的证书 - records, err := app.GetApp().Dao().FindRecordsByFilter("certificate", "expireAt<{:time}&&certUrl!=''", "-created", 500, 0, - dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 20)}) - if err != nil { - app.GetApp().Logger().Error("find expired domains by filter", "error", err) - return - } - - // 组装消息 - msg := buildMsg(records) - if msg == nil { - return - } - - // 发送通知 - if err := SendToAllChannels(msg.Subject, msg.Message); err != nil { - app.GetApp().Logger().Error("send expire msg", "error", err) - } -} - -type notifyTemplates struct { - NotifyTemplates []notifyTemplate `json:"notifyTemplates"` -} - -type notifyTemplate struct { - Title string `json:"title"` - Content string `json:"content"` -} - -type notifyMessage struct { - Subject string - Message string -} - -func buildMsg(records []*models.Record) *notifyMessage { - if len(records) == 0 { - return nil - } - - // 查询模板信息 - templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'") - subject := defaultExpireSubject - message := defaultExpireMessage - - if err == nil { - var templates *notifyTemplates - templateRecord.UnmarshalJSONField("content", templates) - if templates != nil && len(templates.NotifyTemplates) > 0 { - subject = templates.NotifyTemplates[0].Title - message = templates.NotifyTemplates[0].Content - } - } - - // 替换变量 - count := len(records) - domains := make([]string, count) - - for i, record := range records { - domains[i] = record.GetString("san") - } - - countStr := strconv.Itoa(count) - domainStr := strings.Join(domains, ";") - - subject = strings.ReplaceAll(subject, "{COUNT}", countStr) - subject = strings.ReplaceAll(subject, "{DOMAINS}", domainStr) - - message = strings.ReplaceAll(message, "{COUNT}", countStr) - message = strings.ReplaceAll(message, "{DOMAINS}", domainStr) - - // 返回消息 - return ¬ifyMessage{ - Subject: subject, - Message: message, - } -} diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go new file mode 100644 index 00000000..bff25a12 --- /dev/null +++ b/internal/repository/certificate.go @@ -0,0 +1,24 @@ +package repository + +import ( + "context" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/utils/app" +) + +type CertificateRepository struct{} + +func NewCertificateRepository() *CertificateRepository { + return &CertificateRepository{} +} + +func (c *CertificateRepository) GetExpireSoon(ctx context.Context) ([]domain.Certificate, error) { + rs := []domain.Certificate{} + if err := app.GetApp().Dao().DB(). + NewQuery("select * from certificate where expireAt > datetime('now') and expireAt < datetime('now', '+20 days')"). + All(&rs); err != nil { + return nil, err + } + return rs, nil +} diff --git a/internal/repository/workflow.go b/internal/repository/workflow.go index 00da85cf..5ab3f9f4 100644 --- a/internal/repository/workflow.go +++ b/internal/repository/workflow.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/models" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/utils/app" @@ -16,6 +17,26 @@ func NewWorkflowRepository() *WorkflowRepository { return &WorkflowRepository{} } +func (w *WorkflowRepository) ListEnabledAuto(ctx context.Context) ([]domain.Workflow, error) { + records, err := app.GetApp().Dao().FindRecordsByFilter( + "workflow", + "enabled={:enabled} && type={:type}", + "-created", 1000, 0, dbx.Params{"enabled": true, "type": domain.WorkflowTypeAuto}, + ) + if err != nil { + return nil, err + } + rs := make([]domain.Workflow, 0) + for _, record := range records { + workflow, err := record2Workflow(record) + if err != nil { + return nil, err + } + rs = append(rs, *workflow) + } + return rs, nil +} + func (w *WorkflowRepository) SaveRunLog(ctx context.Context, log *domain.WorkflowRunLog) error { collection, err := app.GetApp().Dao().FindCollectionByNameOrId("workflow_run_log") if err != nil { @@ -40,6 +61,10 @@ func (w *WorkflowRepository) Get(ctx context.Context, id string) (*domain.Workfl return nil, err } + return record2Workflow(record) +} + +func record2Workflow(record *models.Record) (*domain.Workflow, error) { content := &domain.WorkflowNode{} if err := record.UnmarshalJSONField("content", content); err != nil { return nil, err @@ -59,6 +84,7 @@ func (w *WorkflowRepository) Get(ctx context.Context, id string) (*domain.Workfl Name: record.GetString("name"), Description: record.GetString("description"), Type: record.GetString("type"), + Crontab: record.GetString("crontab"), Enabled: record.GetBool("enabled"), HasDraft: record.GetBool("hasDraft"), diff --git a/internal/scheduler/certificate.go b/internal/scheduler/certificate.go new file mode 100644 index 00000000..352ff0aa --- /dev/null +++ b/internal/scheduler/certificate.go @@ -0,0 +1,11 @@ +package scheduler + +import "context" + +type CertificateService interface { + InitSchedule(ctx context.Context) error +} + +func NewCertificateScheduler(service CertificateService) error { + return service.InitSchedule(context.Background()) +} diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go new file mode 100644 index 00000000..055beddb --- /dev/null +++ b/internal/scheduler/scheduler.go @@ -0,0 +1,19 @@ +package scheduler + +import ( + "github.com/usual2970/certimate/internal/certificate" + "github.com/usual2970/certimate/internal/repository" + "github.com/usual2970/certimate/internal/workflow" +) + +func Register() { + workflowRepo := repository.NewWorkflowRepository() + workflowSvc := workflow.NewWorkflowService(workflowRepo) + + certificateRepo := repository.NewCertificateRepository() + certificateSvc := certificate.NewCertificateService(certificateRepo) + + NewCertificateScheduler(certificateSvc) + + NewWorkflowScheduler(workflowSvc) +} diff --git a/internal/scheduler/workflow.go b/internal/scheduler/workflow.go new file mode 100644 index 00000000..679bb3aa --- /dev/null +++ b/internal/scheduler/workflow.go @@ -0,0 +1,11 @@ +package scheduler + +import "context" + +type WorkflowService interface { + InitSchedule(ctx context.Context) error +} + +func NewWorkflowScheduler(service WorkflowService) error { + return service.InitSchedule(context.Background()) +} diff --git a/internal/utils/app/schedule.go b/internal/utils/app/schedule.go index e376f04e..2361ccab 100644 --- a/internal/utils/app/schedule.go +++ b/internal/utils/app/schedule.go @@ -2,6 +2,7 @@ package app import ( "sync" + "time" "github.com/pocketbase/pocketbase/tools/cron" ) @@ -13,6 +14,10 @@ var scheduler *cron.Cron func GetScheduler() *cron.Cron { schedulerOnce.Do(func() { scheduler = cron.New() + location, err := time.LoadLocation("Asia/Shanghai") + if err == nil { + scheduler.SetTimezone(location) + } }) return scheduler diff --git a/internal/workflow/event.go b/internal/workflow/event.go new file mode 100644 index 00000000..04b53521 --- /dev/null +++ b/internal/workflow/event.go @@ -0,0 +1,71 @@ +package workflow + +import ( + "context" + "fmt" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/repository" + "github.com/usual2970/certimate/internal/utils/app" +) + +const tableName = "workflow" + +func AddEvent() error { + app := app.GetApp() + + app.OnRecordAfterCreateRequest(tableName).Add(func(e *core.RecordCreateEvent) error { + return update(e.HttpContext.Request().Context(), e.Record) + }) + + app.OnRecordAfterUpdateRequest(tableName).Add(func(e *core.RecordUpdateEvent) error { + return update(e.HttpContext.Request().Context(), e.Record) + }) + + app.OnRecordAfterDeleteRequest(tableName).Add(func(e *core.RecordDeleteEvent) error { + return delete(e.HttpContext.Request().Context(), e.Record) + }) + + return nil +} + +func delete(_ context.Context, record *models.Record) error { + id := record.Id + scheduler := app.GetScheduler() + scheduler.Remove(id) + scheduler.Start() + return nil +} + +func update(ctx context.Context, record *models.Record) error { + // 是不是自动 + // 是不是 enabled + + id := record.Id + enabled := record.GetBool("enabled") + executeMethod := record.GetString("type") + + scheduler := app.GetScheduler() + if !enabled || executeMethod == domain.WorkflowTypeManual { + scheduler.Remove(id) + scheduler.Start() + return nil + } + + err := scheduler.Add(id, record.GetString("crontab"), func() { + NewWorkflowService(repository.NewWorkflowRepository()).Run(ctx, &domain.WorkflowRunReq{ + Id: id, + }) + }) + if err != nil { + app.GetApp().Logger().Error("add cron job failed", "err", err) + return fmt.Errorf("add cron job failed: %w", err) + } + app.GetApp().Logger().Error("add cron job failed", "san", record.GetString("san")) + + scheduler.Start() + return nil +} diff --git a/internal/workflow/service.go b/internal/workflow/service.go index 629cd841..20510443 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -12,6 +12,7 @@ import ( type WorkflowRepository interface { Get(ctx context.Context, id string) (*domain.Workflow, error) SaveRunLog(ctx context.Context, log *domain.WorkflowRunLog) error + ListEnabledAuto(ctx context.Context) ([]domain.Workflow, error) } type WorkflowService struct { @@ -24,6 +25,29 @@ func NewWorkflowService(repo WorkflowRepository) *WorkflowService { } } +func (s *WorkflowService) InitSchedule(ctx context.Context) error { + // 查询所有的 enabled auto workflow + workflows, err := s.repo.ListEnabledAuto(ctx) + if err != nil { + return err + } + scheduler := app.GetScheduler() + for _, workflow := range workflows { + err := scheduler.Add(workflow.Id, workflow.Crontab, func() { + s.Run(ctx, &domain.WorkflowRunReq{ + Id: workflow.Id, + }) + }) + if err != nil { + app.GetApp().Logger().Error("failed to add schedule", "err", err) + return err + } + } + scheduler.Start() + app.GetApp().Logger().Info("workflow schedule started") + return nil +} + func (s *WorkflowService) Run(ctx context.Context, req *domain.WorkflowRunReq) error { // 查询 if req.Id == "" { diff --git a/main.go b/main.go index 39758d42..7aa35221 100644 --- a/main.go +++ b/main.go @@ -13,9 +13,10 @@ import ( _ "github.com/usual2970/certimate/migrations" - "github.com/usual2970/certimate/internal/domains" "github.com/usual2970/certimate/internal/routes" + "github.com/usual2970/certimate/internal/scheduler" "github.com/usual2970/certimate/internal/utils/app" + "github.com/usual2970/certimate/internal/workflow" "github.com/usual2970/certimate/ui" _ "time/tzdata" @@ -38,13 +39,13 @@ func main() { Automigrate: isGoRun, }) - domains.AddEvent() + workflow.AddEvent() app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - domains.InitSchedule() - routes.Register(e.Router) + scheduler.Register() + e.Router.GET( "/*", echo.StaticDirectoryHandler(ui.DistDirFS, false), diff --git a/ui/src/components/certimate/AccessGroupEdit.tsx b/ui/src/components/certimate/AccessGroupEdit.tsx deleted file mode 100644 index 68d881a3..00000000 --- a/ui/src/components/certimate/AccessGroupEdit.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { ClientResponseError } from "pocketbase"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { PbErrorData } from "@/domain/base"; -import { update } from "@/repository/access_group"; -import { useConfigContext } from "@/providers/config"; - -type AccessGroupEditProps = { - className?: string; - trigger: React.ReactNode; -}; - -const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => { - const { reloadAccessGroups } = useConfigContext(); - - const [open, setOpen] = useState(false); - const { t } = useTranslation(); - - const formSchema = z.object({ - name: z - .string() - .min(1, "access.group.form.name.errmsg.empty") - .max(64, t("common.errmsg.string_max", { max: 64 })), - }); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - name: "", - }, - }); - - const onSubmit = async (data: z.infer) => { - try { - await update({ - name: data.name, - }); - - // 更新本地状态 - reloadAccessGroups(); - - setOpen(false); - } catch (e) { - const err = e as ClientResponseError; - - Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => { - form.setError(key as keyof z.infer, { - type: "manual", - message: value.message, - }); - }); - } - }; - - return ( - - - {trigger} - - - - {t("access.group.add")} - - -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.group.form.name.label")} - - - - - - - )} - /> - -
- -
- - -
-
-
- ); -}; - -export default AccessGroupEdit; diff --git a/ui/src/components/certimate/AccessGroupList.tsx b/ui/src/components/certimate/AccessGroupList.tsx deleted file mode 100644 index e07344ca..00000000 --- a/ui/src/components/certimate/AccessGroupList.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import { Group } from "lucide-react"; - -import Show from "@/components/Show"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { useToast } from "@/components/ui/use-toast"; -import AccessGroupEdit from "./AccessGroupEdit"; -import { accessProvidersMap } from "@/domain/access"; -import { getErrMessage } from "@/lib/error"; -import { useConfigContext } from "@/providers/config"; -import { remove } from "@/repository/access_group"; - -const AccessGroupList = () => { - const { - config: { accessGroups }, - reloadAccessGroups, - } = useConfigContext(); - - const { toast } = useToast(); - - const navigate = useNavigate(); - const { t } = useTranslation(); - - const handleRemoveClick = async (id: string) => { - try { - await remove(id); - reloadAccessGroups(); - } catch (e) { - toast({ - title: t("common.delete.failed.message"), - description: getErrMessage(e), - variant: "destructive", - }); - return; - } - }; - - const handleAddAccess = () => { - navigate("/access"); - }; - return ( -
- - <> -
- - - - -
{t("access.group.domains.nodata")}
- {t("access.group.add")}} className="mt-3" /> -
- -
- - -
- {accessGroups.map((accessGroup) => ( - - - {accessGroup.name} - - {t("access.group.total", { - total: accessGroup.expand ? accessGroup.expand.access.length : 0, - })} - - - - {accessGroup.expand ? ( - <> - {accessGroup.expand.access.slice(0, 3).map((access) => ( -
-
-
- provider -
-
-
{access.name}
-
{accessProvidersMap.get(access.configType)!.name}
-
-
-
- ))} - - ) : ( - <> -
-
- -
-
{t("access.group.nodata")}
-
- - )} -
- -
- 0 ? true : false}> -
- -
-
- - -
- -
-
- -
- - - - - - - {t("access.group.delete")} - {t("access.group.delete.confirm")} - - - {t("common.cancel")} - { - handleRemoveClick(accessGroup.id ? accessGroup.id : ""); - }} - > - {t("common.confirm")} - - - - -
-
-
-
- ))} -
-
-
- ); -}; - -export default AccessGroupList; diff --git a/ui/src/components/certimate/AccessSSHForm.tsx b/ui/src/components/certimate/AccessSSHForm.tsx index 440c585c..797538b0 100644 --- a/ui/src/components/certimate/AccessSSHForm.tsx +++ b/ui/src/components/certimate/AccessSSHForm.tsx @@ -3,16 +3,12 @@ import { useTranslation } from "react-i18next"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Plus } from "lucide-react"; import { ClientResponseError } from "pocketbase"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import AccessGroupEdit from "./AccessGroupEdit"; import { readFileContent } from "@/lib/file"; -import { cn } from "@/lib/utils"; import { PbErrorData } from "@/domain/base"; import { accessProvidersMap, accessTypeFormSchema, type Access, type SSHConfig } from "@/domain/access"; import { save } from "@/repository/access"; @@ -26,12 +22,7 @@ type AccessSSHFormProps = { }; const AccessSSHForm = ({ data, op, onAfterReq }: AccessSSHFormProps) => { - const { - addAccess, - updateAccess, - reloadAccessGroups, - config: { accessGroups }, - } = useConfigContext(); + const { addAccess, updateAccess, reloadAccessGroups } = useConfigContext(); const fileInputRef = useRef(null); @@ -216,52 +207,6 @@ const AccessSSHForm = ({ data, op, onAfterReq }: AccessSSHFormProps) => { )} /> - ( - - -
{t("access.authorization.form.ssh_group.label")}
- - - {t("common.add")} - - } - /> -
- - - - - -
- )} - /> - = { - config: Omit & { config: T }; - setConfig: (config: Omit & { config: T }) => void; - - errors: { [K in keyof T]?: string }; - setErrors: (error: { [K in keyof T]?: string }) => void; -}; - -export const Context = createContext({} as DeployEditContext); - -export function useDeployEditContext() { - return useContext>(Context as unknown as ReactContext>); -} diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx deleted file mode 100644 index cc25e8f5..00000000 --- a/ui/src/components/certimate/DeployEditDialog.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Plus } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import AccessEditDialog from "./AccessEditDialog"; -import { Context as DeployEditContext, type DeployEditContext as DeployEditContextType } from "./DeployEdit"; -import DeployToAliyunOSS from "./DeployToAliyunOSS"; -import DeployToAliyunCDN from "./DeployToAliyunCDN"; -import DeployToAliyunCLB from "./DeployToAliyunCLB"; -import DeployToAliyunALB from "./DeployToAliyunALB"; -import DeployToAliyunNLB from "./DeployToAliyunNLB"; -import DeployToTencentCDN from "./DeployToTencentCDN"; -import DeployToTencentCLB from "./DeployToTencentCLB"; -import DeployToTencentCOS from "./DeployToTencentCOS"; -import DeployToTencentTEO from "./DeployToTencentTEO"; -import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN"; -import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB"; -import DeployToBaiduCloudCDN from "./DeployToBaiduCloudCDN"; -import DeployToQiniuCDN from "./DeployToQiniuCDN"; -import DeployToDogeCloudCDN from "./DeployToDogeCloudCDN"; -import DeployToLocal from "./DeployToLocal"; -import DeployToSSH from "./DeployToSSH"; -import DeployToWebhook from "./DeployToWebhook"; -import DeployToKubernetesSecret from "./DeployToKubernetesSecret"; -import DeployToVolcengineLive from "./DeployToVolcengineLive"; -import DeployToVolcengineCDN from "./DeployToVolcengineCDN"; -import DeployToByteplusCDN from "./DeployToByteplusCDN"; -import { deployTargetsMap, type DeployConfig } from "@/domain/domain"; -import { accessProvidersMap } from "@/domain/access"; -import { useConfigContext } from "@/providers/config"; - -type DeployEditDialogProps = { - trigger: React.ReactNode; - deployConfig?: DeployConfig; - onSave: (deploy: DeployConfig) => void; -}; - -const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogProps) => { - const { t } = useTranslation(); - - const { - config: { accesses }, - } = useConfigContext(); - - const [deployType, setDeployType] = useState(""); - - const [locDeployConfig, setLocDeployConfig] = useState({ - access: "", - type: "", - }); - - const [errors, setErrors] = useState>({}); - - const [open, setOpen] = useState(false); - - useEffect(() => { - if (deployConfig) { - setLocDeployConfig({ ...deployConfig }); - } else { - setLocDeployConfig({ - access: "", - type: "", - }); - } - }, [deployConfig]); - - useEffect(() => { - setDeployType(locDeployConfig.type); - setErrors({}); - }, [locDeployConfig.type]); - - const setConfig = useCallback( - (deploy: DeployConfig) => { - if (deploy.type !== locDeployConfig.type) { - setLocDeployConfig({ ...deploy, access: "", config: {} }); - } else { - setLocDeployConfig({ ...deploy }); - } - }, - [locDeployConfig.type] - ); - - const targetAccesses = accesses.filter((item) => { - if (item.usage == "apply") { - return false; - } - - if (locDeployConfig.type == "") { - return true; - } - - return item.configType === deployTargetsMap.get(locDeployConfig.type)?.provider; - }); - - const handleSaveClick = () => { - // 验证数据 - const newError = { ...errors }; - newError.type = locDeployConfig.type === "" ? t("domain.deployment.form.access.placeholder") : ""; - newError.access = locDeployConfig.access === "" ? t("domain.deployment.form.access.placeholder") : ""; - setErrors(newError); - if (Object.values(newError).some((e) => !!e)) return; - - // 保存数据 - onSave(locDeployConfig); - - // 清理数据 - setLocDeployConfig({ - access: "", - type: "", - }); - setErrors({}); - - // 关闭弹框 - setOpen(false); - }; - - let childComponent = <>; - switch (deployType) { - case "aliyun-oss": - childComponent = ; - break; - case "aliyun-cdn": - case "aliyun-dcdn": - childComponent = ; - break; - case "aliyun-clb": - childComponent = ; - break; - case "aliyun-alb": - childComponent = ; - break; - case "aliyun-nlb": - childComponent = ; - break; - case "tencent-cdn": - case "tencent-ecdn": - childComponent = ; - break; - case "tencent-clb": - childComponent = ; - break; - case "tencent-cos": - childComponent = ; - break; - case "tencent-teo": - childComponent = ; - break; - case "huaweicloud-cdn": - childComponent = ; - break; - case "huaweicloud-elb": - childComponent = ; - break; - case "baiducloud-cdn": - childComponent = ; - break; - case "qiniu-cdn": - childComponent = ; - break; - case "dogecloud-cdn": - childComponent = ; - break; - case "local": - childComponent = ; - break; - case "ssh": - childComponent = ; - break; - case "webhook": - childComponent = ; - break; - case "k8s-secret": - childComponent = ; - break; - case "volcengine-live": - childComponent = ; - break; - case "volcengine-cdn": - childComponent = ; - break; - case "byteplus-cdn": - childComponent = ; - break; - } - - return ( - - - {trigger} - { - event.preventDefault(); - }} - > - - {t("domain.deployment.tab")} - - - - -
- {/* 部署方式 */} -
- - - - -
{errors.type}
-
- - {/* 授权配置 */} -
-
- } - op="add" - /> - - - - -
{errors.access}
-
- - {/* 其他参数 */} -
{childComponent}
- -
- - - - -
-
-
- ); -}; - -export default DeployEditDialog; diff --git a/ui/src/components/certimate/DeployList.tsx b/ui/src/components/certimate/DeployList.tsx deleted file mode 100644 index 606ee470..00000000 --- a/ui/src/components/certimate/DeployList.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { nanoid } from "nanoid"; -import { EditIcon, Trash2 } from "lucide-react"; - -import Show from "@/components/Show"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import DeployEditDialog from "./DeployEditDialog"; -import { DeployConfig } from "@/domain/domain"; -import { accessProvidersMap } from "@/domain/access"; -import { deployTargetsMap } from "@/domain/domain"; -import { useConfigContext } from "@/providers/config"; - -type DeployItemProps = { - item: DeployConfig; - onDelete: () => void; - onSave: (deploy: DeployConfig) => void; -}; - -const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => { - const { t } = useTranslation(); - - const { - config: { accesses }, - } = useConfigContext(); - - const access = accesses.find((access) => access.id === item.access); - - const getTypeIcon = () => { - if (!access) { - return ""; - } - - return accessProvidersMap.get(access.configType)?.icon || ""; - }; - - const getTypeName = () => { - return t(deployTargetsMap.get(item.type)?.name || ""); - }; - - return ( -
-
-
- -
-
-
{getTypeName()}
-
{access?.name}
-
-
-
- } - deployConfig={item} - onSave={(deploy: DeployConfig) => { - onSave(deploy); - }} - /> - - { - onDelete(); - }} - /> -
-
- ); -}; - -type DeployListProps = { - deploys: DeployConfig[]; - onChange: (deploys: DeployConfig[]) => void; -}; - -const DeployList = ({ deploys, onChange }: DeployListProps) => { - const [list, setList] = useState([]); - - const { t } = useTranslation(); - - useEffect(() => { - setList(deploys); - }, [deploys]); - - const handleAdd = (deploy: DeployConfig) => { - deploy.id = nanoid(); - - const newList = [...list, deploy]; - - setList(newList); - - onChange(newList); - }; - - const handleDelete = (id: string) => { - const newList = list.filter((item) => item.id !== id); - - setList(newList); - - onChange(newList); - }; - - const handleSave = (deploy: DeployConfig) => { - const newList = list.map((item) => { - if (item.id === deploy.id) { - return { ...deploy }; - } - return item; - }); - - setList(newList); - - onChange(newList); - }; - - return ( - <> - 0} - fallback={ - - -
{t("domain.deployment.nodata")}
-
- { - handleAdd(config); - }} - trigger={} - /> -
-
-
- } - > -
- {t("common.add")}} - onSave={(config: DeployConfig) => { - handleAdd(config); - }} - /> -
- -
-
- {list.map((item) => ( - { - handleDelete(item.id ?? ""); - }} - onSave={(deploy: DeployConfig) => { - handleSave(deploy); - }} - /> - ))} -
-
-
- - ); -}; - -export default DeployList; diff --git a/ui/src/components/certimate/DeployProgress.tsx b/ui/src/components/certimate/DeployProgress.tsx deleted file mode 100644 index 96315f19..00000000 --- a/ui/src/components/certimate/DeployProgress.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useTranslation } from "react-i18next"; - -import { Separator } from "@/components/ui/separator"; -import { cn } from "@/lib/utils"; - -type DeployProgressProps = { - phase?: "check" | "apply" | "deploy"; - phaseSuccess?: boolean; -}; - -const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => { - const { t } = useTranslation(); - - let step = 0; - - if (phase === "check") { - step = 1; - } else if (phase === "apply") { - step = 2; - } else if (phase === "deploy") { - step = 3; - } - - return ( -
-
1 ? "text-green-600" : "")}> - {t("history.props.stage.progress.check")} -
- 1 ? "bg-green-600" : "")} /> -
2 ? "text-green-600" : "" - )} - > - {t("history.props.stage.progress.apply")} -
- 2 ? "bg-green-600" : "")} /> -
3 ? "text-green-600" : "" - )} - > - {t("history.props.stage.progress.deploy")} -
-
- ); -}; - -export default DeployProgress; diff --git a/ui/src/components/certimate/DeployState.tsx b/ui/src/components/certimate/DeployState.tsx deleted file mode 100644 index 58bab9ec..00000000 --- a/ui/src/components/certimate/DeployState.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { CircleCheck, CircleX } from "lucide-react"; - -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { Deployment } from "@/domain/deployment"; - -type DeployStateProps = { - deployment: Deployment; -}; - -const DeployState = ({ deployment }: DeployStateProps) => { - // 获取指定阶段的错误信息 - const error = (state: "check" | "apply" | "deploy") => { - if (!deployment.log[state]) { - return ""; - } - return deployment.log[state][deployment.log[state].length - 1].error; - }; - - return ( - <> - {(deployment.phase === "deploy" && deployment.phaseSuccess) || deployment.wholeSuccess ? ( - - ) : ( - <> - {error(deployment.phase).length ? ( - - - - - - {error(deployment.phase)} - - - ) : ( - - )} - - )} - - ); -}; - -export default DeployState; diff --git a/ui/src/components/certimate/DeployToAliyunALB.tsx b/ui/src/components/certimate/DeployToAliyunALB.tsx deleted file mode 100644 index 33a7c562..00000000 --- a/ui/src/components/certimate/DeployToAliyunALB.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToAliyunALBConfigParams = { - region?: string; - resourceType?: string; - loadbalancerId?: string; - listenerId?: string; -}; - -const DeployToAliyunALB = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - region: "cn-hangzhou", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z - .object({ - region: z.string().min(1, t("domain.deployment.form.aliyun_alb_region.placeholder")), - resourceType: z.union([z.literal("loadbalancer"), z.literal("listener")], { - message: t("domain.deployment.form.aliyun_alb_resource_type.placeholder"), - }), - loadbalancerId: z.string().optional(), - listenerId: z.string().optional(), - }) - .refine((data) => (data.resourceType === "loadbalancer" ? !!data.loadbalancerId?.trim() : true), { - message: t("domain.deployment.form.aliyun_alb_loadbalancer_id.placeholder"), - path: ["loadbalancerId"], - }) - .refine((data) => (data.resourceType === "listener" ? !!data.listenerId?.trim() : true), { - message: t("domain.deployment.form.aliyun_alb_listener_id.placeholder"), - path: ["listenerId"], - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, - resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message, - loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message, - listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.region = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.region}
-
- -
- - -
{errors?.resourceType}
-
- - {config?.config?.resourceType === "loadbalancer" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.loadbalancerId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.loadbalancerId}
-
- ) : ( - <> - )} - - {config?.config?.resourceType === "listener" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.listenerId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.listenerId}
-
- ) : ( - <> - )} -
- ); -}; - -export default DeployToAliyunALB; diff --git a/ui/src/components/certimate/DeployToAliyunCDN.tsx b/ui/src/components/certimate/DeployToAliyunCDN.tsx deleted file mode 100644 index 0d073720..00000000 --- a/ui/src/components/certimate/DeployToAliyunCDN.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToAliyunCDNConfigParams = { - domain?: string; -}; - -const DeployToAliyunCDN = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: {}, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
-
- ); -}; - -export default DeployToAliyunCDN; diff --git a/ui/src/components/certimate/DeployToAliyunCLB.tsx b/ui/src/components/certimate/DeployToAliyunCLB.tsx deleted file mode 100644 index a00c3b5b..00000000 --- a/ui/src/components/certimate/DeployToAliyunCLB.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToAliyunCLBConfigParams = { - region?: string; - resourceType?: string; - loadbalancerId?: string; - listenerPort?: string; -}; - -const DeployToAliyunCLB = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - region: "cn-hangzhou", - listenerPort: "443", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z - .object({ - region: z.string().min(1, t("domain.deployment.form.aliyun_clb_region.placeholder")), - resourceType: z.union([z.literal("certificate"), z.literal("loadbalancer"), z.literal("listener")], { - message: t("domain.deployment.form.aliyun_clb_resource_type.placeholder"), - }), - loadbalancerId: z.string().optional(), - listenerPort: z.string().optional(), - }) - .refine((data) => (data.resourceType === "loadbalancer" || data.resourceType === "listener" ? !!data.loadbalancerId?.trim() : true), { - message: t("domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder"), - path: ["loadbalancerId"], - }) - .refine((data) => (data.resourceType === "listener" ? +data.listenerPort! > 0 && +data.listenerPort! < 65535 : true), { - message: t("domain.deployment.form.aliyun_clb_listener_port.placeholder"), - path: ["listenerPort"], - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, - resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message, - loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message, - listenerPort: res.error?.errors?.find((e) => e.path[0] === "listenerPort")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.region = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.region}
-
- -
- - -
{errors?.resourceType}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.loadbalancerId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.loadbalancerId}
-
- - {config?.config?.resourceType === "listener" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.listenerPort = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.listenerPort}
-
- ) : ( - <> - )} -
- ); -}; - -export default DeployToAliyunCLB; diff --git a/ui/src/components/certimate/DeployToAliyunNLB.tsx b/ui/src/components/certimate/DeployToAliyunNLB.tsx deleted file mode 100644 index 8ca68fa8..00000000 --- a/ui/src/components/certimate/DeployToAliyunNLB.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToAliyunNLBConfigParams = { - region?: string; - resourceType?: string; - loadbalancerId?: string; - listenerId?: string; -}; - -const DeployToAliyunNLB = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - region: "cn-hangzhou", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z - .object({ - region: z.string().min(1, t("domain.deployment.form.aliyun_nlb_region.placeholder")), - resourceType: z.union([z.literal("loadbalancer"), z.literal("listener")], { - message: t("domain.deployment.form.aliyun_nlb_resource_type.placeholder"), - }), - loadbalancerId: z.string().optional(), - listenerId: z.string().optional(), - }) - .refine((data) => (data.resourceType === "loadbalancer" ? !!data.loadbalancerId?.trim() : true), { - message: t("domain.deployment.form.aliyun_nlb_loadbalancer_id.placeholder"), - path: ["loadbalancerId"], - }) - .refine((data) => (data.resourceType === "listener" ? !!data.listenerId?.trim() : true), { - message: t("domain.deployment.form.aliyun_nlb_listener_id.placeholder"), - path: ["listenerId"], - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, - resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message, - loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message, - listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.region = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.region}
-
- -
- - -
{errors?.resourceType}
-
- - {config?.config?.resourceType === "loadbalancer" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.loadbalancerId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.loadbalancerId}
-
- ) : ( - <> - )} - - {config?.config?.resourceType === "listener" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.listenerId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.listenerId}
-
- ) : ( - <> - )} -
- ); -}; - -export default DeployToAliyunNLB; diff --git a/ui/src/components/certimate/DeployToAliyunOSS.tsx b/ui/src/components/certimate/DeployToAliyunOSS.tsx deleted file mode 100644 index 14249758..00000000 --- a/ui/src/components/certimate/DeployToAliyunOSS.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToAliyunOSSConfigParams = { - endpoint?: string; - bucket?: string; - domain?: string; -}; - -const DeployToAliyunOSS = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - endpoint: "oss.aliyuncs.com", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - endpoint: z.string().min(1, { - message: t("domain.deployment.form.aliyun_oss_endpoint.placeholder"), - }), - bucket: z.string().min(1, { - message: t("domain.deployment.form.aliyun_oss_bucket.placeholder"), - }), - domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - endpoint: res.error?.errors?.find((e) => e.path[0] === "endpoint")?.message, - bucket: res.error?.errors?.find((e) => e.path[0] === "bucket")?.message, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.endpoint = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.endpoint}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.bucket = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.bucket}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
-
- ); -}; - -export default DeployToAliyunOSS; diff --git a/ui/src/components/certimate/DeployToBaiduCloudCDN.tsx b/ui/src/components/certimate/DeployToBaiduCloudCDN.tsx deleted file mode 100644 index 262fb3e5..00000000 --- a/ui/src/components/certimate/DeployToBaiduCloudCDN.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToBaiduCloudCDNConfigParams = { - domain?: string; -}; - -const DeployToBaiduCloudCDN = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: {}, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
-
- ); -}; - -export default DeployToBaiduCloudCDN; diff --git a/ui/src/components/certimate/DeployToByteplusCDN.tsx b/ui/src/components/certimate/DeployToByteplusCDN.tsx deleted file mode 100644 index a1944b99..00000000 --- a/ui/src/components/certimate/DeployToByteplusCDN.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToByteplusCDNConfigParams = { - domain?: string; -}; - -const DeployToByteplusCDN = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: {}, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
-
- ); -}; - -export default DeployToByteplusCDN; diff --git a/ui/src/components/certimate/DeployToDogeCloudCDN.tsx b/ui/src/components/certimate/DeployToDogeCloudCDN.tsx deleted file mode 100644 index 5b0ff52d..00000000 --- a/ui/src/components/certimate/DeployToDogeCloudCDN.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToDogeCloudCDNConfigParams = { - domain?: string; -}; - -const DeployToDogeCloudCDN = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: {}, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
-
- ); -}; - -export default DeployToDogeCloudCDN; diff --git a/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx b/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx deleted file mode 100644 index eaeda1b7..00000000 --- a/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToHuaweiCloudCDNConfigParams = { - region?: string; - domain?: string; -}; - -const DeployToHuaweiCloudCDN = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - region: "cn-north-1", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - region: z.string().min(1, { - message: t("domain.deployment.form.huaweicloud_cdn_region.placeholder"), - }), - domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.region = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.region}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
-
- ); -}; - -export default DeployToHuaweiCloudCDN; diff --git a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx deleted file mode 100644 index a26d33c8..00000000 --- a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToHuaweiCloudELBConfigParams = { - region?: string; - resourceType?: string; - certificateId?: string; - loadbalancerId?: string; - listenerId?: string; -}; - -const DeployToHuaweiCloudELB = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - region: "cn-north-1", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z - .object({ - region: z.string().min(1, t("domain.deployment.form.huaweicloud_elb_region.placeholder")), - resourceType: z.union([z.literal("certificate"), z.literal("loadbalancer"), z.literal("listener")], { - message: t("domain.deployment.form.huaweicloud_elb_resource_type.placeholder"), - }), - certificateId: z.string().optional(), - loadbalancerId: z.string().optional(), - listenerId: z.string().optional(), - }) - .refine((data) => (data.resourceType === "certificate" ? !!data.certificateId?.trim() : true), { - message: t("domain.deployment.form.huaweicloud_elb_certificate_id.placeholder"), - path: ["certificateId"], - }) - .refine((data) => (data.resourceType === "loadbalancer" ? !!data.loadbalancerId?.trim() : true), { - message: t("domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder"), - path: ["loadbalancerId"], - }) - .refine((data) => (data.resourceType === "listener" ? !!data.listenerId?.trim() : true), { - message: t("domain.deployment.form.huaweicloud_elb_listener_id.placeholder"), - path: ["listenerId"], - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, - resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message, - certificateId: res.error?.errors?.find((e) => e.path[0] === "certificateId")?.message, - loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message, - listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.region = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.region}
-
- -
- - -
{errors?.resourceType}
-
- - {config?.config?.resourceType === "certificate" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.certificateId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.certificateId}
-
- ) : ( - <> - )} - - {config?.config?.resourceType === "loadbalancer" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.loadbalancerId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.loadbalancerId}
-
- ) : ( - <> - )} - - {config?.config?.resourceType === "listener" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.listenerId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.listenerId}
-
- ) : ( - <> - )} -
- ); -}; - -export default DeployToHuaweiCloudELB; diff --git a/ui/src/components/certimate/DeployToKubernetesSecret.tsx b/ui/src/components/certimate/DeployToKubernetesSecret.tsx deleted file mode 100644 index 119ae12f..00000000 --- a/ui/src/components/certimate/DeployToKubernetesSecret.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToKubernetesSecretConfigParams = { - namespace?: string; - secretName?: string; - secretDataKeyForCrt?: string; - secretDataKeyForKey?: string; -}; - -const DeployToKubernetesSecret = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - namespace: "default", - secretDataKeyForCrt: "tls.crt", - secretDataKeyForKey: "tls.key", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - namespace: z.string().min(1, { - message: t("domain.deployment.form.k8s_namespace.placeholder"), - }), - secretName: z.string().min(1, { - message: t("domain.deployment.form.k8s_secret_name.placeholder"), - }), - secretDataKeyForCrt: z.string().min(1, { - message: t("domain.deployment.form.k8s_secret_data_key_for_crt.placeholder"), - }), - secretDataKeyForKey: z.string().min(1, { - message: t("domain.deployment.form.k8s_secret_data_key_for_key.placeholder"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - namespace: res.error?.errors?.find((e) => e.path[0] === "namespace")?.message, - secretName: res.error?.errors?.find((e) => e.path[0] === "secretName")?.message, - secretDataKeyForCrt: res.error?.errors?.find((e) => e.path[0] === "secretDataKeyForCrt")?.message, - secretDataKeyForKey: res.error?.errors?.find((e) => e.path[0] === "secretDataKeyForKey")?.message, - }); - }, [config]); - - return ( - <> -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.namespace = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.secretName = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.secretDataKeyForCrt = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.secretDataKeyForKey = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
-
- - ); -}; - -export default DeployToKubernetesSecret; diff --git a/ui/src/components/certimate/DeployToLocal.tsx b/ui/src/components/certimate/DeployToLocal.tsx deleted file mode 100644 index 11a20c2a..00000000 --- a/ui/src/components/certimate/DeployToLocal.tsx +++ /dev/null @@ -1,481 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { useDeployEditContext } from "./DeployEdit"; -import { cn } from "@/lib/utils"; - -type DeployToLocalConfigParams = { - format?: string; - certPath?: string; - keyPath?: string; - pfxPassword?: string; - jksAlias?: string; - jksKeypass?: string; - jksStorepass?: string; - shell?: string; - preCommand?: string; - command?: string; -}; - -const DeployToLocal = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - format: "pem", - certPath: "/etc/nginx/ssl/nginx.crt", - keyPath: "/etc/nginx/ssl/nginx.key", - shell: "sh", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z - .object({ - format: z.union([z.literal("pem"), z.literal("pfx"), z.literal("jks")], { - message: t("domain.deployment.form.file_format.placeholder"), - }), - certPath: z - .string() - .min(1, t("domain.deployment.form.file_cert_path.placeholder")) - .max(255, t("common.errmsg.string_max", { max: 255 })), - keyPath: z - .string() - .min(0, t("domain.deployment.form.file_key_path.placeholder")) - .max(255, t("common.errmsg.string_max", { max: 255 })), - pfxPassword: z.string().optional(), - jksAlias: z.string().optional(), - jksKeypass: z.string().optional(), - jksStorepass: z.string().optional(), - shell: z.union([z.literal("sh"), z.literal("cmd"), z.literal("powershell")], { - message: t("domain.deployment.form.shell.placeholder"), - }), - preCommand: z.string().optional(), - command: z.string().optional(), - }) - .refine((data) => (data.format === "pem" ? !!data.keyPath?.trim() : true), { - message: t("domain.deployment.form.file_key_path.placeholder"), - path: ["keyPath"], - }) - .refine((data) => (data.format === "pfx" ? !!data.pfxPassword?.trim() : true), { - message: t("domain.deployment.form.file_pfx_password.placeholder"), - path: ["pfxPassword"], - }) - .refine((data) => (data.format === "jks" ? !!data.jksAlias?.trim() : true), { - message: t("domain.deployment.form.file_jks_alias.placeholder"), - path: ["jksAlias"], - }) - .refine((data) => (data.format === "jks" ? !!data.jksKeypass?.trim() : true), { - message: t("domain.deployment.form.file_jks_keypass.placeholder"), - path: ["jksKeypass"], - }) - .refine((data) => (data.format === "jks" ? !!data.jksStorepass?.trim() : true), { - message: t("domain.deployment.form.file_jks_storepass.placeholder"), - path: ["jksStorepass"], - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - format: res.error?.errors?.find((e) => e.path[0] === "format")?.message, - certPath: res.error?.errors?.find((e) => e.path[0] === "certPath")?.message, - keyPath: res.error?.errors?.find((e) => e.path[0] === "keyPath")?.message, - pfxPassword: res.error?.errors?.find((e) => e.path[0] === "pfxPassword")?.message, - jksAlias: res.error?.errors?.find((e) => e.path[0] === "jksAlias")?.message, - jksKeypass: res.error?.errors?.find((e) => e.path[0] === "jksKeypass")?.message, - jksStorepass: res.error?.errors?.find((e) => e.path[0] === "jksStorepass")?.message, - shell: res.error?.errors?.find((e) => e.path[0] === "shell")?.message, - preCommand: res.error?.errors?.find((e) => e.path[0] === "preCommand")?.message, - command: res.error?.errors?.find((e) => e.path[0] === "command")?.message, - }); - }, [config]); - - useEffect(() => { - if (config.config?.format === "pem") { - if (/(.pfx|.jks)$/.test(config.config.certPath!)) { - setConfig( - produce(config, (draft) => { - draft.config ??= {}; - draft.config.certPath = config.config!.certPath!.replace(/(.pfx|.jks)$/, ".crt"); - }) - ); - } - } else if (config.config?.format === "pfx") { - if (/(.crt|.jks)$/.test(config.config.certPath!)) { - setConfig( - produce(config, (draft) => { - draft.config ??= {}; - draft.config.certPath = config.config!.certPath!.replace(/(.crt|.jks)$/, ".pfx"); - }) - ); - } - } else if (config.config?.format === "jks") { - if (/(.crt|.pfx)$/.test(config.config.certPath!)) { - setConfig( - produce(config, (draft) => { - draft.config ??= {}; - draft.config.certPath = config.config!.certPath!.replace(/(.crt|.pfx)$/, ".jks"); - }) - ); - } - } - }, [config.config?.format]); - - const getOptionCls = (val: string) => { - if (config.config?.shell === val) { - return "border-primary dark:border-primary"; - } - - return ""; - }; - - const handleUsePresetScript = (key: string) => { - switch (key) { - case "reload_nginx": - { - setConfig( - produce(config, (draft) => { - draft.config ??= {}; - draft.config.shell = "sh"; - draft.config.command = "sudo service nginx reload"; - }) - ); - } - break; - - case "binding_iis": - { - setConfig( - produce(config, (draft) => { - draft.config ??= {}; - draft.config.shell = "powershell"; - draft.config.command = ` -# 请将以下变量替换为实际值 -$pfxPath = "" # PFX 文件路径 -$pfxPassword = "" # PFX 密码 -$siteName = "" # IIS 网站名称 -$domain = "" # 域名 -$ipaddr = "" # 绑定 IP,“*”表示所有 IP 绑定 -$port = "" # 绑定端口 - - -# 导入证书到本地计算机的个人存储区 -$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable -# 获取 Thumbprint -$thumbprint = $cert.Thumbprint -# 导入 WebAdministration 模块 -Import-Module WebAdministration -# 检查是否已存在 HTTPS 绑定 -$existingBinding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -HostHeader "$domain" -ErrorAction SilentlyContinue -if (!$existingBinding) { - # 添加新的 HTTPS 绑定 - New-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" -} -# 获取绑定对象 -$binding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" -# 绑定 SSL 证书 -$binding.AddSslCertificate($thumbprint, "My") -# 删除目录下的证书文件 -Remove-Item -Path "$pfxPath" -Force - `.trim(); - }) - ); - } - break; - - case "binding_netsh": - { - setConfig( - produce(config, (draft) => { - draft.config ??= {}; - draft.config.shell = "powershell"; - draft.config.command = ` -# 请将以下变量替换为实际值 -$pfxPath = "" # PFX 文件路径 -$pfxPassword = "" # PFX 密码 -$ipaddr = "" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名。 -$port = "" # 绑定端口 - -$addr = $ipaddr + ":" + $port - -# 导入证书到本地计算机的个人存储区 -$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable -# 获取 Thumbprint -$thumbprint = $cert.Thumbprint -# 检测端口是否绑定证书,如绑定则删除绑定 -$isExist = netsh http show sslcert ipport=$addr -if ($isExist -like "*$addr*"){ netsh http delete sslcert ipport=$addr } -# 绑定到端口 -netsh http add sslcert ipport=$addr certhash=$thumbprint -# 删除目录下的证书文件 -Remove-Item -Path "$pfxPath" -Force - `.trim(); - }) - ); - } - break; - } - }; - - return ( - <> -
-
- - -
{errors?.format}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.certPath = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.certPath}
-
- - {config.config?.format === "pem" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.keyPath = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.keyPath}
-
- ) : ( - <> - )} - - {config.config?.format === "pfx" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.pfxPassword = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.pfxPassword}
-
- ) : ( - <> - )} - - {config.config?.format === "jks" ? ( - <> -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.jksAlias = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.jksAlias}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.jksKeypass = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.jksKeypass}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.jksStorepass = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.jksStorepass}
-
- - ) : ( - <> - )} - -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.shell = val; - }); - setConfig(nv); - }} - > -
- - -
-
- - -
-
- - -
-
-
{errors?.shell}
-
- -
- - -
{errors?.preCommand}
-
- -
-
- - - - {t("domain.deployment.form.shell_preset_scripts.trigger")} - - - handleUsePresetScript("reload_nginx")}> - {t("domain.deployment.form.shell_preset_scripts.option.reload_nginx.label")} - - handleUsePresetScript("binding_iis")}> - {t("domain.deployment.form.shell_preset_scripts.option.binding_iis.label")} - - handleUsePresetScript("binding_netsh")}> - {t("domain.deployment.form.shell_preset_scripts.option.binding_netsh.label")} - - - -
- -
{errors?.command}
-
-
- - ); -}; - -export default DeployToLocal; diff --git a/ui/src/components/certimate/DeployToQiniuCDN.tsx b/ui/src/components/certimate/DeployToQiniuCDN.tsx deleted file mode 100644 index 90163c12..00000000 --- a/ui/src/components/certimate/DeployToQiniuCDN.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToQiniuCDNConfigParams = { - domain?: string; -}; - -const DeployToQiniuCDN = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: {}, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
-
- ); -}; - -export default DeployToQiniuCDN; diff --git a/ui/src/components/certimate/DeployToSSH.tsx b/ui/src/components/certimate/DeployToSSH.tsx deleted file mode 100644 index 8db0cde5..00000000 --- a/ui/src/components/certimate/DeployToSSH.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToSSHConfigParams = { - format?: string; - certPath?: string; - keyPath?: string; - pfxPassword?: string; - jksAlias?: string; - jksKeypass?: string; - jksStorepass?: string; - shell?: string; - preCommand?: string; - command?: string; -}; - -const DeployToSSH = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - format: "pem", - certPath: "/etc/nginx/ssl/nginx.crt", - keyPath: "/etc/nginx/ssl/nginx.key", - command: "sudo service nginx reload", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z - .object({ - format: z.union([z.literal("pem"), z.literal("pfx"), z.literal("jks")], { - message: t("domain.deployment.form.file_format.placeholder"), - }), - certPath: z - .string() - .min(1, t("domain.deployment.form.file_cert_path.placeholder")) - .max(255, t("common.errmsg.string_max", { max: 255 })), - keyPath: z - .string() - .min(0, t("domain.deployment.form.file_key_path.placeholder")) - .max(255, t("common.errmsg.string_max", { max: 255 })), - pfxPassword: z.string().optional(), - jksAlias: z.string().optional(), - jksKeypass: z.string().optional(), - jksStorepass: z.string().optional(), - preCommand: z.string().optional(), - command: z.string().optional(), - }) - .refine((data) => (data.format === "pem" ? !!data.keyPath?.trim() : true), { - message: t("domain.deployment.form.file_key_path.placeholder"), - path: ["keyPath"], - }) - .refine((data) => (data.format === "pfx" ? !!data.pfxPassword?.trim() : true), { - message: t("domain.deployment.form.file_pfx_password.placeholder"), - path: ["pfxPassword"], - }) - .refine((data) => (data.format === "jks" ? !!data.jksAlias?.trim() : true), { - message: t("domain.deployment.form.file_jks_alias.placeholder"), - path: ["jksAlias"], - }) - .refine((data) => (data.format === "jks" ? !!data.jksKeypass?.trim() : true), { - message: t("domain.deployment.form.file_jks_keypass.placeholder"), - path: ["jksKeypass"], - }) - .refine((data) => (data.format === "jks" ? !!data.jksStorepass?.trim() : true), { - message: t("domain.deployment.form.file_jks_storepass.placeholder"), - path: ["jksStorepass"], - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - format: res.error?.errors?.find((e) => e.path[0] === "format")?.message, - certPath: res.error?.errors?.find((e) => e.path[0] === "certPath")?.message, - keyPath: res.error?.errors?.find((e) => e.path[0] === "keyPath")?.message, - pfxPassword: res.error?.errors?.find((e) => e.path[0] === "pfxPassword")?.message, - jksAlias: res.error?.errors?.find((e) => e.path[0] === "jksAlias")?.message, - jksKeypass: res.error?.errors?.find((e) => e.path[0] === "jksKeypass")?.message, - jksStorepass: res.error?.errors?.find((e) => e.path[0] === "jksStorepass")?.message, - preCommand: res.error?.errors?.find((e) => e.path[0] === "preCommand")?.message, - command: res.error?.errors?.find((e) => e.path[0] === "command")?.message, - }); - }, [config]); - - useEffect(() => { - if (config.config?.format === "pem") { - if (/(.pfx|.jks)$/.test(config.config.certPath!)) { - setConfig( - produce(config, (draft) => { - draft.config ??= {}; - draft.config.certPath = config.config!.certPath!.replace(/(.pfx|.jks)$/, ".crt"); - }) - ); - } - } else if (config.config?.format === "pfx") { - if (/(.crt|.jks)$/.test(config.config.certPath!)) { - setConfig( - produce(config, (draft) => { - draft.config ??= {}; - draft.config.certPath = config.config!.certPath!.replace(/(.crt|.jks)$/, ".pfx"); - }) - ); - } - } else if (config.config?.format === "jks") { - if (/(.crt|.pfx)$/.test(config.config.certPath!)) { - setConfig( - produce(config, (draft) => { - draft.config ??= {}; - draft.config.certPath = config.config!.certPath!.replace(/(.crt|.pfx)$/, ".jks"); - }) - ); - } - } - }, [config.config?.format]); - - return ( - <> -
-
- - -
{errors?.format}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.certPath = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.certPath}
-
- - {config.config?.format === "pem" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.keyPath = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.keyPath}
-
- ) : ( - <> - )} - - {config.config?.format === "pfx" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.pfxPassword = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.pfxPassword}
-
- ) : ( - <> - )} - - {config.config?.format === "jks" ? ( - <> -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.jksAlias = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.jksAlias}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.jksKeypass = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.jksKeypass}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.jksStorepass = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.jksStorepass}
-
- - ) : ( - <> - )} - -
- - -
{errors?.preCommand}
-
- -
- - -
{errors?.command}
-
-
- - ); -}; - -export default DeployToSSH; diff --git a/ui/src/components/certimate/DeployToTencentCDN.tsx b/ui/src/components/certimate/DeployToTencentCDN.tsx deleted file mode 100644 index fff3d9b5..00000000 --- a/ui/src/components/certimate/DeployToTencentCDN.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToTencentCDNParams = { - domain?: string; -}; - -const DeployToTencentCDN = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: {}, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
-
- ); -}; - -export default DeployToTencentCDN; diff --git a/ui/src/components/certimate/DeployToTencentCLB.tsx b/ui/src/components/certimate/DeployToTencentCLB.tsx deleted file mode 100644 index 91bd5317..00000000 --- a/ui/src/components/certimate/DeployToTencentCLB.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToTencentCLBParams = { - region?: string; - resourceType?: string; - loadbalancerId?: string; - listenerId?: string; - domain?: string; -}; - -const DeployToTencentCLB = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - region: "ap-guangzhou", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z - .object({ - region: z.string().min(1, t("domain.deployment.form.tencent_clb_region.placeholder")), - resourceType: z.union([z.literal("ssl-deploy"), z.literal("loadbalancer"), z.literal("listener"), z.literal("ruledomain")], { - message: t("domain.deployment.form.tencent_clb_resource_type.placeholder"), - }), - loadbalancerId: z.string().min(1, t("domain.deployment.form.tencent_clb_loadbalancer_id.placeholder")), - listenerId: z.string().optional(), - domain: z.string().optional(), - }) - .refine( - (data) => { - switch (data.resourceType) { - case "ssl-deploy": - case "listener": - case "ruledomain": - return !!data.listenerId?.trim(); - } - return true; - }, - { - message: t("domain.deployment.form.tencent_clb_listener_id.placeholder"), - path: ["listenerId"], - } - ) - .refine( - (data) => { - switch (data.resourceType) { - case "ssl-deploy": - case "ruledomain": - return !!data.domain?.trim() && /^$|^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/.test(data.domain); - } - return true; - }, - { - message: t("domain.deployment.form.tencent_clb_ruledomain.placeholder"), - path: ["domain"], - } - ); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, - resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message, - loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message, - listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.region = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.region}
-
- -
- - -
{errors?.resourceType}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.loadbalancerId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.loadbalancerId}
-
- - {config?.config?.resourceType === "ssl-deploy" || config?.config?.resourceType === "listener" || config?.config?.resourceType === "ruledomain" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.listenerId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.listenerId}
-
- ) : ( - <> - )} - - {config?.config?.resourceType === "ssl-deploy" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
- ) : ( - <> - )} - - {config?.config?.resourceType === "ruledomain" ? ( -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
- ) : ( - <> - )} -
- ); -}; - -export default DeployToTencentCLB; diff --git a/ui/src/components/certimate/DeployToTencentCOS.tsx b/ui/src/components/certimate/DeployToTencentCOS.tsx deleted file mode 100644 index b045fb22..00000000 --- a/ui/src/components/certimate/DeployToTencentCOS.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToTencentCOSParams = { - region?: string; - bucket?: string; - domain?: string; -}; - -const DeployToTencentCOS = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: { - region: "ap-guangzhou", - }, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - region: z.string().min(1, t("domain.deployment.form.tencent_cos_region.placeholder")), - bucket: z.string().min(1, t("domain.deployment.form.tencent_cos_bucket.placeholder")), - domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, - bucket: res.error?.errors?.find((e) => e.path[0] === "bucket")?.message, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.region = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.region}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.bucket = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.bucket}
-
- -
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.domain = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.domain}
-
-
- ); -}; - -export default DeployToTencentCOS; diff --git a/ui/src/components/certimate/DeployToTencentTEO.tsx b/ui/src/components/certimate/DeployToTencentTEO.tsx deleted file mode 100644 index 56de4d68..00000000 --- a/ui/src/components/certimate/DeployToTencentTEO.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; - -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { useDeployEditContext } from "./DeployEdit"; - -type DeployToTencentTEOParams = { - zoneId?: string; - domain?: string; -}; - -const DeployToTencentTEO = () => { - const { t } = useTranslation(); - - const { config, setConfig, errors, setErrors } = useDeployEditContext(); - - useEffect(() => { - if (!config.id) { - setConfig({ - ...config, - config: {}, - }); - } - }, []); - - useEffect(() => { - setErrors({}); - }, []); - - const formSchema = z.object({ - zoneId: z.string().min(1, t("domain.deployment.form.tencent_teo_zone_id.placeholder")), - domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }), - }); - - useEffect(() => { - const res = formSchema.safeParse(config.config); - setErrors({ - ...errors, - zoneId: res.error?.errors?.find((e) => e.path[0] === "zoneId")?.message, - domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, - }); - }, [config]); - - return ( -
-
- - { - const nv = produce(config, (draft) => { - draft.config ??= {}; - draft.config.zoneId = e.target.value?.trim(); - }); - setConfig(nv); - }} - /> -
{errors?.zoneId}
-
- -
- -