diff --git a/internal/certificate/service.go b/internal/certificate/service.go index 715d2ae2..f8874f2d 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -11,6 +11,8 @@ import ( "time" "github.com/go-acme/lego/v4/certcrypto" + "github.com/pocketbase/dbx" + "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/domain/dtos" @@ -27,21 +29,29 @@ const ( type certificateRepository interface { ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) GetById(ctx context.Context, id string) (*domain.Certificate, error) + DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) +} + +type settingsRepository interface { + GetByName(ctx context.Context, name string) (*domain.Settings, error) } type CertificateService struct { - certRepo certificateRepository + certificateRepo certificateRepository + settingsRepo settingsRepository } -func NewCertificateService(certRepo certificateRepository) *CertificateService { +func NewCertificateService(certificateRepo certificateRepository, settingsRepo settingsRepository) *CertificateService { return &CertificateService{ - certRepo: certRepo, + certificateRepo: certificateRepo, + settingsRepo: settingsRepo, } } func (s *CertificateService) InitSchedule(ctx context.Context) error { + // 每日发送过期证书提醒 app.GetScheduler().MustAdd("certificateExpireSoonNotify", "0 0 * * *", func() { - certificates, err := s.certRepo.ListExpireSoon(context.Background()) + certificates, err := s.certificateRepo.ListExpireSoon(context.Background()) if err != nil { app.GetLogger().Error("failed to get certificates which expire soon", "err", err) return @@ -56,11 +66,37 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error { app.GetLogger().Error("failed to send notification", "err", err) } }) + + // 每日清理过期证书 + app.GetScheduler().MustAdd("certificateExpiredCleanup", "0 0 * * *", func() { + settings, err := s.settingsRepo.GetByName(ctx, "persistence") + if err != nil { + app.GetLogger().Error("failed to get persistence settings", "err", err) + return + } + + var settingsContent *domain.PersistenceSettingsContent + json.Unmarshal([]byte(settings.Content), &settingsContent) + if settingsContent != nil && settingsContent.ExpiredCertificatesMaxDaysRetention != 0 { + ret, err := s.certificateRepo.DeleteWhere( + context.Background(), + dbx.NewExp(fmt.Sprintf("expireAt 0 { + app.GetLogger().Info(fmt.Sprintf("cleanup %d expired certificates", ret)) + } + } + }) + return nil } func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) (*dtos.CertificateArchiveFileResp, error) { - certificate, err := s.certRepo.GetById(ctx, req.CertificateId) + certificate, err := s.certificateRepo.GetById(ctx, req.CertificateId) if err != nil { return nil, err } diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 0c7dbbd9..4e03ad2e 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -176,7 +176,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, Region: maputil.GetString(options.ProviderDeployConfig, "region"), - ServiceVersion: maputil.GetString(options.ProviderDeployConfig, "serviceVersion"), + ServiceVersion: maputil.GetOrDefaultString(options.ProviderDeployConfig, "serviceVersion", "3.0"), Domain: maputil.GetString(options.ProviderDeployConfig, "domain"), }) return deployer, err diff --git a/internal/domain/settings.go b/internal/domain/settings.go index 9819ec43..ebe6b9d7 100644 --- a/internal/domain/settings.go +++ b/internal/domain/settings.go @@ -14,12 +14,10 @@ type Settings struct { } type NotifyTemplatesSettingsContent struct { - NotifyTemplates []NotifyTemplate `json:"notifyTemplates"` -} - -type NotifyTemplate struct { - Subject string `json:"subject"` - Message string `json:"message"` + NotifyTemplates []struct { + Subject string `json:"subject"` + Message string `json:"message"` + } `json:"notifyTemplates"` } type NotifyChannelsSettingsContent map[string]map[string]any @@ -37,3 +35,8 @@ func (s *Settings) GetNotifyChannelConfig(channel string) (map[string]any, error return v, nil } + +type PersistenceSettingsContent struct { + WorkflowRunsMaxDaysRetention int `json:"workflowRunsMaxDaysRetention"` + ExpiredCertificatesMaxDaysRetention int `json:"expiredCertificatesMaxDaysRetention"` +} diff --git a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go index d3f17965..edd2bc76 100644 --- a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go +++ b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go @@ -23,7 +23,6 @@ type DeployerConfig struct { // 阿里云地域。 Region string `json:"region"` // 服务版本。 - // 零值时默认为 "3.0"。 ServiceVersion string `json:"serviceVersion"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` @@ -70,7 +69,7 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { switch d.config.ServiceVersion { - case "", "3.0": + case "3.0": if err := d.deployToFC3(ctx, certPem, privkeyPem); err != nil { return nil, err } diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go index 0695ca47..13d2c094 100644 --- a/internal/repository/certificate.go +++ b/internal/repository/certificate.go @@ -67,11 +67,9 @@ func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflo dbx.Params{"workflowNodeId": workflowNodeId}, ) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, domain.ErrRecordNotFound - } return nil, err } + if len(records) == 0 { return nil, domain.ErrRecordNotFound } @@ -125,6 +123,29 @@ func (r *CertificateRepository) Save(ctx context.Context, certificate *domain.Ce return certificate, nil } +func (r *CertificateRepository) DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) { + records, err := app.GetApp().FindAllRecords(domain.CollectionNameCertificate, exprs...) + if err != nil { + return 0, nil + } + + var ret int + var errs []error + for _, record := range records { + if err := app.GetApp().Delete(record); err != nil { + errs = append(errs, err) + } else { + ret++ + } + } + + if len(errs) > 0 { + return ret, errors.Join(errs...) + } + + return ret, nil +} + func (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain.Certificate, error) { if record == nil { return nil, fmt.Errorf("record is nil") diff --git a/internal/repository/workflow_run.go b/internal/repository/workflow_run.go index aef61ac3..19a06747 100644 --- a/internal/repository/workflow_run.go +++ b/internal/repository/workflow_run.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/domain" @@ -96,6 +97,29 @@ func (r *WorkflowRunRepository) Save(ctx context.Context, workflowRun *domain.Wo return workflowRun, nil } +func (r *WorkflowRunRepository) DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) { + records, err := app.GetApp().FindAllRecords(domain.CollectionNameWorkflowRun, exprs...) + if err != nil { + return 0, nil + } + + var ret int + var errs []error + for _, record := range records { + if err := app.GetApp().Delete(record); err != nil { + errs = append(errs, err) + } else { + ret++ + } + } + + if len(errs) > 0 { + return ret, errors.Join(errs...) + } + + return ret, nil +} + func (r *WorkflowRunRepository) castRecordToModel(record *core.Record) (*domain.WorkflowRun, error) { if record == nil { return nil, fmt.Errorf("record is nil") diff --git a/internal/rest/routes/routes.go b/internal/rest/routes/routes.go index 87fcb297..6172bc12 100644 --- a/internal/rest/routes/routes.go +++ b/internal/rest/routes/routes.go @@ -23,17 +23,15 @@ var ( ) func Register(router *router.Router[*core.RequestEvent]) { - certificateRepo := repository.NewCertificateRepository() - certificateSvc = certificate.NewCertificateService(certificateRepo) - workflowRepo := repository.NewWorkflowRepository() workflowRunRepo := repository.NewWorkflowRunRepository() - workflowSvc = workflow.NewWorkflowService(workflowRepo, workflowRunRepo) - - statisticsRepo := repository.NewStatisticsRepository() - statisticsSvc = statistics.NewStatisticsService(statisticsRepo) - + certificateRepo := repository.NewCertificateRepository() settingsRepo := repository.NewSettingsRepository() + statisticsRepo := repository.NewStatisticsRepository() + + certificateSvc = certificate.NewCertificateService(certificateRepo, settingsRepo) + workflowSvc = workflow.NewWorkflowService(workflowRepo, workflowRunRepo, settingsRepo) + statisticsSvc = statistics.NewStatisticsService(statisticsRepo) notifySvc = notify.NewNotifyService(settingsRepo) group := router.Group("/api") diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 91dbf115..ba4ee9c3 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -10,10 +10,11 @@ import ( func Register() { workflowRepo := repository.NewWorkflowRepository() workflowRunRepo := repository.NewWorkflowRunRepository() - workflowSvc := workflow.NewWorkflowService(workflowRepo, workflowRunRepo) - certificateRepo := repository.NewCertificateRepository() - certificateSvc := certificate.NewCertificateService(certificateRepo) + settingsRepo := repository.NewSettingsRepository() + + workflowSvc := workflow.NewWorkflowService(workflowRepo, workflowRunRepo, settingsRepo) + certificateSvc := certificate.NewCertificateService(certificateRepo, settingsRepo) if err := InitWorkflowScheduler(workflowSvc); err != nil { app.GetLogger().Error("failed to init workflow scheduler", "err", err) diff --git a/internal/workflow/event.go b/internal/workflow/event.go index 0fedd67b..ec850af9 100644 --- a/internal/workflow/event.go +++ b/internal/workflow/event.go @@ -65,7 +65,7 @@ func onWorkflowRecordCreateOrUpdate(ctx context.Context, record *core.Record) er // 反之,重新添加定时任务 err := scheduler.Add(fmt.Sprintf("workflow#%s", workflowId), record.GetString("triggerCron"), func() { - workflowSrv := NewWorkflowService(repository.NewWorkflowRepository(), repository.NewWorkflowRunRepository()) + workflowSrv := NewWorkflowService(repository.NewWorkflowRepository(), repository.NewWorkflowRunRepository(), repository.NewSettingsRepository()) workflowSrv.StartRun(ctx, &dtos.WorkflowStartRunReq{ WorkflowId: workflowId, RunTrigger: domain.WorkflowTriggerTypeAuto, diff --git a/internal/workflow/service.go b/internal/workflow/service.go index 892ee00b..f234be63 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -2,10 +2,13 @@ package workflow import ( "context" + "encoding/json" "errors" "fmt" "time" + "github.com/pocketbase/dbx" + "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/domain/dtos" @@ -21,6 +24,11 @@ type workflowRepository interface { type workflowRunRepository interface { GetById(ctx context.Context, id string) (*domain.WorkflowRun, error) Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) + DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) +} + +type settingsRepository interface { + GetByName(ctx context.Context, name string) (*domain.Settings, error) } type WorkflowService struct { @@ -28,40 +36,71 @@ type WorkflowService struct { workflowRepo workflowRepository workflowRunRepo workflowRunRepository + settingsRepo settingsRepository } -func NewWorkflowService(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository) *WorkflowService { +func NewWorkflowService(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository, settingsRepo settingsRepository) *WorkflowService { srv := &WorkflowService{ dispatcher: dispatcher.GetSingletonDispatcher(), workflowRepo: workflowRepo, workflowRunRepo: workflowRunRepo, + settingsRepo: settingsRepo, } return srv } func (s *WorkflowService) InitSchedule(ctx context.Context) error { - workflows, err := s.workflowRepo.ListEnabledAuto(ctx) - if err != nil { - return err - } - - scheduler := app.GetScheduler() - for _, workflow := range workflows { - var errs []error - - err := scheduler.Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() { - s.StartRun(ctx, &dtos.WorkflowStartRunReq{ - WorkflowId: workflow.Id, - RunTrigger: domain.WorkflowTriggerTypeAuto, - }) - }) + // 每日清理工作流执行历史 + app.GetScheduler().MustAdd("workflowHistoryRunsCleanup", "0 0 * * *", func() { + settings, err := s.settingsRepo.GetByName(ctx, "persistence") if err != nil { - errs = append(errs, err) + app.GetLogger().Error("failed to get persistence settings", "err", err) + return } - if len(errs) > 0 { - return errors.Join(errs...) + var settingsContent *domain.PersistenceSettingsContent + json.Unmarshal([]byte(settings.Content), &settingsContent) + if settingsContent != nil && settingsContent.WorkflowRunsMaxDaysRetention != 0 { + ret, err := s.workflowRunRepo.DeleteWhere( + context.Background(), + dbx.NewExp(fmt.Sprintf("status!='%s'", string(domain.WorkflowRunStatusTypePending))), + dbx.NewExp(fmt.Sprintf("status!='%s'", string(domain.WorkflowRunStatusTypeRunning))), + dbx.NewExp(fmt.Sprintf("endedAt 0 { + app.GetLogger().Info(fmt.Sprintf("cleanup %d workflow history runs", ret)) + } + } + }) + + // 工作流 + { + workflows, err := s.workflowRepo.ListEnabledAuto(ctx) + if err != nil { + return err + } + + for _, workflow := range workflows { + var errs []error + + err := app.GetScheduler().Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() { + s.StartRun(ctx, &dtos.WorkflowStartRunReq{ + WorkflowId: workflow.Id, + RunTrigger: domain.WorkflowTriggerTypeAuto, + }) + }) + if err != nil { + errs = append(errs, err) + } + + if len(errs) > 0 { + return errors.Join(errs...) + } } } diff --git a/ui/src/components/access/AccessFormAzureConfig.tsx b/ui/src/components/access/AccessFormAzureConfig.tsx index 954d47aa..a5facbf9 100644 --- a/ui/src/components/access/AccessFormAzureConfig.tsx +++ b/ui/src/components/access/AccessFormAzureConfig.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; +import { AutoComplete, Form, type FormInstance, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -92,7 +92,11 @@ const AccessFormAzureConfig = ({ form: formInst, formName, disabled, initialValu rules={[formRule]} tooltip={} > - + ({ value }))} + placeholder={t("access.form.azure_cloud_name.placeholder")} + filterOption={(inputValue, option) => option!.value.toLowerCase().includes(inputValue.toLowerCase())} + /> ); diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index 977d80fc..e34105e0 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -3,6 +3,7 @@ export const SETTINGS_NAMES = Object.freeze({ NOTIFY_TEMPLATES: "notifyTemplates", NOTIFY_CHANNELS: "notifyChannels", SSL_PROVIDER: "sslProvider", + PERSISTENCE: "persistence", } as const); export type SettingsNames = (typeof SETTINGS_NAMES)[keyof typeof SETTINGS_NAMES]; @@ -165,3 +166,10 @@ export type SSLProviderGoogleTrustServicesConfig = { eabHmacKey: string; }; // #endregion + +// #region Settings: Persistence +export type PersistenceSettingsContent = { + workflowRunsMaxDaysRetention?: number; + expiredCertificatesMaxDaysRetention?: number; +}; +// #endregion diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index f4b3c85f..74e869bd 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -90,5 +90,15 @@ "settings.sslprovider.form.gts_eab_kid.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", "settings.sslprovider.form.gts_eab_hmac_key.label": "EAB HMAC Key", "settings.sslprovider.form.gts_eab_hmac_key.placeholder": "Please enter EAB HMAC Key", - "settings.sslprovider.form.gts_eab_hmac_key.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial" + "settings.sslprovider.form.gts_eab_hmac_key.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", + + "settings.persistence.tab": "Persistence", + "settings.persistence.form.workflow_runs_max_days.label": "Max days retention of workflow history runs", + "settings.persistence.form.workflow_runs_max_days.placeholder": "Please enter the maximum retention days of workflow history runs", + "settings.persistence.form.workflow_runs_max_days.unit": "days", + "settings.persistence.form.workflow_runs_max_days.extra": "Set to 0 to disable cleanup workflow history runs. It is recommended to set it to 180 days or more.", + "settings.persistence.form.expired_certificates_max_days.label": "Max days retention of expired certificates", + "settings.persistence.form.expired_certificates_max_days.placeholder": "Please enter the maximum retention days of expired certificates", + "settings.persistence.form.expired_certificates_max_days.unit": "days", + "settings.persistence.form.expired_certificates_max_days.extra": "Set to 0 to disable cleanup expired certificates." } diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index 1fcec35d..0c51d33e 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -71,7 +71,7 @@ "settings.notification.channel.form.wecom_webhook_url.placeholder": "请输入机器人 Webhook 地址", "settings.notification.channel.form.wecom_webhook_url.tooltip": "这是什么?请参阅 https://open.work.weixin.qq.com/help2/pc/18401", - "settings.sslprovider.tab": "证书颁发机构(CA)", + "settings.sslprovider.tab": "证书颁发机构", "settings.sslprovider.form.provider.label": "ACME 服务商", "settings.sslprovider.form.provider.option.letsencrypt.label": "Let's Encrypt", "settings.sslprovider.form.provider.option.letsencrypt_staging.label": "Let's Encrypt 测试环境", @@ -90,5 +90,15 @@ "settings.sslprovider.form.gts_eab_kid.tooltip": "这是什么?请参阅 https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", "settings.sslprovider.form.gts_eab_hmac_key.label": "EAB HMAC Key", "settings.sslprovider.form.gts_eab_hmac_key.placeholder": "请输入 EAB HMAC Key", - "settings.sslprovider.form.gts_eab_hmac_key.tooltip": "这是什么?请参阅 https://cloud.google.com/certificate-manager/docs/public-ca-tutorial" + "settings.sslprovider.form.gts_eab_hmac_key.tooltip": "这是什么?请参阅 https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", + + "settings.persistence.tab": "数据持久化", + "settings.persistence.form.workflow_runs_max_days.label": "工作流执行历史保留天数", + "settings.persistence.form.workflow_runs_max_days.placeholder": "请输入执行历史保留天数", + "settings.persistence.form.workflow_runs_max_days.unit": "天", + "settings.persistence.form.workflow_runs_max_days.extra": "设置为 0 表示永久保留,不会自动清理。建议设置为 180 天以上。", + "settings.persistence.form.expired_certificates_max_days.label": "证书过期后保留天数", + "settings.persistence.form.expired_certificates_max_days.placeholder": "请输入过期证书保留天数", + "settings.persistence.form.expired_certificates_max_days.unit": "天", + "settings.persistence.form.expired_certificates_max_days.extra": "设置为 0 表示永久保留,不会自动清理。" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index a2ff6c8e..cbb6a2cf 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -85,7 +85,7 @@ "workflow_node.deploy.form.provider_access.placeholder": "请选择主机提供商授权", "workflow_node.deploy.form.provider_access.tooltip": "用于部署证书,注意与申请阶段所需的 DNS 提供商相区分。", "workflow_node.deploy.form.provider_access.button": "新建", - "workflow_node.deploy.form.provider_access.guide_for_local": "小贴士:由于表单限制,你同样需要为本地部署选择一个授权 —— 即使它是空白的。
请注意:如果你使用 Docker 安装 Certimate,“本地部署”将会部署到容器内而非宿主机上。", + "workflow_node.deploy.form.provider_access.guide_for_local": "小贴士:由于表单限制,你同样需要为本地部署选择一个授权 —— 即使它是空白的。
请注意,如果你使用 Docker 安装 Certimate,“本地部署”将会部署到容器内而非宿主机上。", "workflow_node.deploy.form.certificate.label": "待部署证书", "workflow_node.deploy.form.certificate.placeholder": "请选择待部署证书", "workflow_node.deploy.form.certificate.tooltip": "待部署证书来自之前的申请阶段。如果选项为空请先确保前序节点配置正确。", diff --git a/ui/src/pages/settings/Settings.tsx b/ui/src/pages/settings/Settings.tsx index dcd4ef14..be72efc0 100644 --- a/ui/src/pages/settings/Settings.tsx +++ b/ui/src/pages/settings/Settings.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { ApiOutlined as ApiOutlinedIcon, + DatabaseOutlined as DatabaseOutlinedIcon, LockOutlined as LockOutlinedIcon, SendOutlined as SendOutlinedIcon, UserOutlined as UserOutlinedIcon, @@ -69,6 +70,15 @@ const Settings = () => { ), }, + { + key: "persistence", + label: ( + + + + + ), + }, ]} activeTabKey={tabValue} onTabChange={(key) => { diff --git a/ui/src/pages/settings/SettingsAccount.tsx b/ui/src/pages/settings/SettingsAccount.tsx index aedd746a..734e8bce 100644 --- a/ui/src/pages/settings/SettingsAccount.tsx +++ b/ui/src/pages/settings/SettingsAccount.tsx @@ -18,7 +18,7 @@ const SettingsAccount = () => { const [notificationApi, NotificationContextHolder] = notification.useNotification(); const formSchema = z.object({ - username: z.string({ message: "settings.account.form.email.placeholder" }).email({ message: t("common.errmsg.email_invalid") }), + username: z.string({ message: t("settings.account.form.email.placeholder") }).email({ message: t("common.errmsg.email_invalid") }), }); const formRule = createSchemaFieldRule(formSchema); const { diff --git a/ui/src/pages/settings/SettingsPersistence.tsx b/ui/src/pages/settings/SettingsPersistence.tsx new file mode 100644 index 00000000..e75ed8a3 --- /dev/null +++ b/ui/src/pages/settings/SettingsPersistence.tsx @@ -0,0 +1,132 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, Form, InputNumber, Skeleton, message, notification } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { produce } from "immer"; +import { z } from "zod"; + +import Show from "@/components/Show"; +import { type PersistenceSettingsContent, SETTINGS_NAMES, type SettingsModel } from "@/domain/settings"; +import { useAntdForm } from "@/hooks"; +import { get as getSettings, save as saveSettings } from "@/repository/settings"; +import { getErrMsg } from "@/utils/error"; + +const SettingsPersistence = () => { + const { t } = useTranslation(); + + const [messageApi, MessageContextHolder] = message.useMessage(); + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const [settings, setSettings] = useState>(); + const [loading, setLoading] = useState(true); + useEffect(() => { + const fetchData = async () => { + setLoading(true); + + const settings = await getSettings(SETTINGS_NAMES.PERSISTENCE); + setSettings(settings); + + setLoading(false); + }; + + fetchData(); + }, []); + + const formSchema = z.object({ + workflowRunsMaxDaysRetention: z + .number({ message: t("settings.persistence.form.workflow_runs_max_days.placeholder") }) + .gte(0, t("settings.persistence.form.workflow_runs_max_days.placeholder")), + expiredCertificatesMaxDaysRetention: z + .number({ message: t("settings.persistence.form.expired_certificates_max_days.placeholder") }) + .gte(0, t("settings.persistence.form.expired_certificates_max_days.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + const { + form: formInst, + formPending, + formProps, + } = useAntdForm>({ + initialValues: { + workflowRunsMaxDaysRetention: settings?.content?.workflowRunsMaxDaysRetention ?? 0, + expiredCertificatesMaxDaysRetention: settings?.content?.expiredCertificatesMaxDaysRetention ?? 0, + }, + onSubmit: async (values) => { + try { + await saveSettings( + produce(settings!, (draft) => { + draft.content ??= {} as PersistenceSettingsContent; + draft.content.workflowRunsMaxDaysRetention = values.workflowRunsMaxDaysRetention; + draft.content.expiredCertificatesMaxDaysRetention = values.expiredCertificatesMaxDaysRetention; + }) + ); + + messageApi.success(t("common.text.operation_succeeded")); + } catch (err) { + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + + throw err; + } + }, + }); + const [formChanged, setFormChanged] = useState(false); + + const handleInputChange = () => { + const changed = + formInst.getFieldValue("workflowRunsMaxDaysRetention") !== formProps.initialValues?.workflowRunsMaxDaysRetention || + formInst.getFieldValue("expiredCertificatesMaxDaysRetention") !== formProps.initialValues?.workflowRunsMaxDaysRetention; + setFormChanged(changed); + }; + + return ( + <> + {MessageContextHolder} + {NotificationContextHolder} + + }> +
+
+ } + rules={[formRule]} + > + + + + } + rules={[formRule]} + > + + + + + + +
+
+
+ + ); +}; + +export default SettingsPersistence; diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 0bfa8b41..923f7f6f 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -10,6 +10,7 @@ import Settings from "./pages/settings/Settings"; import SettingsAccount from "./pages/settings/SettingsAccount"; import SettingsNotification from "./pages/settings/SettingsNotification"; import SettingsPassword from "./pages/settings/SettingsPassword"; +import SettingsPersistence from "./pages/settings/SettingsPersistence"; import SettingsSSLProvider from "./pages/settings/SettingsSSLProvider"; import WorkflowDetail from "./pages/workflows/WorkflowDetail"; import WorkflowList from "./pages/workflows/WorkflowList"; @@ -64,6 +65,10 @@ export const router = createHashRouter([ path: "/settings/ssl-provider", element: , }, + { + path: "/settings/persistence", + element: , + }, ], }, ],