feat: a new status for canceled workflow run

This commit is contained in:
Fu Diwei 2025-01-22 02:47:56 +08:00
parent 8dc86209df
commit 79c1da6d14
20 changed files with 280 additions and 104 deletions

View File

@ -22,8 +22,8 @@ const (
) )
type certificateRepository interface { type certificateRepository interface {
GetById(ctx context.Context, id string) (*domain.Certificate, error)
ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error)
GetById(ctx context.Context, id string) (*domain.Certificate, error)
} }
type CertificateService struct { type CertificateService struct {

View File

@ -1,6 +1,9 @@
package domain package domain
import "time" import (
"strings"
"time"
)
const CollectionNameWorkflowRun = "workflow_run" const CollectionNameWorkflowRun = "workflow_run"
@ -22,6 +25,7 @@ const (
WorkflowRunStatusTypeRunning WorkflowRunStatusType = "running" WorkflowRunStatusTypeRunning WorkflowRunStatusType = "running"
WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded" WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded"
WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed" WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed"
WorkflowRunStatusTypeCanceled WorkflowRunStatusType = "canceled"
) )
type WorkflowRunLog struct { type WorkflowRunLog struct {
@ -40,12 +44,13 @@ type WorkflowRunLogOutput struct {
type WorkflowRunLogs []WorkflowRunLog type WorkflowRunLogs []WorkflowRunLog
func (r WorkflowRunLogs) FirstError() string { func (r WorkflowRunLogs) ErrorString() string {
var builder strings.Builder
for _, log := range r { for _, log := range r {
if log.Error != "" { if log.Error != "" {
return log.Error builder.WriteString(log.Error)
builder.WriteString("\n")
} }
} }
return builder.String()
return ""
} }

View File

@ -1,12 +1,14 @@
package certs package certs
import ( import (
"crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors" "errors"
"github.com/go-acme/lego/v4/certcrypto"
xerrors "github.com/pkg/errors" xerrors "github.com/pkg/errors"
) )
@ -34,6 +36,19 @@ func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error)
return cert, nil return cert, nil
} }
// 从 PEM 编码的私钥字符串解析并返回一个 crypto.PrivateKey 对象。
//
// 入参:
// - privkeyPem: 私钥 PEM 内容。
//
// 出参:
// - privkey: crypto.PrivateKey 对象,可能是 rsa.PrivateKey、ecdsa.PrivateKey 或 ed25519.PrivateKey。
// - err: 错误。
func ParsePrivateKeyFromPEM(privkeyPem string) (privkey crypto.PrivateKey, err error) {
pemData := []byte(privkeyPem)
return certcrypto.ParsePEMPrivateKey(pemData)
}
// 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。 // 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。
// //
// 入参: // 入参:

View File

@ -6,7 +6,6 @@ import (
"errors" "errors"
"time" "time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/pavlo-v-chernykh/keystore-go/v4" "github.com/pavlo-v-chernykh/keystore-go/v4"
"software.sslmate.com/src/go-pkcs12" "software.sslmate.com/src/go-pkcs12"
) )
@ -27,7 +26,7 @@ func TransformCertificateFromPEMToPFX(certPem string, privkeyPem string, pfxPass
return nil, err return nil, err
} }
privkey, err := certcrypto.ParsePEMPrivateKey([]byte(privkeyPem)) privkey, err := ParsePrivateKeyFromPEM(privkeyPem)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -134,7 +134,7 @@ func (a *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24
expirationTime := time.Until(lastCertificate.ExpireAt) expirationTime := time.Until(lastCertificate.ExpireAt)
if lastCertificate != nil && expirationTime > renewalInterval { if lastCertificate != nil && expirationTime > renewalInterval {
return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,距 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,预计距 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays)
} }
} }

View File

@ -19,15 +19,15 @@ func NewWorkflowProcessor(workflow *domain.Workflow) *workflowProcessor {
} }
} }
func (w *workflowProcessor) Log(ctx context.Context) []domain.WorkflowRunLog {
return w.logs
}
func (w *workflowProcessor) Run(ctx context.Context) error { func (w *workflowProcessor) Run(ctx context.Context) error {
ctx = setContextWorkflowId(ctx, w.workflow.Id) ctx = setContextWorkflowId(ctx, w.workflow.Id)
return w.processNode(ctx, w.workflow.Content) return w.processNode(ctx, w.workflow.Content)
} }
func (w *workflowProcessor) GetRunLogs() []domain.WorkflowRunLog {
return w.logs
}
func (w *workflowProcessor) processNode(ctx context.Context, node *domain.WorkflowNode) error { func (w *workflowProcessor) processNode(ctx context.Context, node *domain.WorkflowNode) error {
current := node current := node
for current != nil { for current != nil {
@ -39,26 +39,26 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl
} }
} }
var runErr error
var processor nodes.NodeProcessor var processor nodes.NodeProcessor
var runErr error
for { for {
if current.Type == domain.WorkflowNodeTypeBranch || current.Type == domain.WorkflowNodeTypeExecuteResultBranch { if current.Type != domain.WorkflowNodeTypeBranch && current.Type != domain.WorkflowNodeTypeExecuteResultBranch {
break processor, runErr = nodes.GetProcessor(current)
if runErr != nil {
break
}
runErr = processor.Run(ctx)
log := processor.Log(ctx)
if log != nil {
w.logs = append(w.logs, *log)
}
if runErr != nil {
break
}
} }
processor, runErr = nodes.GetProcessor(current) break
if runErr != nil {
break
}
runErr = processor.Run(ctx)
log := processor.Log(ctx)
if log != nil {
w.logs = append(w.logs, *log)
}
if runErr != nil {
break
}
} }
if runErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch { if runErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch {

View File

@ -35,35 +35,20 @@ type WorkflowService struct {
} }
func NewWorkflowService(repo workflowRepository) *WorkflowService { func NewWorkflowService(repo workflowRepository) *WorkflowService {
rs := &WorkflowService{ srv := &WorkflowService{
repo: repo, repo: repo,
ch: make(chan *workflowRunData, 1), ch: make(chan *workflowRunData, 1),
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
rs.cancel = cancel srv.cancel = cancel
rs.wg.Add(defaultRoutines) srv.wg.Add(defaultRoutines)
for i := 0; i < defaultRoutines; i++ { for i := 0; i < defaultRoutines; i++ {
go rs.process(ctx) go srv.run(ctx)
} }
return rs return srv
}
func (s *WorkflowService) process(ctx context.Context) {
defer s.wg.Done()
for {
select {
case data := <-s.ch:
// 执行
if err := s.run(ctx, data); err != nil {
app.GetLogger().Error("failed to run workflow", "id", data.Workflow.Id, "err", err)
}
case <-ctx.Done():
return
}
}
} }
func (s *WorkflowService) InitSchedule(ctx context.Context) error { func (s *WorkflowService) InitSchedule(ctx context.Context) error {
@ -90,7 +75,6 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error {
} }
func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) error { func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) error {
// 查询
workflow, err := s.repo.GetById(ctx, req.WorkflowId) workflow, err := s.repo.GetById(ctx, req.WorkflowId)
if err != nil { if err != nil {
app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err) app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err)
@ -101,9 +85,8 @@ func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) err
return errors.New("workflow is running") return errors.New("workflow is running")
} }
// set last run
workflow.LastRunTime = time.Now() workflow.LastRunTime = time.Now()
workflow.LastRunStatus = domain.WorkflowRunStatusTypeRunning workflow.LastRunStatus = domain.WorkflowRunStatusTypePending
workflow.LastRunId = "" workflow.LastRunId = ""
if err := s.repo.Save(ctx, workflow); err != nil { if err := s.repo.Save(ctx, workflow); err != nil {
@ -118,42 +101,56 @@ func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) err
return nil return nil
} }
func (s *WorkflowService) run(ctx context.Context, runData *workflowRunData) error { func (s *WorkflowService) Stop(ctx context.Context) {
// 执行 s.cancel()
s.wg.Wait()
}
func (s *WorkflowService) run(ctx context.Context) {
defer s.wg.Done()
for {
select {
case data := <-s.ch:
if err := s.runWithData(ctx, data); err != nil {
app.GetLogger().Error("failed to run workflow", "id", data.Workflow.Id, "err", err)
}
case <-ctx.Done():
return
}
}
}
func (s *WorkflowService) runWithData(ctx context.Context, runData *workflowRunData) error {
workflow := runData.Workflow workflow := runData.Workflow
run := &domain.WorkflowRun{ run := &domain.WorkflowRun{
WorkflowId: workflow.Id, WorkflowId: workflow.Id,
Status: domain.WorkflowRunStatusTypeRunning, Status: domain.WorkflowRunStatusTypeRunning,
Trigger: runData.RunTrigger, Trigger: runData.RunTrigger,
StartedAt: time.Now(), StartedAt: time.Now(),
EndedAt: time.Now(),
} }
processor := processor.NewWorkflowProcessor(workflow) processor := processor.NewWorkflowProcessor(workflow)
if err := processor.Run(ctx); err != nil { if runErr := processor.Run(ctx); runErr != nil {
run.Status = domain.WorkflowRunStatusTypeFailed run.Status = domain.WorkflowRunStatusTypeFailed
run.EndedAt = time.Now() run.EndedAt = time.Now()
run.Logs = processor.Log(ctx) run.Logs = processor.GetRunLogs()
run.Error = err.Error() run.Error = runErr.Error()
if err := s.repo.SaveRun(ctx, run); err != nil { if err := s.repo.SaveRun(ctx, run); err != nil {
app.GetLogger().Error("failed to save workflow run", "err", err) app.GetLogger().Error("failed to save workflow run", "err", err)
} }
return fmt.Errorf("failed to run workflow: %w", err) return fmt.Errorf("failed to run workflow: %w", runErr)
} }
// 保存日志
logs := processor.Log(ctx)
runStatus := domain.WorkflowRunStatusTypeSucceeded
runError := domain.WorkflowRunLogs(logs).FirstError()
if runError != "" {
runStatus = domain.WorkflowRunStatusTypeFailed
}
run.Status = runStatus
run.EndedAt = time.Now() run.EndedAt = time.Now()
run.Logs = processor.Log(ctx) run.Logs = processor.GetRunLogs()
run.Error = runError run.Error = domain.WorkflowRunLogs(run.Logs).ErrorString()
if run.Error == "" {
run.Status = domain.WorkflowRunStatusTypeSucceeded
} else {
run.Status = domain.WorkflowRunStatusTypeFailed
}
if err := s.repo.SaveRun(ctx, run); err != nil { if err := s.repo.SaveRun(ctx, run); err != nil {
app.GetLogger().Error("failed to save workflow run", "err", err) app.GetLogger().Error("failed to save workflow run", "err", err)
return err return err
@ -161,8 +158,3 @@ func (s *WorkflowService) run(ctx context.Context, runData *workflowRunData) err
return nil return nil
} }
func (s *WorkflowService) Stop(ctx context.Context) {
s.cancel()
s.wg.Wait()
}

View File

@ -0,0 +1,65 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{
"hidden": false,
"id": "zivdxh23",
"maxSelect": 1,
"name": "lastRunStatus",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"pending",
"running",
"succeeded",
"failed",
"canceled"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{
"hidden": false,
"id": "zivdxh23",
"maxSelect": 1,
"name": "lastRunStatus",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"pending",
"running",
"succeeded",
"failed"
]
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@ -0,0 +1,65 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"hidden": false,
"id": "qldmh0tw",
"maxSelect": 1,
"name": "status",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"pending",
"running",
"succeeded",
"failed",
"canceled"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"hidden": false,
"id": "qldmh0tw",
"maxSelect": 1,
"name": "status",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"pending",
"running",
"succeeded",
"failed"
]
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@ -41,11 +41,11 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
</Show> </Show>
<div className="mt-4 rounded-md bg-black p-4 text-stone-200"> <div className="mt-4 rounded-md bg-black p-4 text-stone-200">
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-4">
{data!.logs?.map((item, i) => { {data!.logs?.map((item, i) => {
return ( return (
<div key={i} className="flex flex-col space-y-2"> <div key={i} className="flex flex-col space-y-2">
<div>{item.nodeName}</div> <div className="font-semibold">{item.nodeName}</div>
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
{item.outputs?.map((output, j) => { {item.outputs?.map((output, j) => {
return ( return (

View File

@ -5,6 +5,7 @@ import {
ClockCircleOutlined as ClockCircleOutlinedIcon, ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon,
PauseCircleOutlined as PauseCircleOutlinedIcon,
SelectOutlined as SelectOutlinedIcon, SelectOutlined as SelectOutlinedIcon,
SyncOutlined as SyncOutlinedIcon, SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
@ -70,6 +71,12 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
{t("workflow_run.props.status.failed")} {t("workflow_run.props.status.failed")}
</Tag> </Tag>
); );
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
return (
<Tag icon={<PauseCircleOutlinedIcon />} color="warning">
{t("workflow_run.props.status.canceled")}
</Tag>
);
} }
return <></>; return <></>;
@ -133,7 +140,11 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
<Button <Button
color="danger" color="danger"
danger danger
disabled={record.status !== WORKFLOW_RUN_STATUSES.SUCCEEDED && record.status !== WORKFLOW_RUN_STATUSES.FAILED} disabled={
record.status !== WORKFLOW_RUN_STATUSES.SUCCEEDED &&
record.status !== WORKFLOW_RUN_STATUSES.FAILED &&
record.status !== WORKFLOW_RUN_STATUSES.CANCELED
}
icon={<DeleteOutlinedIcon />} icon={<DeleteOutlinedIcon />}
variant="text" variant="text"
onClick={() => { onClick={() => {

View File

@ -1,7 +1,7 @@
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons"; import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography } from "antd"; import { Alert, Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography } from "antd";
import { createSchemaFieldRule } from "antd-zod"; import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod"; import { z } from "zod";
@ -310,6 +310,15 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
</Form.Item> </Form.Item>
</Form.Item> </Form.Item>
<Show when={fieldProvider === DEPLOY_PROVIDERS.LOCAL}>
<Form.Item>
<Alert
type="info"
message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.provider_access.guide_for_local") }}></span>}
/>
</Form.Item>
</Show>
<Form.Item <Form.Item
name="certificate" name="certificate"
label={t("workflow_node.deploy.form.certificate.label")} label={t("workflow_node.deploy.form.certificate.label")}

View File

@ -136,7 +136,7 @@ const StartNodeConfigForm = forwardRef<StartNodeConfigFormInstance, StartNodeCon
<Show when={fieldTrigger === WORKFLOW_TRIGGERS.AUTO}> <Show when={fieldTrigger === WORKFLOW_TRIGGERS.AUTO}>
<Form.Item> <Form.Item>
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.start.form.trigger_cron_alert.content") }}></span>} /> <Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.start.form.trigger_cron.guide") }}></span>} />
</Form.Item> </Form.Item>
</Show> </Show>
</Form> </Form>

View File

@ -32,6 +32,7 @@ export const WORKFLOW_RUN_STATUSES = Object.freeze({
RUNNING: "running", RUNNING: "running",
SUCCEEDED: "succeeded", SUCCEEDED: "succeeded",
FAILED: "failed", FAILED: "failed",
CANCELED: "canceled",
} as const); } as const);
export type WorkflorRunStatusType = (typeof WORKFLOW_RUN_STATUSES)[keyof typeof WORKFLOW_RUN_STATUSES]; export type WorkflorRunStatusType = (typeof WORKFLOW_RUN_STATUSES)[keyof typeof WORKFLOW_RUN_STATUSES];

View File

@ -20,7 +20,7 @@
"workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression", "workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression",
"workflow_node.start.form.trigger_cron.tooltip": "Time zone is based on the server.", "workflow_node.start.form.trigger_cron.tooltip": "Time zone is based on the server.",
"workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:", "workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
"workflow_node.start.form.trigger_cron_alert.content": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Lets Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Lets Encrypt (ACME) client run at a random time?</a>", "workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Lets Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Lets Encrypt (ACME) client run at a random time?</a>",
"workflow_node.apply.label": "Application", "workflow_node.apply.label": "Application",
"workflow_node.apply.form.domains.label": "Domains", "workflow_node.apply.form.domains.label": "Domains",
@ -82,6 +82,7 @@
"workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of host provider", "workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of host provider",
"workflow_node.deploy.form.provider_access.tooltip": "Used to deploy certificates.", "workflow_node.deploy.form.provider_access.tooltip": "Used to deploy certificates.",
"workflow_node.deploy.form.provider_access.button": "Create", "workflow_node.deploy.form.provider_access.button": "Create",
"workflow_node.deploy.form.provider_access.guide_for_local": "Tips: Due to the form validations, youe need to select an authorization for local deployment also, even if it means nothing.",
"workflow_node.deploy.form.certificate.label": "Certificate", "workflow_node.deploy.form.certificate.label": "Certificate",
"workflow_node.deploy.form.certificate.placeholder": "Please select certificate", "workflow_node.deploy.form.certificate.placeholder": "Please select certificate",
"workflow_node.deploy.form.certificate.tooltip": "The certificate to be deployed comes from the previous application stage node.", "workflow_node.deploy.form.certificate.tooltip": "The certificate to be deployed comes from the previous application stage node.",

View File

@ -9,6 +9,7 @@
"workflow_run.props.status.running": "Running", "workflow_run.props.status.running": "Running",
"workflow_run.props.status.succeeded": "Succeeded", "workflow_run.props.status.succeeded": "Succeeded",
"workflow_run.props.status.failed": "Failed", "workflow_run.props.status.failed": "Failed",
"workflow_run.props.status.canceled": "Canceled",
"workflow_run.props.trigger": "Trigger", "workflow_run.props.trigger": "Trigger",
"workflow_run.props.trigger.auto": "Timing", "workflow_run.props.trigger.auto": "Timing",
"workflow_run.props.trigger.manual": "Manual", "workflow_run.props.trigger.manual": "Manual",

View File

@ -20,7 +20,7 @@
"workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式", "workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式",
"workflow_node.start.form.trigger_cron.tooltip": "支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式,时区以服务器设置为准。", "workflow_node.start.form.trigger_cron.tooltip": "支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式,时区以服务器设置为准。",
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:", "workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
"workflow_node.start.form.trigger_cron_alert.content": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Lets Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Lets Encrypt (ACME) 客户端启动时间应当随机?</a>", "workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Lets Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Lets Encrypt (ACME) 客户端启动时间应当随机?</a>",
"workflow_node.apply.label": "申请", "workflow_node.apply.label": "申请",
"workflow_node.apply.form.domains.label": "域名", "workflow_node.apply.form.domains.label": "域名",
@ -37,6 +37,7 @@
"workflow_node.apply.form.provider_access.placeholder": "请选择 DNS 提供商授权", "workflow_node.apply.form.provider_access.placeholder": "请选择 DNS 提供商授权",
"workflow_node.apply.form.provider_access.tooltip": "用于 ACME DNS-01 认证时操作域名解析记录,注意与部署阶段所需的主机提供商相区分。", "workflow_node.apply.form.provider_access.tooltip": "用于 ACME DNS-01 认证时操作域名解析记录,注意与部署阶段所需的主机提供商相区分。",
"workflow_node.apply.form.provider_access.button": "新建", "workflow_node.apply.form.provider_access.button": "新建",
"workflow_node.deploy.form.provider_access.guide_for_local": "小贴士:由于表单限制,你同样需要为本地部署选择一个授权 —— 即使它是空白的。",
"workflow_node.apply.form.aws_route53_region.label": "AWS Route53 区域", "workflow_node.apply.form.aws_route53_region.label": "AWS Route53 区域",
"workflow_node.apply.form.aws_route53_region.placeholder": "请输入 AWS Route53 区域例如us-east-1", "workflow_node.apply.form.aws_route53_region.placeholder": "请输入 AWS Route53 区域例如us-east-1",
"workflow_node.apply.form.aws_route53_region.tooltip": "这是什么?请参阅 <a href=\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\" tworkflow_node.applyank\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>", "workflow_node.apply.form.aws_route53_region.tooltip": "这是什么?请参阅 <a href=\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\" tworkflow_node.applyank\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>",
@ -385,7 +386,7 @@
"workflow_node.execute_result_branch.label": "执行结果分支", "workflow_node.execute_result_branch.label": "执行结果分支",
"workflow_node.execute_success.label": "若前节点执行成功…", "workflow_node.execute_success.label": "若前节点执行成功…",
"workflow_node.execute_failure.label": "若前节点执行失败…" "workflow_node.execute_failure.label": "若前节点执行失败…"
} }

View File

@ -7,8 +7,9 @@
"workflow_run.props.status": "状态", "workflow_run.props.status": "状态",
"workflow_run.props.status.pending": "等待执行", "workflow_run.props.status.pending": "等待执行",
"workflow_run.props.status.running": "执行中", "workflow_run.props.status.running": "执行中",
"workflow_run.props.status.succeeded": "成功", "workflow_run.props.status.succeeded": "已成功",
"workflow_run.props.status.failed": "失败", "workflow_run.props.status.failed": "已失败",
"workflow_run.props.status.canceled": "已取消",
"workflow_run.props.trigger": "执行方式", "workflow_run.props.trigger": "执行方式",
"workflow_run.props.trigger.auto": "定时执行", "workflow_run.props.trigger.auto": "定时执行",
"workflow_run.props.trigger.manual": "手动执行", "workflow_run.props.trigger.manual": "手动执行",

View File

@ -2,15 +2,16 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import {
ApiOutlined, ApiOutlined as ApiOutlinedIcon,
CheckCircleOutlined, CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined, ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined, CloseCircleOutlined as CloseCircleOutlinedIcon,
LockOutlined, LockOutlined as LockOutlinedIcon,
PlusOutlined, PauseCircleOutlined as PauseCircleOutlinedIcon,
SelectOutlined, PlusOutlined as PlusOutlinedIcon,
SendOutlined, SelectOutlined as SelectOutlinedIcon,
SyncOutlined, SendOutlined as SendOutlinedIcon,
SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-components"; import { PageHeader } from "@ant-design/pro-components";
import { useRequest } from "ahooks"; import { useRequest } from "ahooks";
@ -99,25 +100,31 @@ const Dashboard = () => {
ellipsis: true, ellipsis: true,
render: (_, record) => { render: (_, record) => {
if (record.status === WORKFLOW_RUN_STATUSES.PENDING) { if (record.status === WORKFLOW_RUN_STATUSES.PENDING) {
return <Tag icon={<ClockCircleOutlined />}>{t("workflow_run.props.status.pending")}</Tag>; return <Tag icon={<ClockCircleOutlinedIcon />}>{t("workflow_run.props.status.pending")}</Tag>;
} else if (record.status === WORKFLOW_RUN_STATUSES.RUNNING) { } else if (record.status === WORKFLOW_RUN_STATUSES.RUNNING) {
return ( return (
<Tag icon={<SyncOutlined spin />} color="processing"> <Tag icon={<SyncOutlinedIcon spin />} color="processing">
{t("workflow_run.props.status.running")} {t("workflow_run.props.status.running")}
</Tag> </Tag>
); );
} else if (record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED) { } else if (record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
return ( return (
<Tag icon={<CheckCircleOutlined />} color="success"> <Tag icon={<CheckCircleOutlinedIcon />} color="success">
{t("workflow_run.props.status.succeeded")} {t("workflow_run.props.status.succeeded")}
</Tag> </Tag>
); );
} else if (record.status === WORKFLOW_RUN_STATUSES.FAILED) { } else if (record.status === WORKFLOW_RUN_STATUSES.FAILED) {
return ( return (
<Tag icon={<CloseCircleOutlined />} color="error"> <Tag icon={<CloseCircleOutlinedIcon />} color="error">
{t("workflow_run.props.status.failed")} {t("workflow_run.props.status.failed")}
</Tag> </Tag>
); );
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
return (
<Tag icon={<PauseCircleOutlinedIcon />} color="warning">
{t("workflow_run.props.status.canceled")}
</Tag>
);
} }
return <></>; return <></>;
@ -153,7 +160,7 @@ const Dashboard = () => {
width: 120, width: 120,
render: (_, record) => ( render: (_, record) => (
<Button.Group> <Button.Group>
<WorkflowRunDetailDrawer data={record} trigger={<Button color="primary" icon={<SelectOutlined />} variant="text" />} /> <WorkflowRunDetailDrawer data={record} trigger={<Button color="primary" icon={<SelectOutlinedIcon />} variant="text" />} />
</Button.Group> </Button.Group>
), ),
}, },
@ -248,16 +255,16 @@ const Dashboard = () => {
<Flex justify="stretch" vertical={!breakpoints.lg} gap={16}> <Flex justify="stretch" vertical={!breakpoints.lg} gap={16}>
<Card className="max-lg:flex-1 lg:w-[360px]" title={t("dashboard.quick_actions")}> <Card className="max-lg:flex-1 lg:w-[360px]" title={t("dashboard.quick_actions")}>
<Space className="w-full" direction="vertical" size="large"> <Space className="w-full" direction="vertical" size="large">
<Button block type="primary" size="large" icon={<PlusOutlined />} onClick={() => navigate("/workflows/new")}> <Button block type="primary" size="large" icon={<PlusOutlinedIcon />} onClick={() => navigate("/workflows/new")}>
{t("dashboard.quick_actions.create_workflow")} {t("dashboard.quick_actions.create_workflow")}
</Button> </Button>
<Button block size="large" icon={<LockOutlined />} onClick={() => navigate("/settings/password")}> <Button block size="large" icon={<LockOutlinedIcon />} onClick={() => navigate("/settings/password")}>
{t("dashboard.quick_actions.change_login_password")} {t("dashboard.quick_actions.change_login_password")}
</Button> </Button>
<Button block size="large" icon={<SendOutlined />} onClick={() => navigate("/settings/notification")}> <Button block size="large" icon={<SendOutlinedIcon />} onClick={() => navigate("/settings/notification")}>
{t("dashboard.quick_actions.cofigure_notification")} {t("dashboard.quick_actions.cofigure_notification")}
</Button> </Button>
<Button block size="large" icon={<ApiOutlined />} onClick={() => navigate("/settings/ssl-provider")}> <Button block size="large" icon={<ApiOutlinedIcon />} onClick={() => navigate("/settings/ssl-provider")}>
{t("dashboard.quick_actions.configure_ca")} {t("dashboard.quick_actions.configure_ca")}
</Button> </Button>
</Space> </Space>

View File

@ -7,6 +7,7 @@ import {
CloseCircleOutlined as CloseCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon,
EditOutlined as EditOutlinedIcon, EditOutlined as EditOutlinedIcon,
PauseCircleOutlined as PauseCircleOutlinedIcon,
PlusOutlined as PlusOutlinedIcon, PlusOutlined as PlusOutlinedIcon,
SyncOutlined as SyncOutlinedIcon, SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
@ -168,6 +169,8 @@ const WorkflowList = () => {
icon = <CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />; icon = <CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />;
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) { } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) {
icon = <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />; icon = <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />;
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.CANCELED) {
icon = <PauseCircleOutlinedIcon style={{ color: themeToken.colorWarning }} />;
} }
return ( return (