details improvement and unnecessary files deletion

This commit is contained in:
yoan 2024-11-24 13:36:17 +08:00
parent 37df882ed3
commit 9ff3e22c80
57 changed files with 978 additions and 5047 deletions

View File

@ -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,
}
}

View File

@ -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 {

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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"`

View File

@ -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())
}

View File

@ -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 &notifyMessage{
Subject: subject,
Message: message,
}
}

View File

@ -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
}

View File

@ -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"),

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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

View File

@ -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
}

View File

@ -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 == "" {

View File

@ -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),

View File

@ -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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
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<typeof formSchema>, {
type: "manual",
message: value.message,
});
});
}
};
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild className={cn(className)}>
{trigger}
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
<DialogHeader>
<DialogTitle>{t("access.group.add")}</DialogTitle>
</DialogHeader>
<div className="container py-3">
<Form {...form}>
<form
onSubmit={(e) => {
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.group.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.group.form.name.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
);
};
export default AccessGroupEdit;

View File

@ -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 (
<div className="mt-10">
<Show when={accessGroups.length == 0}>
<>
<div className="flex flex-col items-center mt-10">
<span className="bg-orange-100 p-5 rounded-full">
<Group size={40} className="text-primary" />
</span>
<div className="text-center text-sm text-muted-foreground mt-3">{t("access.group.domains.nodata")}</div>
<AccessGroupEdit trigger={<Button>{t("access.group.add")}</Button>} className="mt-3" />
</div>
</>
</Show>
<ScrollArea className="h-[75vh] overflow-hidden">
<div className="flex gap-5 flex-wrap">
{accessGroups.map((accessGroup) => (
<Card className="w-full md:w-[350px]">
<CardHeader>
<CardTitle>{accessGroup.name}</CardTitle>
<CardDescription>
{t("access.group.total", {
total: accessGroup.expand ? accessGroup.expand.access.length : 0,
})}
</CardDescription>
</CardHeader>
<CardContent className="min-h-[180px]">
{accessGroup.expand ? (
<>
{accessGroup.expand.access.slice(0, 3).map((access) => (
<div key={access.id} className="flex flex-col mb-3">
<div className="flex items-center">
<div className="">
<img src={accessProvidersMap.get(access.configType)!.icon} alt="provider" className="w-8 h-8"></img>
</div>
<div className="ml-3">
<div className="text-sm font-semibold text-gray-700 dark:text-gray-200">{access.name}</div>
<div className="text-xs text-muted-foreground">{accessProvidersMap.get(access.configType)!.name}</div>
</div>
</div>
</div>
))}
</>
) : (
<>
<div className="flex text-gray-700 dark:text-gray-200 items-center">
<div>
<Group size={40} />
</div>
<div className="ml-2">{t("access.group.nodata")}</div>
</div>
</>
)}
</CardContent>
<CardFooter>
<div className="flex justify-end w-full">
<Show when={accessGroup.expand && accessGroup.expand.access.length > 0 ? true : false}>
<div>
<Button
size="sm"
variant={"link"}
onClick={() => {
navigate(`/access?accessGroupId=${accessGroup.id}&tab=access`, {
replace: true,
});
}}
>
{t("access.group.domains")}
</Button>
</div>
</Show>
<Show when={!accessGroup.expand || accessGroup.expand.access.length == 0 ? true : false}>
<div>
<Button size="sm" onClick={handleAddAccess}>
{t("access.authorization.add")}
</Button>
</div>
</Show>
<div className="ml-3">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"destructive"} size={"sm"}>
{t("common.delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="dark:text-gray-200">{t("access.group.delete")}</AlertDialogTitle>
<AlertDialogDescription>{t("access.group.delete.confirm")}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="dark:text-gray-200">{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleRemoveClick(accessGroup.id ? accessGroup.id : "");
}}
>
{t("common.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</CardFooter>
</Card>
))}
</div>
</ScrollArea>
</div>
);
};
export default AccessGroupList;

View File

@ -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<HTMLInputElement | null>(null);
@ -216,52 +207,6 @@ const AccessSSHForm = ({ data, op, onAfterReq }: AccessSSHFormProps) => {
)}
/>
<FormField
control={form.control}
name="group"
render={({ field }) => (
<FormItem>
<FormLabel className="w-full flex justify-between">
<div>{t("access.authorization.form.ssh_group.label")}</div>
<AccessGroupEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
defaultValue="emptyId"
onValueChange={(value) => {
form.setValue("group", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("access.authorization.form.access_group.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="emptyId">
<div className={cn("flex items-center space-x-2 rounded cursor-pointer")}>--</div>
</SelectItem>
{accessGroups.map((item) => (
<SelectItem value={item.id ? item.id : ""} key={item.id}>
<div className={cn("flex items-center space-x-2 rounded cursor-pointer")}>{item.name}</div>
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"

View File

@ -1,17 +0,0 @@
import { createContext, useContext, type Context as ReactContext } from "react";
import { type DeployConfig } from "@/domain/domain";
export type DeployEditContext<T extends DeployConfig["config"] = DeployConfig["config"]> = {
config: Omit<DeployConfig, "config"> & { config: T };
setConfig: (config: Omit<DeployConfig, "config"> & { config: T }) => void;
errors: { [K in keyof T]?: string };
setErrors: (error: { [K in keyof T]?: string }) => void;
};
export const Context = createContext<DeployEditContext>({} as DeployEditContext);
export function useDeployEditContext<T extends DeployConfig["config"] = DeployConfig["config"]>() {
return useContext<DeployEditContext<T>>(Context as unknown as ReactContext<DeployEditContext<T>>);
}

View File

@ -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<DeployConfig>({
access: "",
type: "",
});
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
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 = <DeployToAliyunOSS />;
break;
case "aliyun-cdn":
case "aliyun-dcdn":
childComponent = <DeployToAliyunCDN />;
break;
case "aliyun-clb":
childComponent = <DeployToAliyunCLB />;
break;
case "aliyun-alb":
childComponent = <DeployToAliyunALB />;
break;
case "aliyun-nlb":
childComponent = <DeployToAliyunNLB />;
break;
case "tencent-cdn":
case "tencent-ecdn":
childComponent = <DeployToTencentCDN />;
break;
case "tencent-clb":
childComponent = <DeployToTencentCLB />;
break;
case "tencent-cos":
childComponent = <DeployToTencentCOS />;
break;
case "tencent-teo":
childComponent = <DeployToTencentTEO />;
break;
case "huaweicloud-cdn":
childComponent = <DeployToHuaweiCloudCDN />;
break;
case "huaweicloud-elb":
childComponent = <DeployToHuaweiCloudELB />;
break;
case "baiducloud-cdn":
childComponent = <DeployToBaiduCloudCDN />;
break;
case "qiniu-cdn":
childComponent = <DeployToQiniuCDN />;
break;
case "dogecloud-cdn":
childComponent = <DeployToDogeCloudCDN />;
break;
case "local":
childComponent = <DeployToLocal />;
break;
case "ssh":
childComponent = <DeployToSSH />;
break;
case "webhook":
childComponent = <DeployToWebhook />;
break;
case "k8s-secret":
childComponent = <DeployToKubernetesSecret />;
break;
case "volcengine-live":
childComponent = <DeployToVolcengineLive />;
break;
case "volcengine-cdn":
childComponent = <DeployToVolcengineCDN />;
break;
case "byteplus-cdn":
childComponent = <DeployToByteplusCDN />;
break;
}
return (
<DeployEditContext.Provider
value={{
config: locDeployConfig as DeployEditContextType["config"],
setConfig: setConfig as DeployEditContextType["setConfig"],
errors: errors as DeployEditContextType["errors"],
setErrors: setErrors as DeployEditContextType["setErrors"],
}}
>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent
className="dark:text-stone-200"
onInteractOutside={(event) => {
event.preventDefault();
}}
>
<DialogHeader>
<DialogTitle>{t("domain.deployment.tab")}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[80vh]">
<div className="container py-3">
{/* 部署方式 */}
<div>
<Label>{t("domain.deployment.form.type.label")}</Label>
<Select
value={locDeployConfig.type}
onValueChange={(val: string) => {
setConfig({ ...locDeployConfig, type: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder={t("domain.deployment.form.type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.deployment.form.type.list")}</SelectLabel>
{Array.from(deployTargetsMap.entries()).map(([key, target]) => (
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
<img className="w-6" src={target.icon} />
<div>{t(target.name)}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{errors.type}</div>
</div>
{/* 授权配置 */}
<div className="mt-8">
<Label className="flex justify-between">
<div>{t("domain.deployment.form.access.label")}</div>
<AccessEditDialog
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
/>
</Label>
<Select
value={locDeployConfig.access}
onValueChange={(val: string) => {
setConfig({ ...locDeployConfig, access: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder={t("domain.deployment.form.access.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.deployment.form.access.list")}</SelectLabel>
{targetAccesses.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img className="w-6" src={accessProvidersMap.get(item.configType)?.icon} />
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{errors.access}</div>
</div>
{/* 其他参数 */}
<div className="mt-8">{childComponent}</div>
</div>
</ScrollArea>
<DialogFooter>
<Button
onClick={(e) => {
e.stopPropagation();
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DeployEditContext.Provider>
);
};
export default DeployEditDialog;

View File

@ -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 (
<div className="flex justify-between text-sm p-3 items-center text-stone-700 dark:text-stone-200">
<div className="flex space-x-2 items-center">
<div>
<img src={getTypeIcon()} className="w-9"></img>
</div>
<div className="text-stone-600 flex-col flex space-y-0 dark:text-stone-200">
<div>{getTypeName()}</div>
<div>{access?.name}</div>
</div>
</div>
<div className="flex space-x-2">
<DeployEditDialog
trigger={<EditIcon size={16} className="cursor-pointer" />}
deployConfig={item}
onSave={(deploy: DeployConfig) => {
onSave(deploy);
}}
/>
<Trash2
size={16}
className="cursor-pointer"
onClick={() => {
onDelete();
}}
/>
</div>
</div>
);
};
type DeployListProps = {
deploys: DeployConfig[];
onChange: (deploys: DeployConfig[]) => void;
};
const DeployList = ({ deploys, onChange }: DeployListProps) => {
const [list, setList] = useState<DeployConfig[]>([]);
const { t } = useTranslation();
useEffect(() => {
setList(deploys);
}, [deploys]);
const handleAdd = (deploy: DeployConfig) => {
deploy.id = nanoid();
const newList = [...list, deploy];
setList(newList);
onChange(newList);
};
const handleDelete = (id: string) => {
const newList = list.filter((item) => item.id !== id);
setList(newList);
onChange(newList);
};
const handleSave = (deploy: DeployConfig) => {
const newList = list.map((item) => {
if (item.id === deploy.id) {
return { ...deploy };
}
return item;
});
setList(newList);
onChange(newList);
};
return (
<>
<Show
when={list.length > 0}
fallback={
<Alert className="w-full border dark:border-stone-400">
<AlertDescription className="flex flex-col items-center">
<div>{t("domain.deployment.nodata")}</div>
<div className="flex justify-end mt-2">
<DeployEditDialog
onSave={(config: DeployConfig) => {
handleAdd(config);
}}
trigger={<Button size={"sm"}>{t("common.add")}</Button>}
/>
</div>
</AlertDescription>
</Alert>
}
>
<div className="flex justify-end py-2 border-b dark:border-stone-400">
<DeployEditDialog
trigger={<Button size={"sm"}>{t("common.add")}</Button>}
onSave={(config: DeployConfig) => {
handleAdd(config);
}}
/>
</div>
<div className="w-full md:w-[35em] rounded mt-5 border dark:border-stone-400 dark:text-stone-200">
<div className="">
{list.map((item) => (
<DeployItem
key={item.id}
item={item}
onDelete={() => {
handleDelete(item.id ?? "");
}}
onSave={(deploy: DeployConfig) => {
handleSave(deploy);
}}
/>
))}
</div>
</div>
</Show>
</>
);
};
export default DeployList;

View File

@ -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 (
<div className="flex items-center">
<div className={cn("text-xs text-nowrap", step === 1 ? (phaseSuccess ? "text-green-600" : "text-red-600") : "", step > 1 ? "text-green-600" : "")}>
{t("history.props.stage.progress.check")}
</div>
<Separator className={cn("h-1 grow max-w-[60px]", step > 1 ? "bg-green-600" : "")} />
<div
className={cn(
"text-xs text-nowrap",
step < 2 ? "text-muted-foreground" : "",
step === 2 ? (phaseSuccess ? "text-green-600" : "text-red-600") : "",
step > 2 ? "text-green-600" : ""
)}
>
{t("history.props.stage.progress.apply")}
</div>
<Separator className={cn("h-1 grow max-w-[60px]", step > 2 ? "bg-green-600" : "")} />
<div
className={cn(
"text-xs text-nowrap",
step < 3 ? "text-muted-foreground" : "",
step === 3 ? (phaseSuccess ? "text-green-600" : "text-red-600") : "",
step > 3 ? "text-green-600" : ""
)}
>
{t("history.props.stage.progress.deploy")}
</div>
</div>
);
};
export default DeployProgress;

View File

@ -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 ? (
<CircleCheck size={16} className="text-green-700" />
) : (
<>
{error(deployment.phase).length ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild className="cursor-pointer">
<CircleX size={16} className="text-red-700" />
</TooltipTrigger>
<TooltipContent className="max-w-[35em]">{error(deployment.phase)}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<CircleX size={16} className="text-red-700" />
)}
</>
)}
</>
);
};
export default DeployState;

View File

@ -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<DeployToAliyunALBConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.aliyun_alb_region.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_alb_region.placeholder")}
className="w-full mt-1"
value={config?.config?.region}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.aliyun_alb_resource_type.label")}</Label>
<Select
value={config?.config?.resourceType}
onValueChange={(value) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.resourceType = value?.trim();
});
setConfig(nv);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.deployment.form.aliyun_alb_resource_type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="loadbalancer">{t("domain.deployment.form.aliyun_alb_resource_type.option.loadbalancer.label")}</SelectItem>
<SelectItem value="listener">{t("domain.deployment.form.aliyun_alb_resource_type.option.listener.label")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{errors?.resourceType}</div>
</div>
{config?.config?.resourceType === "loadbalancer" ? (
<div>
<Label>{t("domain.deployment.form.aliyun_alb_loadbalancer_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_alb_loadbalancer_id.placeholder")}
className="w-full mt-1"
value={config?.config?.loadbalancerId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.loadbalancerId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.loadbalancerId}</div>
</div>
) : (
<></>
)}
{config?.config?.resourceType === "listener" ? (
<div>
<Label>{t("domain.deployment.form.aliyun_alb_listener_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_alb_listener_id.placeholder")}
className="w-full mt-1"
value={config?.config?.listenerId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.listenerId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.listenerId}</div>
</div>
) : (
<></>
)}
</div>
);
};
export default DeployToAliyunALB;

View File

@ -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<DeployToAliyunCDNConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToAliyunCDN;

View File

@ -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<DeployToAliyunCLBConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.aliyun_clb_region.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_clb_region.placeholder")}
className="w-full mt-1"
value={config?.config?.region}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.aliyun_clb_resource_type.label")}</Label>
<Select
value={config?.config?.resourceType}
onValueChange={(value) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.resourceType = value;
});
setConfig(nv);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.deployment.form.aliyun_clb_resource_type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="loadbalancer">{t("domain.deployment.form.aliyun_clb_resource_type.option.loadbalancer.label")}</SelectItem>
<SelectItem value="listener">{t("domain.deployment.form.aliyun_clb_resource_type.option.listener.label")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{errors?.resourceType}</div>
</div>
<div>
<Label>{t("domain.deployment.form.aliyun_clb_loadbalancer_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder")}
className="w-full mt-1"
value={config?.config?.loadbalancerId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.loadbalancerId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.loadbalancerId}</div>
</div>
{config?.config?.resourceType === "listener" ? (
<div>
<Label>{t("domain.deployment.form.aliyun_clb_listener_port.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_clb_listener_port.placeholder")}
className="w-full mt-1"
value={config?.config?.listenerPort}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.listenerPort = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.listenerPort}</div>
</div>
) : (
<></>
)}
</div>
);
};
export default DeployToAliyunCLB;

View File

@ -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<DeployToAliyunNLBConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.aliyun_nlb_region.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_nlb_region.placeholder")}
className="w-full mt-1"
value={config?.config?.region}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.aliyun_nlb_resource_type.label")}</Label>
<Select
value={config?.config?.resourceType}
onValueChange={(value) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.resourceType = value;
});
setConfig(nv);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.deployment.form.aliyun_nlb_resource_type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="loadbalancer">{t("domain.deployment.form.aliyun_nlb_resource_type.option.loadbalancer.label")}</SelectItem>
<SelectItem value="listener">{t("domain.deployment.form.aliyun_nlb_resource_type.option.listener.label")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{errors?.resourceType}</div>
</div>
{config?.config?.resourceType === "loadbalancer" ? (
<div>
<Label>{t("domain.deployment.form.aliyun_nlb_loadbalancer_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_nlb_loadbalancer_id.placeholder")}
className="w-full mt-1"
value={config?.config?.loadbalancerId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.loadbalancerId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.loadbalancerId}</div>
</div>
) : (
<></>
)}
{config?.config?.resourceType === "listener" ? (
<div>
<Label>{t("domain.deployment.form.aliyun_nlb_listener_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_nlb_listener_id.placeholder")}
className="w-full mt-1"
value={config?.config?.listenerId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.listenerId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.listenerId}</div>
</div>
) : (
<></>
)}
</div>
);
};
export default DeployToAliyunNLB;

View File

@ -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<DeployToAliyunOSSConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.aliyun_oss_endpoint.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_oss_endpoint.placeholder")}
className="w-full mt-1"
value={config?.config?.endpoint}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.endpoint = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.endpoint}</div>
</div>
<div>
<Label>{t("domain.deployment.form.aliyun_oss_bucket.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_oss_bucket.placeholder")}
className="w-full mt-1"
value={config?.config?.bucket}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.bucket = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.bucket}</div>
</div>
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.label")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToAliyunOSS;

View File

@ -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<DeployToBaiduCloudCDNConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToBaiduCloudCDN;

View File

@ -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<DeployToByteplusCDNConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label.wildsupported")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToByteplusCDN;

View File

@ -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<DeployToDogeCloudCDNConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToDogeCloudCDN;

View File

@ -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<DeployToHuaweiCloudCDNConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.huaweicloud_cdn_region.label")}</Label>
<Input
placeholder={t("domain.deployment.form.huaweicloud_cdn_region.placeholder")}
className="w-full mt-1"
value={config?.config?.region}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToHuaweiCloudCDN;

View File

@ -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<DeployToHuaweiCloudELBConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.huaweicloud_elb_region.label")}</Label>
<Input
placeholder={t("domain.deployment.form.huaweicloud_elb_region.placeholder")}
className="w-full mt-1"
value={config?.config?.region}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.huaweicloud_elb_resource_type.label")}</Label>
<Select
value={config?.config?.resourceType}
onValueChange={(value) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.resourceType = value;
});
setConfig(nv);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.deployment.form.huaweicloud_elb_resource_type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="certificate">{t("domain.deployment.form.huaweicloud_elb_resource_type.option.certificate.label")}</SelectItem>
<SelectItem value="loadbalancer">{t("domain.deployment.form.huaweicloud_elb_resource_type.option.loadbalancer.label")}</SelectItem>
<SelectItem value="listener">{t("domain.deployment.form.huaweicloud_elb_resource_type.option.listener.label")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{errors?.resourceType}</div>
</div>
{config?.config?.resourceType === "certificate" ? (
<div>
<Label>{t("domain.deployment.form.huaweicloud_elb_certificate_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.huaweicloud_elb_certificate_id.placeholder")}
className="w-full mt-1"
value={config?.config?.certificateId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.certificateId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.certificateId}</div>
</div>
) : (
<></>
)}
{config?.config?.resourceType === "loadbalancer" ? (
<div>
<Label>{t("domain.deployment.form.huaweicloud_elb_loadbalancer_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder")}
className="w-full mt-1"
value={config?.config?.loadbalancerId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.loadbalancerId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.loadbalancerId}</div>
</div>
) : (
<></>
)}
{config?.config?.resourceType === "listener" ? (
<div>
<Label>{t("domain.deployment.form.huaweicloud_elb_listener_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.huaweicloud_elb_listener_id.placeholder")}
className="w-full mt-1"
value={config?.config?.listenerId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.listenerId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.listenerId}</div>
</div>
) : (
<></>
)}
</div>
);
};
export default DeployToHuaweiCloudELB;

View File

@ -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<DeployToKubernetesSecretConfigParams>();
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 (
<>
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.k8s_namespace.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_namespace.label")}
className="w-full mt-1"
value={config?.config?.namespace}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.namespace = e.target.value?.trim();
});
setConfig(nv);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.k8s_secret_name.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_secret_name.label")}
className="w-full mt-1"
value={config?.config?.secretName}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.secretName = e.target.value?.trim();
});
setConfig(nv);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.k8s_secret_data_key_for_crt.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_secret_data_key_for_crt.label")}
className="w-full mt-1"
value={config?.config?.secretDataKeyForCrt}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.secretDataKeyForCrt = e.target.value?.trim();
});
setConfig(nv);
}}
/>
</div>
<div>
<Label>{t("domain.deployment.form.k8s_secret_data_key_for_key.label")}</Label>
<Input
placeholder={t("domain.deployment.form.k8s_secret_data_key_for_key.label")}
className="w-full mt-1"
value={config?.config?.secretDataKeyForKey}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.secretDataKeyForKey = e.target.value?.trim();
});
setConfig(nv);
}}
/>
</div>
</div>
</>
);
};
export default DeployToKubernetesSecret;

View File

@ -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<DeployToLocalConfigParams>();
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 = "<your-pfx-path>" # PFX
$pfxPassword = "<your-pfx-password>" # PFX
$siteName = "<your-site-name>" # IIS
$domain = "<your-domain-name>" #
$ipaddr = "<your-binding-ip>" # IP* IP
$port = "<your-binding-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 = "<your-pfx-path>" # PFX
$pfxPassword = "<your-pfx-password>" # PFX
$ipaddr = "<your-binding-ip>" # IP0.0.0.0 IP
$port = "<your-binding-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 (
<>
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.file_format.label")}</Label>
<Select
value={config?.config?.format}
onValueChange={(value) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.format = value;
});
setConfig(nv);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.deployment.form.file_format.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="pem">PEM</SelectItem>
<SelectItem value="pfx">PFX</SelectItem>
<SelectItem value="jks">JKS</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{errors?.format}</div>
</div>
<div>
<Label>{t("domain.deployment.form.file_cert_path.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_cert_path.label")}
className="w-full mt-1"
value={config?.config?.certPath}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.certPath = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.certPath}</div>
</div>
{config.config?.format === "pem" ? (
<div>
<Label>{t("domain.deployment.form.file_key_path.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_key_path.placeholder")}
className="w-full mt-1"
value={config?.config?.keyPath}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.keyPath = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.keyPath}</div>
</div>
) : (
<></>
)}
{config.config?.format === "pfx" ? (
<div>
<Label>{t("domain.deployment.form.file_pfx_password.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_pfx_password.placeholder")}
className="w-full mt-1"
value={config?.config?.pfxPassword}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.pfxPassword = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.pfxPassword}</div>
</div>
) : (
<></>
)}
{config.config?.format === "jks" ? (
<>
<div>
<Label>{t("domain.deployment.form.file_jks_alias.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_alias.placeholder")}
className="w-full mt-1"
value={config?.config?.jksAlias}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksAlias = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.jksAlias}</div>
</div>
<div>
<Label>{t("domain.deployment.form.file_jks_keypass.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_keypass.placeholder")}
className="w-full mt-1"
value={config?.config?.jksKeypass}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksKeypass = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.jksKeypass}</div>
</div>
<div>
<Label>{t("domain.deployment.form.file_jks_storepass.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_storepass.placeholder")}
className="w-full mt-1"
value={config?.config?.jksStorepass}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksStorepass = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.jksStorepass}</div>
</div>
</>
) : (
<></>
)}
<div>
<Label>{t("domain.deployment.form.shell.label")}</Label>
<RadioGroup
className="flex mt-1"
value={config?.config?.shell}
onValueChange={(val) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.shell = val;
});
setConfig(nv);
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="sh" id="shellOptionSh" />
<Label htmlFor="shellOptionSh">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("sh"))}>
<div>POSIX Bash (Linux)</div>
</div>
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="cmd" id="shellOptionCmd" />
<Label htmlFor="shellOptionCmd">
<div className={cn("border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("cmd"))}>
<div>CMD (Windows)</div>
</div>
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="powershell" id="shellOptionPowerShell" />
<Label htmlFor="shellOptionPowerShell">
<div className={cn("border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("powershell"))}>
<div>PowerShell (Windows)</div>
</div>
</Label>
</div>
</RadioGroup>
<div className="text-red-600 text-sm mt-1">{errors?.shell}</div>
</div>
<div>
<Label>{t("domain.deployment.form.shell_pre_command.label")}</Label>
<Textarea
className="mt-1"
value={config?.config?.preCommand}
placeholder={t("domain.deployment.form.shell_pre_command.placeholder")}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.preCommand = e.target.value;
});
setConfig(nv);
}}
></Textarea>
<div className="text-red-600 text-sm mt-1">{errors?.preCommand}</div>
</div>
<div>
<div className="flex items-center justify-between">
<Label>{t("domain.deployment.form.shell_command.label")}</Label>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<a className="text-xs text-blue-500 cursor-pointer">{t("domain.deployment.form.shell_preset_scripts.trigger")}</a>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => handleUsePresetScript("reload_nginx")}>
{t("domain.deployment.form.shell_preset_scripts.option.reload_nginx.label")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleUsePresetScript("binding_iis")}>
{t("domain.deployment.form.shell_preset_scripts.option.binding_iis.label")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleUsePresetScript("binding_netsh")}>
{t("domain.deployment.form.shell_preset_scripts.option.binding_netsh.label")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Textarea
className="mt-1"
value={config?.config?.command}
placeholder={t("domain.deployment.form.shell_command.placeholder")}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.command = e.target.value;
});
setConfig(nv);
}}
></Textarea>
<div className="text-red-600 text-sm mt-1">{errors?.command}</div>
</div>
</div>
</>
);
};
export default DeployToLocal;

View File

@ -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<DeployToQiniuCDNConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label.wildsupported")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToQiniuCDN;

View File

@ -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<DeployToSSHConfigParams>();
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 (
<>
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.file_format.label")}</Label>
<Select
value={config?.config?.format}
onValueChange={(value) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.format = value;
});
setConfig(nv);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.deployment.form.file_format.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="pem">PEM</SelectItem>
<SelectItem value="pfx">PFX</SelectItem>
<SelectItem value="jks">JKS</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{errors?.format}</div>
</div>
<div>
<Label>{t("domain.deployment.form.file_cert_path.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_cert_path.label")}
className="w-full mt-1"
value={config?.config?.certPath}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.certPath = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.certPath}</div>
</div>
{config.config?.format === "pem" ? (
<div>
<Label>{t("domain.deployment.form.file_key_path.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_key_path.placeholder")}
className="w-full mt-1"
value={config?.config?.keyPath}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.keyPath = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.keyPath}</div>
</div>
) : (
<></>
)}
{config.config?.format === "pfx" ? (
<div>
<Label>{t("domain.deployment.form.file_pfx_password.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_pfx_password.placeholder")}
className="w-full mt-1"
value={config?.config?.pfxPassword}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.pfxPassword = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.pfxPassword}</div>
</div>
) : (
<></>
)}
{config.config?.format === "jks" ? (
<>
<div>
<Label>{t("domain.deployment.form.file_jks_alias.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_alias.placeholder")}
className="w-full mt-1"
value={config?.config?.jksAlias}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksAlias = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.jksAlias}</div>
</div>
<div>
<Label>{t("domain.deployment.form.file_jks_keypass.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_keypass.placeholder")}
className="w-full mt-1"
value={config?.config?.jksKeypass}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksKeypass = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.jksKeypass}</div>
</div>
<div>
<Label>{t("domain.deployment.form.file_jks_storepass.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_storepass.placeholder")}
className="w-full mt-1"
value={config?.config?.jksStorepass}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksStorepass = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.jksStorepass}</div>
</div>
</>
) : (
<></>
)}
<div>
<Label>{t("domain.deployment.form.shell_pre_command.label")}</Label>
<Textarea
className="mt-1"
value={config?.config?.preCommand}
placeholder={t("domain.deployment.form.shell_pre_command.placeholder")}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.preCommand = e.target.value;
});
setConfig(nv);
}}
></Textarea>
<div className="text-red-600 text-sm mt-1">{errors?.preCommand}</div>
</div>
<div>
<Label>{t("domain.deployment.form.shell_command.label")}</Label>
<Textarea
className="mt-1"
value={config?.config?.command}
placeholder={t("domain.deployment.form.shell_command.placeholder")}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.command = e.target.value;
});
setConfig(nv);
}}
></Textarea>
<div className="text-red-600 text-sm mt-1">{errors?.command}</div>
</div>
</div>
</>
);
};
export default DeployToSSH;

View File

@ -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<DeployToTencentCDNParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label.wildsupported")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToTencentCDN;

View File

@ -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<DeployToTencentCLBParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.tencent_clb_region.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_region.placeholder")}
className="w-full mt-1"
value={config?.config?.region}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.tencent_clb_resource_type.label")}</Label>
<Select
value={config?.config?.resourceType}
onValueChange={(value) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.resourceType = value;
});
setConfig(nv);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.deployment.form.tencent_clb_resource_type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="ssl-deploy">{t("domain.deployment.form.tencent_clb_resource_type.option.ssl_deploy.label")}</SelectItem>
<SelectItem value="loadbalancer">{t("domain.deployment.form.tencent_clb_resource_type.option.loadbalancer.label")}</SelectItem>
<SelectItem value="listener">{t("domain.deployment.form.tencent_clb_resource_type.option.listener.label")}</SelectItem>
<SelectItem value="ruledomain">{t("domain.deployment.form.tencent_clb_resource_type.option.ruledomain.label")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{errors?.resourceType}</div>
</div>
<div>
<Label>{t("domain.deployment.form.tencent_clb_loadbalancer_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_loadbalancer_id.placeholder")}
className="w-full mt-1"
value={config?.config?.loadbalancerId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.loadbalancerId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.loadbalancerId}</div>
</div>
{config?.config?.resourceType === "ssl-deploy" || config?.config?.resourceType === "listener" || config?.config?.resourceType === "ruledomain" ? (
<div>
<Label>{t("domain.deployment.form.tencent_clb_listener_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_listener_id.placeholder")}
className="w-full mt-1"
value={config?.config?.listenerId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.listenerId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.listenerId}</div>
</div>
) : (
<></>
)}
{config?.config?.resourceType === "ssl-deploy" ? (
<div>
<Label>{t("domain.deployment.form.tencent_clb_domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
) : (
<></>
)}
{config?.config?.resourceType === "ruledomain" ? (
<div>
<Label>{t("domain.deployment.form.tencent_clb_ruledomain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_ruledomain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
) : (
<></>
)}
</div>
);
};
export default DeployToTencentCLB;

View File

@ -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<DeployToTencentCOSParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.tencent_cos_region.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_cos_region.placeholder")}
className="w-full mt-1"
value={config?.config?.region}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.tencent_cos_bucket.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_cos_bucket.placeholder")}
className="w-full mt-1"
value={config?.config?.bucket}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.bucket = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.bucket}</div>
</div>
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToTencentCOS;

View File

@ -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<DeployToTencentTEOParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.tencent_teo_zone_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_teo_zone_id.placeholder")}
className="w-full mt-1"
value={config?.config?.zoneId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.zoneId = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.zoneId}</div>
</div>
<div>
<Label>{t("domain.deployment.form.tencent_teo_domain.label")}</Label>
<Textarea
placeholder={t("domain.deployment.form.tencent_teo_domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToTencentTEO;

View File

@ -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 DeployToVolcengineCDNConfigParams = {
domain?: string;
};
const DeployToVolcengineCDN = () => {
const { t } = useTranslation();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToVolcengineCDNConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label.wildsupported")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToVolcengineCDN;

View File

@ -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 DeployToVolcengineLiveConfigParams = {
domain?: string;
};
const DeployToVolcengineLive = () => {
const { t } = useTranslation();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToVolcengineLiveConfigParams>();
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 (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label.wildsupported")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToVolcengineLive;

View File

@ -1,40 +0,0 @@
import { useEffect } from "react";
import { produce } from "immer";
import { useDeployEditContext } from "./DeployEdit";
import KVList from "./KVList";
import { type KVType } from "@/domain/domain";
const DeployToWebhook = () => {
const { config, setConfig, setErrors } = useDeployEditContext();
useEffect(() => {
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
setErrors({});
}, []);
return (
<>
<KVList
variables={config?.config?.variables}
onValueChange={(variables: KVType[]) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.variables = variables;
});
setConfig(nv);
}}
/>
</>
);
};
export default DeployToWebhook;

View File

@ -18,6 +18,9 @@ import DeployToTencentCOS from "./DeployToTencentCOS";
import DeployToTencentTEO from "./DeployToTencentTEO";
import DeployToSSH from "./DeployToSSH";
import DeployToLocal from "./DeployToLocal";
import DeployToByteplusCDN from "./DeployToByteplusCDN";
import DeployToVolcengineCDN from "./DeployToVolcengineCDN";
import DeployToVolcengineLive from "./DeployToVolcengineLive";
export type DeployFormProps = {
data: WorkflowNode;
@ -70,6 +73,12 @@ const getForm = (data: WorkflowNode, defaultProivder?: string) => {
return <DeployToSSH data={data} />;
case "local":
return <DeployToLocal data={data} />;
case "byteplus-cdn":
return <DeployToByteplusCDN data={data} />;
case "volcengine-cdn":
return <DeployToVolcengineCDN data={data} />;
case "volcengine-live":
return <DeployToVolcengineLive data={data} />;
default:
return <></>;
}

View File

@ -32,7 +32,6 @@ const DeployToBaiduCloudCDN = ({ data }: DeployFormProps) => {
useEffect(() => {
const rs = getWorkflowOuptutBeforeId(data.id, "certificate");
console.log(rs);
setBeforeOutput(rs);
}, [data]);

View File

@ -0,0 +1,181 @@
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { Input } from "@/components/ui/input";
import { DeployFormProps } from "./DeployForm";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { WorkflowNode, WorkflowNodeConfig } from "@/domain/workflow";
import { useWorkflowStore, WorkflowState } from "@/providers/workflow";
import { useShallow } from "zustand/shallow";
import { usePanel } from "./PanelProvider";
import { Button } from "../ui/button";
import { useEffect, useState } from "react";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import { SelectLabel } from "@radix-ui/react-select";
import AccessSelect from "./AccessSelect";
import AccessEditDialog from "../certimate/AccessEditDialog";
import { Plus } from "lucide-react";
const selectState = (state: WorkflowState) => ({
updateNode: state.updateNode,
getWorkflowOuptutBeforeId: state.getWorkflowOuptutBeforeId,
});
const DeployToByteplusCDN = ({ data }: DeployFormProps) => {
const { updateNode, getWorkflowOuptutBeforeId } = useWorkflowStore(useShallow(selectState));
const { hidePanel } = usePanel();
const { t } = useTranslation();
const [beforeOutput, setBeforeOutput] = useState<WorkflowNode[]>([]);
useEffect(() => {
const rs = getWorkflowOuptutBeforeId(data.id, "certificate");
setBeforeOutput(rs);
}, [data]);
const formSchema = z.object({
providerType: z.string(),
access: z.string().min(1, t("domain.deployment.form.access.placeholder")),
certificate: z.string().min(1),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
let config: WorkflowNodeConfig = {
certificate: "",
providerType: "byteplus-cdn",
access: "",
domain: "",
};
if (data) config = data.config ?? config;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
providerType: "byteplus-cdn",
access: config.access as string,
certificate: config.certificate as string,
domain: config.domain as string,
},
});
const onSubmit = async (config: z.infer<typeof formSchema>) => {
updateNode({ ...data, config: { ...config }, validated: true });
hidePanel();
};
return (
<>
<Form {...form}>
<form
onSubmit={(e) => {
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="access"
render={({ field }) => (
<FormItem>
<FormLabel className="flex justify-between">
<div>{t("domain.deployment.form.access.label")}</div>
<AccessEditDialog
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
outConfigType="byteplus"
/>
</FormLabel>
<FormControl>
<AccessSelect
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("access", value);
}}
providerType="byteplus-cdn"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="certificate"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workflow.common.certificate.label")}</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("certificate", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("workflow.common.certificate.placeholder")} />
</SelectTrigger>
<SelectContent>
{beforeOutput.map((item) => (
<>
<SelectGroup key={item.id}>
<SelectLabel>{item.name}</SelectLabel>
{item.output?.map((output) => (
<SelectItem key={output.name} value={`${item.id}#${output.name}`}>
<div>
{item.name}-{output.label}
</div>
</SelectItem>
))}
</SelectGroup>
</>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain.deployment.form.domain.label")}</FormLabel>
<FormControl>
<Input placeholder={t("domain.deployment.form.domain.label")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>
</>
);
};
export default DeployToByteplusCDN;

View File

@ -0,0 +1,181 @@
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { Input } from "@/components/ui/input";
import { DeployFormProps } from "./DeployForm";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { WorkflowNode, WorkflowNodeConfig } from "@/domain/workflow";
import { useWorkflowStore, WorkflowState } from "@/providers/workflow";
import { useShallow } from "zustand/shallow";
import { usePanel } from "./PanelProvider";
import { Button } from "../ui/button";
import { useEffect, useState } from "react";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import { SelectLabel } from "@radix-ui/react-select";
import AccessSelect from "./AccessSelect";
import AccessEditDialog from "../certimate/AccessEditDialog";
import { Plus } from "lucide-react";
const selectState = (state: WorkflowState) => ({
updateNode: state.updateNode,
getWorkflowOuptutBeforeId: state.getWorkflowOuptutBeforeId,
});
const DeployToVolcengineCDN = ({ data }: DeployFormProps) => {
const { updateNode, getWorkflowOuptutBeforeId } = useWorkflowStore(useShallow(selectState));
const { hidePanel } = usePanel();
const { t } = useTranslation();
const [beforeOutput, setBeforeOutput] = useState<WorkflowNode[]>([]);
useEffect(() => {
const rs = getWorkflowOuptutBeforeId(data.id, "certificate");
setBeforeOutput(rs);
}, [data]);
const formSchema = z.object({
providerType: z.string(),
access: z.string().min(1, t("domain.deployment.form.access.placeholder")),
certificate: z.string().min(1),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
let config: WorkflowNodeConfig = {
certificate: "",
providerType: "volcengine-cdn",
access: "",
domain: "",
};
if (data) config = data.config ?? config;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
providerType: "volcengine-cdn",
access: config.access as string,
certificate: config.certificate as string,
domain: config.domain as string,
},
});
const onSubmit = async (config: z.infer<typeof formSchema>) => {
updateNode({ ...data, config: { ...config }, validated: true });
hidePanel();
};
return (
<>
<Form {...form}>
<form
onSubmit={(e) => {
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="access"
render={({ field }) => (
<FormItem>
<FormLabel className="flex justify-between">
<div>{t("domain.deployment.form.access.label")}</div>
<AccessEditDialog
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
outConfigType="volcengine"
/>
</FormLabel>
<FormControl>
<AccessSelect
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("access", value);
}}
providerType="volcengine-cdn"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="certificate"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workflow.common.certificate.label")}</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("certificate", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("workflow.common.certificate.placeholder")} />
</SelectTrigger>
<SelectContent>
{beforeOutput.map((item) => (
<>
<SelectGroup key={item.id}>
<SelectLabel>{item.name}</SelectLabel>
{item.output?.map((output) => (
<SelectItem key={output.name} value={`${item.id}#${output.name}`}>
<div>
{item.name}-{output.label}
</div>
</SelectItem>
))}
</SelectGroup>
</>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain.deployment.form.domain.label")}</FormLabel>
<FormControl>
<Input placeholder={t("domain.deployment.form.domain.label")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>
</>
);
};
export default DeployToVolcengineCDN;

View File

@ -0,0 +1,181 @@
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { Input } from "@/components/ui/input";
import { DeployFormProps } from "./DeployForm";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { WorkflowNode, WorkflowNodeConfig } from "@/domain/workflow";
import { useWorkflowStore, WorkflowState } from "@/providers/workflow";
import { useShallow } from "zustand/shallow";
import { usePanel } from "./PanelProvider";
import { Button } from "../ui/button";
import { useEffect, useState } from "react";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import { SelectLabel } from "@radix-ui/react-select";
import AccessSelect from "./AccessSelect";
import AccessEditDialog from "../certimate/AccessEditDialog";
import { Plus } from "lucide-react";
const selectState = (state: WorkflowState) => ({
updateNode: state.updateNode,
getWorkflowOuptutBeforeId: state.getWorkflowOuptutBeforeId,
});
const DeployToVolcengineLive = ({ data }: DeployFormProps) => {
const { updateNode, getWorkflowOuptutBeforeId } = useWorkflowStore(useShallow(selectState));
const { hidePanel } = usePanel();
const { t } = useTranslation();
const [beforeOutput, setBeforeOutput] = useState<WorkflowNode[]>([]);
useEffect(() => {
const rs = getWorkflowOuptutBeforeId(data.id, "certificate");
setBeforeOutput(rs);
}, [data]);
const formSchema = z.object({
providerType: z.string(),
access: z.string().min(1, t("domain.deployment.form.access.placeholder")),
certificate: z.string().min(1),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
let config: WorkflowNodeConfig = {
certificate: "",
providerType: "volcengine-live",
access: "",
domain: "",
};
if (data) config = data.config ?? config;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
providerType: "volcengine-live",
access: config.access as string,
certificate: config.certificate as string,
domain: config.domain as string,
},
});
const onSubmit = async (config: z.infer<typeof formSchema>) => {
updateNode({ ...data, config: { ...config }, validated: true });
hidePanel();
};
return (
<>
<Form {...form}>
<form
onSubmit={(e) => {
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="access"
render={({ field }) => (
<FormItem>
<FormLabel className="flex justify-between">
<div>{t("domain.deployment.form.access.label")}</div>
<AccessEditDialog
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
outConfigType="volcengine"
/>
</FormLabel>
<FormControl>
<AccessSelect
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("access", value);
}}
providerType="volcengine-live"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="certificate"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workflow.common.certificate.label")}</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("certificate", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("workflow.common.certificate.placeholder")} />
</SelectTrigger>
<SelectContent>
{beforeOutput.map((item) => (
<>
<SelectGroup key={item.id}>
<SelectLabel>{item.name}</SelectLabel>
{item.output?.map((output) => (
<SelectItem key={output.name} value={`${item.id}#${output.name}`}>
<div>
{item.name}-{output.label}
</div>
</SelectItem>
))}
</SelectGroup>
</>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain.deployment.form.domain.label")}</FormLabel>
<FormControl>
<Input placeholder={t("domain.deployment.form.domain.label")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>
</>
);
};
export default DeployToVolcengineLive;

View File

@ -1,10 +0,0 @@
import { Access } from "./access";
export type AccessGroup = {
id?: string;
name?: string;
access?: string[];
expand?: {
access: Access[];
};
};

View File

@ -15,10 +15,7 @@ import {
} from "@/components/ui/alert-dialog.tsx";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import AccessEditDialog from "@/components/certimate/AccessEditDialog";
import AccessGroupEdit from "@/components/certimate/AccessGroupEdit";
import AccessGroupList from "@/components/certimate/AccessGroupList";
import XPagination from "@/components/certimate/XPagination";
import { convertZulu2Beijing } from "@/lib/time";
import { Access as AccessType, accessProvidersMap } from "@/domain/access";
@ -40,10 +37,6 @@ const Access = () => {
const page = query.get("page");
const pageNumber = page ? Number(page) : 1;
const tab = query.get("tab");
const accessGroupId = query.get("accessGroupId");
const startIndex = (pageNumber - 1) * perPage;
const endIndex = startIndex + perPage;
@ -52,143 +45,104 @@ const Access = () => {
deleteAccess(rs.id);
};
const handleTabItemClick = (tab: string) => {
query.set("tab", tab);
navigate({ search: query.toString() });
};
return (
<div className="">
<div className="flex justify-between items-center">
<div className="text-muted-foreground">{t("access.page.title")}</div>
{tab != "access_group" ? (
<AccessEditDialog trigger={<Button>{t("access.authorization.add")}</Button>} op="add" />
) : (
<AccessGroupEdit trigger={<Button>{t("access.group.add")}</Button>} />
)}
<AccessEditDialog trigger={<Button>{t("access.authorization.add")}</Button>} op="add" />
</div>
<Tabs defaultValue={tab ? tab : "access"} value={tab ? tab : "access"} className="w-full mt-5">
<TabsList className="space-x-5 px-3">
<TabsTrigger
value="access"
onClick={() => {
handleTabItemClick("access");
}}
>
{t("access.authorization.tab")}
</TabsTrigger>
<TabsTrigger
value="access_group"
onClick={() => {
handleTabItemClick("access_group");
}}
>
{t("access.group.tab")}
</TabsTrigger>
</TabsList>
<TabsContent value="access">
{accesses.length === 0 ? (
<div className="flex flex-col items-center mt-10">
<span className="bg-orange-100 p-5 rounded-full">
<Key size={40} className="text-primary" />
</span>
{accesses.length === 0 ? (
<div className="flex flex-col items-center mt-10">
<span className="bg-orange-100 p-5 rounded-full">
<Key size={40} className="text-primary" />
</span>
<div className="text-center text-sm text-muted-foreground mt-3">{t("access.authorization.nodata")}</div>
<AccessEditDialog trigger={<Button>{t("access.authorization.add")}</Button>} op="add" className="mt-3" />
</div>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48">{t("common.text.name")}</div>
<div className="w-48">{t("common.text.provider")}</div>
<div className="text-center text-sm text-muted-foreground mt-3">{t("access.authorization.nodata")}</div>
<AccessEditDialog trigger={<Button>{t("access.authorization.add")}</Button>} op="add" className="mt-3" />
</div>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48">{t("common.text.name")}</div>
<div className="w-48">{t("common.text.provider")}</div>
<div className="w-60">{t("common.text.created_at")}</div>
<div className="w-60">{t("common.text.updated_at")}</div>
<div className="grow">{t("common.text.operations")}</div>
<div className="w-60">{t("common.text.created_at")}</div>
<div className="w-60">{t("common.text.updated_at")}</div>
<div className="grow">{t("common.text.operations")}</div>
</div>
{accesses.slice(startIndex, endIndex).map((access) => (
<div
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
key={access.id}
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-start">
<div className="pr-3 truncate">{access.name}</div>
</div>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center space-x-2">
<img src={accessProvidersMap.get(access.configType)?.icon} className="w-6" />
<div>{t(accessProvidersMap.get(access.configType)?.name || "")}</div>
</div>
{accesses
.filter((item) => {
return accessGroupId ? item.group == accessGroupId : true;
})
.slice(startIndex, endIndex)
.map((access) => (
<div
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
key={access.id}
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-start">
<div className="pr-3 truncate">{access.name}</div>
</div>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center space-x-2">
<img src={accessProvidersMap.get(access.configType)?.icon} className="w-6" />
<div>{t(accessProvidersMap.get(access.configType)?.name || "")}</div>
</div>
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">{access.created && convertZulu2Beijing(access.created)}</div>
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">{access.updated && convertZulu2Beijing(access.updated)}</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0">
<AccessEditDialog
trigger={
<Button variant={"link"} className="p-0">
{t("common.edit")}
</Button>
}
op="edit"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<AccessEditDialog
trigger={
<Button variant={"link"} className="p-0">
{t("common.copy")}
</Button>
}
op="copy"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
{t("common.delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="dark:text-gray-200">{t("access.authorization.delete")}</AlertDialogTitle>
<AlertDialogDescription>{t("access.authorization.delete.confirm")}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="dark:text-gray-200">{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDelete(access);
}}
>
{t("common.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
))}
<XPagination
totalPages={totalPages}
currentPage={pageNumber}
onPageChange={(page) => {
query.set("page", page.toString());
navigate({ search: query.toString() });
}}
/>
</>
)}
</TabsContent>
<TabsContent value="access_group">
<AccessGroupList />
</TabsContent>
</Tabs>
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">{access.created && convertZulu2Beijing(access.created)}</div>
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">{access.updated && convertZulu2Beijing(access.updated)}</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0">
<AccessEditDialog
trigger={
<Button variant={"link"} className="p-0">
{t("common.edit")}
</Button>
}
op="edit"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<AccessEditDialog
trigger={
<Button variant={"link"} className="p-0">
{t("common.copy")}
</Button>
}
op="copy"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
{t("common.delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="dark:text-gray-200">{t("access.authorization.delete")}</AlertDialogTitle>
<AlertDialogDescription>{t("access.authorization.delete.confirm")}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="dark:text-gray-200">{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDelete(access);
}}
>
{t("common.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
))}
<XPagination
totalPages={totalPages}
currentPage={pageNumber}
onPageChange={(page) => {
query.set("page", page.toString());
navigate({ search: query.toString() });
}}
/>
</>
)}
</div>
);
};

View File

@ -1,503 +0,0 @@
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { ChevronsUpDown, Plus, CircleHelp } from "lucide-react";
import { ClientResponseError } from "pocketbase";
import { Button } from "@/components/ui/button";
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Toaster } from "@/components/ui/toaster";
import { useToast } from "@/components/ui/use-toast";
import AccessEditDialog from "@/components/certimate/AccessEditDialog";
import DeployList from "@/components/certimate/DeployList";
import EmailsEdit from "@/components/certimate/EmailsEdit";
import StringList from "@/components/certimate/StringList";
import { cn } from "@/lib/utils";
import { PbErrorData } from "@/domain/base";
import { accessProvidersMap } from "@/domain/access";
import { EmailsSetting } from "@/domain/settings";
import { DeployConfig, Domain } from "@/domain/domain";
import { save, get } from "@/repository/domains";
import { useConfigContext } from "@/providers/config";
import { Switch } from "@/components/ui/switch";
import { TooltipFast } from "@/components/ui/tooltip";
const Edit = () => {
const {
config: { accesses, emails },
} = useConfigContext();
const [domain, setDomain] = useState<Domain>({} as Domain);
const location = useLocation();
const { t } = useTranslation();
const [tab, setTab] = useState<"apply" | "deploy">("apply");
useEffect(() => {
// Parsing query parameters
const queryParams = new URLSearchParams(location.search);
const id = queryParams.get("id");
if (id) {
const fetchData = async () => {
const data = await get(id);
setDomain(data);
};
fetchData();
}
}, [location.search]);
const formSchema = z.object({
id: z.string().optional(),
domain: z.string().min(1, {
message: "common.errmsg.domain_invalid",
}),
email: z.string().email("common.errmsg.email_invalid").optional(),
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "domain.application.form.access.placeholder",
}),
keyAlgorithm: z.string().optional(),
nameservers: z.string().optional(),
timeout: z.number().optional(),
disableFollowCNAME: z.boolean().optional(),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: "",
domain: "",
email: "",
access: "",
keyAlgorithm: "RSA2048",
nameservers: "",
timeout: 60,
disableFollowCNAME: true,
},
});
useEffect(() => {
if (domain) {
form.reset({
id: domain.id,
domain: domain.domain,
email: domain.applyConfig?.email,
access: domain.applyConfig?.access,
keyAlgorithm: domain.applyConfig?.keyAlgorithm,
nameservers: domain.applyConfig?.nameservers,
timeout: domain.applyConfig?.timeout,
disableFollowCNAME: domain.applyConfig?.disableFollowCNAME,
});
}
}, [domain, form]);
const { toast } = useToast();
const onSubmit = async (data: z.infer<typeof formSchema>) => {
const req: Domain = {
id: data.id as string,
crontab: "0 0 * * *",
domain: data.domain,
email: data.email,
access: data.access,
applyConfig: {
email: data.email ?? "",
access: data.access,
keyAlgorithm: data.keyAlgorithm,
nameservers: data.nameservers,
timeout: data.timeout,
disableFollowCNAME: data.disableFollowCNAME,
},
};
try {
const resp = await save(req);
let description = t("domain.application.form.domain.changed.message");
if (req.id == "") {
description = t("domain.application.form.domain.added.message");
}
toast({
title: t("common.save.succeeded.message"),
description,
});
if (!domain?.id) setTab("deploy");
setDomain({ ...resp });
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
});
return;
}
};
const handelOnDeployListChange = async (list: DeployConfig[]) => {
const req = {
...domain,
deployConfig: list,
};
try {
const resp = await save(req);
let description = t("domain.application.form.domain.changed.message");
if (req.id == "") {
description = t("domain.application.form.domain.added.message");
}
toast({
title: t("common.save.succeeded.message"),
description,
});
if (!domain?.id) setTab("deploy");
setDomain({ ...resp });
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
});
return;
}
};
return (
<>
<div className="">
<Toaster />
<div className="h-5 text-muted-foreground">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#/domains">{t("domain.page.title")}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{domain?.id ? t("domain.edit") : t("domain.add")}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="flex flex-col justify-center w-full mt-5 md:space-x-10 md:flex-row">
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex md:mt-5">
<div
className={cn("cursor-pointer text-right", tab === "apply" ? "text-primary" : "")}
onClick={() => {
setTab("apply");
}}
>
{t("domain.application.tab")}
</div>
<div
className={cn("cursor-pointer text-right", tab === "deploy" ? "text-primary" : "")}
onClick={() => {
if (!domain?.id) {
toast({
title: t("domain.application.unsaved.message"),
description: t("domain.application.unsaved.message"),
variant: "destructive",
});
return;
}
setTab("deploy");
}}
>
{t("domain.deployment.tab")}
</div>
</div>
<div className="flex flex-col">
<div className={cn("w-full md:w-[35em] p-5 rounded mt-3 md:mt-0", tab == "deploy" && "hidden")}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 dark:text-stone-200">
{/* 域名 */}
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<>
<StringList
value={field.value}
valueType="domain"
onValueChange={(domain: string) => {
form.setValue("domain", domain);
}}
/>
</>
<FormMessage />
</FormItem>
)}
/>
{/* 邮箱 */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="flex justify-between w-full">
<div>{t("domain.application.form.email.label") + " " + t("domain.application.form.email.tips")}</div>
<EmailsEdit
trigger={
<div className="flex items-center font-normal cursor-pointer text-primary hover:underline">
<Plus size={14} />
{t("common.add")}
</div>
}
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("email", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.application.form.email.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.application.form.email.list")}</SelectLabel>
{(emails.content as EmailsSetting).emails.map((item) => (
<SelectItem key={item} value={item}>
<div>{item}</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* DNS 服务商授权 */}
<FormField
control={form.control}
name="access"
render={({ field }) => (
<FormItem>
<FormLabel className="flex justify-between w-full">
<div>{t("domain.application.form.access.label")}</div>
<AccessEditDialog
trigger={
<div className="flex items-center font-normal cursor-pointer text-primary hover:underline">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("access", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.application.form.access.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.application.form.access.list")}</SelectLabel>
{accesses
.filter((item) => item.usage != "deploy")
.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img className="w-6" src={accessProvidersMap.get(item.configType)?.icon} />
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div>
<hr />
<Collapsible>
<CollapsibleTrigger className="w-full my-4">
<div className="flex items-center justify-between space-x-4">
<span className="flex-1 text-sm text-left text-gray-600">{t("domain.application.form.advanced_settings.label")}</span>
<ChevronsUpDown className="w-4 h-4" />
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col space-y-8">
{/* 证书算法 */}
<FormField
control={form.control}
name="keyAlgorithm"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain.application.form.key_algorithm.label")}</FormLabel>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("keyAlgorithm", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.application.form.key_algorithm.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="RSA2048">RSA2048</SelectItem>
<SelectItem value="RSA3072">RSA3072</SelectItem>
<SelectItem value="RSA4096">RSA4096</SelectItem>
<SelectItem value="RSA8192">RSA8192</SelectItem>
<SelectItem value="EC256">EC256</SelectItem>
<SelectItem value="EC384">EC384</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
{/* DNS */}
<FormField
control={form.control}
name="nameservers"
render={({ field }) => (
<FormItem>
<StringList
value={field.value ?? ""}
onValueChange={(val: string) => {
form.setValue("nameservers", val);
}}
valueType="dns"
></StringList>
<FormMessage />
</FormItem>
)}
/>
{/* DNS 超时时间 */}
<FormField
control={form.control}
name="timeout"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain.application.form.timeout.label")}</FormLabel>
<FormControl>
<Input
type="number"
placeholder={t("domain.application.form.timeout.placeholder")}
{...field}
value={field.value}
onChange={(e) => {
form.setValue("timeout", parseInt(e.target.value));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 禁用 CNAME 跟随 */}
<FormField
control={form.control}
name="disableFollowCNAME"
render={({ field }) => (
<FormItem>
<FormLabel>
<div className="flex">
<span className="mr-1">{t("domain.application.form.disable_follow_cname.label")} </span>
<TooltipFast
className="max-w-[20rem]"
contentView={
<p>
{t("domain.application.form.disable_follow_cname.tips")}
<a
className="text-primary"
target="_blank"
href="https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname"
>
{t("domain.application.form.disable_follow_cname.tips_link")}
</a>
</p>
}
>
<CircleHelp size={14} />
</TooltipFast>
</div>
</FormLabel>
<FormControl>
<div>
<Switch
defaultChecked={field.value}
onCheckedChange={(value) => {
form.setValue(field.name, value);
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CollapsibleContent>
</Collapsible>
</div>
<div className="flex justify-end">
<Button type="submit">{domain?.id ? t("common.save") : t("common.next")}</Button>
</div>
</form>
</Form>
</div>
<div className={cn("flex flex-col space-y-5 w-full md:w-[35em]", tab == "apply" && "hidden")}>
<DeployList
deploys={domain?.deployConfig ?? []}
onChange={(list: DeployConfig[]) => {
handelOnDeployListChange(list);
}}
/>
</div>
</div>
</div>
</div>
</>
);
};
export default Edit;

View File

@ -1,339 +0,0 @@
import { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useTranslation, Trans } from "react-i18next";
import { TooltipContent, TooltipProvider } from "@radix-ui/react-tooltip";
import { Earth } from "lucide-react";
import Show from "@/components/Show";
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import XPagination from "@/components/certimate/XPagination";
import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialog,
AlertDialogContent,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Toaster } from "@/components/ui/toaster";
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
import { useToast } from "@/components/ui/use-toast";
import { CustomFile, saveFiles2ZIP } from "@/lib/file";
import { convertZulu2Beijing, getDate, getLeftDays } from "@/lib/time";
import { Domain } from "@/domain/domain";
import { list, remove, save, subscribeId, unsubscribeId } from "@/repository/domains";
const Home = () => {
const toast = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const location = useLocation();
const query = new URLSearchParams(location.search);
const page = query.get("page");
const state = query.get("state");
const [totalPage, setTotalPage] = useState(0);
const handleCreateClick = () => {
navigate("/edit");
};
const setPage = (newPage: number) => {
query.set("page", newPage.toString());
navigate(`?${query.toString()}`);
};
const handleEditClick = (id: string) => {
navigate(`/edit?id=${id}`);
};
const handleHistoryClick = (id: string) => {
navigate(`/history?domain=${id}`);
};
const handleDeleteClick = async (id: string) => {
try {
await remove(id);
setDomains(domains.filter((domain) => domain.id !== id));
} catch (error) {
console.error("Error deleting domain:", error);
}
};
const [domains, setDomains] = useState<Domain[]>([]);
useEffect(() => {
const fetchData = async () => {
const data = await list({
page: page ? Number(page) : 1,
perPage: 10,
state: state ? state : "",
});
setDomains(data.items);
setTotalPage(data.totalPages);
};
fetchData();
}, [page, state]);
const handelCheckedChange = async (id: string) => {
const checkedDomains = domains.filter((domain) => domain.id === id);
const isChecked = checkedDomains[0].enabled;
const data = checkedDomains[0];
data.enabled = !isChecked;
await save(data);
const updatedDomains = domains.map((domain) => {
if (domain.id === id) {
return { ...domain, checked: !isChecked };
}
return domain;
});
setDomains(updatedDomains);
};
const handleRightNowClick = async (domain: Domain) => {
try {
unsubscribeId(domain.id ?? "");
subscribeId(domain.id ?? "", (resp) => {
const updatedDomains = domains.map((domain) => {
if (domain.id === resp.id) {
return { ...resp };
}
return domain;
});
setDomains(updatedDomains);
});
domain.rightnow = true;
await save(domain);
toast.toast({
title: t("domain.deploy.started.message"),
description: t("domain.deploy.started.tips"),
});
} catch (e) {
toast.toast({
title: t("domain.deploy.failed.message"),
description: (
// 这里的 text 只是占位作用,实际文案在 src/i18n/locales/[lang].json
<Trans i18nKey="domain.deploy.failed.tips">
text1
<Link to={`/history?domain=${domain.id}`} className="underline text-blue-500">
text2
</Link>
text3
</Trans>
),
variant: "destructive",
});
}
};
const handleForceClick = async (domain: Domain) => {
await handleRightNowClick({ ...domain, deployed: false });
};
const handleDownloadClick = async (domain: Domain) => {
const zipName = `${domain.id}-${domain.domain}.zip`;
const files: CustomFile[] = [
{
name: `${domain.domain}.pem`,
content: domain.certificate ? domain.certificate : "",
},
{
name: `${domain.domain}.key`,
content: domain.privateKey ? domain.privateKey : "",
},
];
await saveFiles2ZIP(zipName, files);
};
return (
<>
<div className="">
<Toaster />
<div className="flex justify-between items-center">
<div className="text-muted-foreground">{t("domain.page.title")}</div>
<Button onClick={handleCreateClick}>{t("domain.add")}</Button>
</div>
{!domains.length ? (
<>
<div className="flex flex-col items-center mt-10">
<span className="bg-orange-100 p-5 rounded-full">
<Earth size={40} className="text-primary" />
</span>
<div className="text-center text-sm text-muted-foreground mt-3">{t("domain.nodata")}</div>
<Button onClick={handleCreateClick} className="mt-3">
{t("domain.add")}
</Button>
</div>
</>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-36">{t("common.text.domain")}</div>
<div className="w-40">{t("domain.props.expiry")}</div>
<div className="w-32">{t("domain.props.last_execution_status")}</div>
<div className="w-64">{t("domain.props.last_execution_stage")}</div>
<div className="w-40 sm:ml-2">{t("domain.props.last_execution_time")}</div>
<div className="w-24">{t("domain.props.enable")}</div>
<div className="grow">{t("common.text.operations")}</div>
</div>
{domains.map((domain) => (
<div
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
key={domain.id}
>
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex flex-col items-start">
{domain.domain.split(";").map((domain: string) => (
<div className="pr-3 truncate w-full">{domain}</div>
))}
</div>
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
<div>
{domain.expiredAt ? (
<>
<div>{t("domain.props.expiry.date1", { date: `${getLeftDays(domain.expiredAt)}/90` })}</div>
<div>
{t("domain.props.expiry.date2", {
date: getDate(domain.expiredAt),
})}
</div>
</>
) : (
"---"
)}
</div>
</div>
<div className="sm:w-32 w-full pt-1 sm:pt-0 flex items-center">
{domain.lastDeployedAt && domain.expand?.lastDeployment ? (
<>
<DeployState deployment={domain.expand.lastDeployment} />
</>
) : (
"---"
)}
</div>
<div className="sm:w-64 w-full pt-1 sm:pt-0 flex items-center">
{domain.lastDeployedAt && domain.expand?.lastDeployment ? (
<DeployProgress phase={domain.expand.lastDeployment?.phase} phaseSuccess={domain.expand.lastDeployment?.phaseSuccess} />
) : (
"---"
)}
</div>
<div className="sm:w-40 pt-1 sm:pt-0 sm:ml-2 flex items-center">
{domain.lastDeployedAt ? convertZulu2Beijing(domain.lastDeployedAt) : "---"}
</div>
<div className="sm:w-24 flex items-center">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Switch
checked={domain.enabled}
onCheckedChange={() => {
handelCheckedChange(domain.id ?? "");
}}
></Switch>
</TooltipTrigger>
<TooltipContent>
<div className="border rounded-sm px-3 bg-background text-muted-foreground text-xs">
{domain.enabled ? t("domain.props.enable.disabled") : t("domain.props.enable.enabled")}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0">
<Button variant={"link"} className="p-0" onClick={() => handleHistoryClick(domain.id ?? "")}>
{t("domain.history")}
</Button>
<Show when={domain.enabled ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button variant={"link"} className="p-0" onClick={() => handleRightNowClick(domain)}>
{t("domain.deploy")}
</Button>
</Show>
<Show when={(domain.enabled ? true : false) && domain.deployed ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button variant={"link"} className="p-0" onClick={() => handleForceClick(domain)}>
{t("domain.deploy_forced")}
</Button>
</Show>
<Show when={domain.expiredAt ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button variant={"link"} className="p-0" onClick={() => handleDownloadClick(domain)}>
{t("common.download")}
</Button>
</Show>
{!domain.enabled && (
<>
<Separator orientation="vertical" className="h-4 mx-2" />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
{t("common.delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("domain.delete")}</AlertDialogTitle>
<AlertDialogDescription>{t("domain.delete.confirm")}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDeleteClick(domain.id ?? "");
}}
>
{t("common.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button variant={"link"} className="p-0" onClick={() => handleEditClick(domain.id ?? "")}>
{t("common.edit")}
</Button>
</>
)}
</div>
</div>
))}
<XPagination
totalPages={totalPage}
currentPage={page ? Number(page) : 1}
onPageChange={(page) => {
setPage(page);
}}
/>
</>
)}
</div>
</>
);
};
export default Home;

View File

@ -1,170 +0,0 @@
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Smile } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import { convertZulu2Beijing } from "@/lib/time";
import { Deployment, DeploymentListReq, Log } from "@/domain/deployment";
import { list } from "@/repository/deployment";
const History = () => {
const navigate = useNavigate();
const [deployments, setDeployments] = useState<Deployment[]>();
const [searchParams] = useSearchParams();
const { t } = useTranslation();
const domain = searchParams.get("domain");
useEffect(() => {
const fetchData = async () => {
const param: DeploymentListReq = {};
if (domain) {
param.domain = domain;
}
const data = await list(param);
setDeployments(data.items);
};
fetchData();
}, [domain]);
return (
<ScrollArea className="h-[80vh] overflow-hidden">
<div className="text-muted-foreground">{t("history.page.title")}</div>
{!deployments?.length ? (
<>
<Alert className="max-w-[40em] mx-auto mt-20">
<AlertTitle>{t("common.text.nodata")}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
<Smile className="text-yellow-400" size={36} />
</div>
<div className="ml-2"> {t("history.nodata")}</div>
</div>
<div className="mt-2 flex justify-end">
<Button
onClick={() => {
navigate("/");
}}
>
{t("domain.add")}
</Button>
</div>
</AlertDescription>
</Alert>
</>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48">{t("history.props.domain")}</div>
<div className="w-24">{t("history.props.status")}</div>
<div className="w-56">{t("history.props.stage")}</div>
<div className="w-56 sm:ml-2 text-center">{t("history.props.last_execution_time")}</div>
<div className="grow">{t("common.text.operations")}</div>
</div>
{deployments?.map((deployment) => (
<div
key={deployment.id}
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-start flex-col">
{deployment.expand.domain?.domain.split(";").map((domain: string) => <div className="pr-3 truncate w-full">{domain}</div>)}
</div>
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
<DeployState deployment={deployment} />
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
<DeployProgress phase={deployment.phase} phaseSuccess={deployment.phaseSuccess} />
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center sm:justify-center">{convertZulu2Beijing(deployment.deployedAt)}</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0 sm:ml-2">
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
{t("history.log")}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
{t("history.log")}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh] overflow-y-auto">
{deployment.log.check && (
<>
{deployment.log.check.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.error && <div className="mt-1 text-red-600">{item.error}</div>}
</div>
);
})}
</>
)}
{deployment.log.apply && (
<>
{deployment.log.apply.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.info &&
item.info.map((info: string) => {
return <div className="mt-1 text-green-600">{info}</div>;
})}
{item.error && <div className="mt-1 text-red-600">{item.error}</div>}
</div>
);
})}
</>
)}
{deployment.log.deploy && (
<>
{deployment.log.deploy.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.info &&
item.info.map((info: string) => {
return <div className="mt-1 text-green-600 break-words">{info}</div>;
})}
{item.error && <div className="mt-1 text-red-600">{item.error}</div>}
</div>
);
})}
</>
)}
</div>
</SheetContent>
</Sheet>
</div>
</div>
))}
</>
)}
</ScrollArea>
);
};
export default History;

View File

@ -43,7 +43,6 @@ const WorkflowDetail = () => {
const [running, setRunning] = useState(false);
useEffect(() => {
console.log(id);
init(id ?? "");
if (id) {
setLocId(id);
@ -87,6 +86,9 @@ const WorkflowDetail = () => {
return;
}
switchEnable();
if (!locId) {
navigate(`/workflow/detail?id=${workflow.id}`);
}
};
const handleWorkflowSaveClick = () => {
@ -99,6 +101,9 @@ const WorkflowDetail = () => {
return;
}
save();
if (!locId) {
navigate(`/workflow/detail?id=${workflow.id}`);
}
};
const getTabCls = (tabName: string) => {

View File

@ -1,10 +1,7 @@
import { createHashRouter } from "react-router-dom";
import DashboardLayout from "./pages/DashboardLayout";
import Home from "./pages/domains/Home";
import Edit from "./pages/domains/Edit";
import Access from "./pages/access/Access";
import History from "./pages/history/History";
import Login from "./pages/login/Login";
import LoginLayout from "./pages/LoginLayout";
import Password from "./pages/setting/Password";
@ -26,22 +23,10 @@ export const router = createHashRouter([
path: "/",
element: <Dashboard />,
},
{
path: "/domains",
element: <Home />,
},
{
path: "/edit",
element: <Edit />,
},
{
path: "/access",
element: <Access />,
},
{
path: "/history",
element: <History />,
},
{
path: "/workflow",
element: <Workflow />,