Add workflow execution process

This commit is contained in:
yoan 2024-11-18 19:40:24 +08:00
parent bde2147dd3
commit 775b12aec1
23 changed files with 741 additions and 21 deletions

View File

@ -1,6 +1,7 @@
package applicant package applicant
import ( import (
"context"
"crypto" "crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
@ -170,7 +171,38 @@ func Get(record *models.Record) (Applicant, error) {
DisableFollowCNAME: applyConfig.DisableFollowCNAME, DisableFollowCNAME: applyConfig.DisableFollowCNAME,
} }
switch access.GetString("configType") { return GetWithTypeOption(access.GetString("configType"), option)
}
func GetWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
// 获取授权配置
accessRepo := repository.NewAccessRepository()
access, err := accessRepo.GetById(context.Background(), node.GetConfigString("access"))
if err != nil {
return nil, fmt.Errorf("access record not found: %w", err)
}
timeout := node.GetConfigInt64("timeout")
if timeout == 0 {
timeout = defaultTimeout
}
applyConfig := &ApplyOption{
Email: node.GetConfigString("email"),
Domain: node.GetConfigString("domain"),
Access: access.Config,
KeyAlgorithm: node.GetConfigString("keyAlgorithm"),
Nameservers: node.GetConfigString("nameservers"),
Timeout: timeout,
DisableFollowCNAME: node.GetConfigBool("disableFollowCNAME"),
}
return GetWithTypeOption(access.ConfigType, applyConfig)
}
func GetWithTypeOption(t string, option *ApplyOption) (Applicant, error) {
switch t {
case configTypeAliyun: case configTypeAliyun:
return NewAliyun(option), nil return NewAliyun(option), nil
case configTypeTencent: case configTypeTencent:

View File

@ -1,5 +1,16 @@
package domain package domain
import "time"
type Access struct {
Meta
Name string `json:"name"`
Config string `json:"config"`
ConfigType string `json:"configType"`
Deleted time.Time `json:"deleted"`
Usage string `json:"usage"`
}
type AliyunAccess struct { type AliyunAccess struct {
AccessKeyId string `json:"accessKeyId"` AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"` AccessKeySecret string `json:"accessKeySecret"`

View File

@ -0,0 +1,39 @@
package domain
import "time"
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"`
ExpireAt time.Time `json:"ExpireAt"`
}
type MetaData struct {
Version string `json:"version"`
SerialNumber string `json:"serialNumber"`
Validity CertificateValidity `json:"validity"`
SignatureAlgorithm string `json:"signatureAlgorithm"`
Issuer CertificateIssuer `json:"issuer"`
Subject CertificateSubject `json:"subject"`
}
type CertificateIssuer struct {
Country string `json:"country"`
Organization string `json:"organization"`
CommonName string `json:"commonName"`
}
type CertificateSubject struct {
CN string `json:"CN"`
}
type CertificateValidity struct {
NotBefore string `json:"notBefore"`
NotAfter string `json:"notAfter"`
}

View File

@ -0,0 +1,9 @@
package domain
import "time"
type Meta struct {
Id string `json:"id"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}

View File

@ -1,6 +1,16 @@
package domain package domain
var ErrAuthFailed = NewXError(4999, "auth failed") var (
ErrInvalidParams = NewXError(400, "invalid params")
ErrRecordNotFound = NewXError(404, "record not found")
)
func IsRecordNotFound(err error) bool {
if e, ok := err.(*XError); ok {
return e.GetCode() == ErrRecordNotFound.GetCode()
}
return false
}
type XError struct { type XError struct {
Code int `json:"code"` Code int `json:"code"`

View File

@ -1,9 +1,22 @@
package domain package domain
import "time" import (
"fmt"
"strconv"
)
const (
WorkflowNodeTypeStart = "start"
WorkflowNodeTypeEnd = "end"
WorkflowNodeTypeApply = "apply"
WorkflowNodeTypeDeply = "deploy"
WorkflowNodeTypeNotify = "notify"
WorkflowNodeTypeBranch = "branch"
WorkflowNodeTypeCondition = "condition"
)
type Workflow struct { type Workflow struct {
Id string `json:"id"` Meta
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Type string `json:"type"` Type string `json:"type"`
@ -11,8 +24,6 @@ type Workflow struct {
Draft *WorkflowNode `json:"draft"` Draft *WorkflowNode `json:"draft"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
HasDraft bool `json:"hasDraft"` HasDraft bool `json:"hasDraft"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
} }
type WorkflowNode struct { type WorkflowNode struct {
@ -29,11 +40,48 @@ type WorkflowNode struct {
Branches []WorkflowNode `json:"branches"` Branches []WorkflowNode `json:"branches"`
} }
func (n *WorkflowNode) GetConfigString(key string) string {
if v, ok := n.Config[key]; ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
func (n *WorkflowNode) GetConfigBool(key string) bool {
if v, ok := n.Config[key]; ok {
if b, ok := v.(bool); ok {
return b
}
}
return false
}
func (n *WorkflowNode) GetConfigInt64(key string) int64 {
// 先转成字符串,再转成 int64
if v, ok := n.Config[key]; ok {
temp := fmt.Sprintf("%v", v)
if i, err := strconv.ParseInt(temp, 10, 64); err == nil {
return i
}
}
return 0
}
type WorkflowNodeIo struct { type WorkflowNodeIo struct {
Label string `json:"label"` Label string `json:"label"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Required bool `json:"required"` Required bool `json:"required"`
Value any `json:"value"`
ValueSelector WorkflowNodeIoValueSelector `json:"valueSelector"`
}
type WorkflowNodeIoValueSelector struct {
Id string `json:"id"`
Name string `json:"name"`
} }
type WorkflowRunReq struct { type WorkflowRunReq struct {

View File

@ -0,0 +1,12 @@
package domain
const WorkflowOutputCertificate = "certificate"
type WorkflowOutput struct {
Meta
Workflow string `json:"workflow"`
NodeId string `json:"nodeId"`
Node *WorkflowNode `json:"node"`
Output []WorkflowNodeIo `json:"output"`
Succeed bool `json:"succeed"`
}

View File

@ -0,0 +1,17 @@
package repository
import (
"context"
"github.com/usual2970/certimate/internal/domain"
)
type AccessRepository struct{}
func NewAccessRepository() *AccessRepository {
return &AccessRepository{}
}
func (a *AccessRepository) GetById(ctx context.Context, id string) (*domain.Access, error) {
return nil, nil
}

View File

@ -0,0 +1,54 @@
package repository
import (
"context"
"database/sql"
"errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/app"
)
type WorkflowRepository struct{}
func NewWorkflowRepository() *WorkflowRepository {
return &WorkflowRepository{}
}
func (w *WorkflowRepository) Get(ctx context.Context, id string) (*domain.Workflow, error) {
record, err := app.GetApp().Dao().FindRecordById("workflow", id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrRecordNotFound
}
return nil, err
}
content := &domain.WorkflowNode{}
if err := record.UnmarshalJSONField("content", content); err != nil {
return nil, err
}
draft := &domain.WorkflowNode{}
if err := record.UnmarshalJSONField("draft", draft); err != nil {
return nil, err
}
workflow := &domain.Workflow{
Meta: domain.Meta{
Id: record.GetId(),
Created: record.GetTime("created"),
Updated: record.GetTime("updated"),
},
Name: record.GetString("name"),
Description: record.GetString("description"),
Type: record.GetString("type"),
Enabled: record.GetBool("enabled"),
HasDraft: record.GetBool("hasDraft"),
Content: content,
Draft: draft,
}
return workflow, nil
}

View File

@ -0,0 +1,105 @@
package repository
import (
"context"
"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"
)
type WorkflowOutputRepository struct{}
func NewWorkflowOutputRepository() *WorkflowOutputRepository {
return &WorkflowOutputRepository{}
}
func (w *WorkflowOutputRepository) Get(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error) {
record, err := app.GetApp().Dao().FindFirstRecordByFilter("workflow_output", "nodeId={:nodeId}", dbx.Params{"nodeId": nodeId})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrRecordNotFound
}
return nil, err
}
node := &domain.WorkflowNode{}
if err := record.UnmarshalJSONField("node", node); err != nil {
return nil, errors.New("failed to unmarshal node")
}
output := make([]domain.WorkflowNodeIo, 0)
if err := record.UnmarshalJSONField("output", &output); err != nil {
return nil, errors.New("failed to unmarshal output")
}
rs := &domain.WorkflowOutput{
Meta: domain.Meta{
Id: record.GetId(),
Created: record.GetTime("created"),
Updated: record.GetTime("updated"),
},
Workflow: record.GetString("workflow"),
NodeId: record.GetString("nodeId"),
Node: node,
Output: output,
}
return rs, nil
}
// 保存节点输出
func (w *WorkflowOutputRepository) Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error {
var record *models.Record
var err error
if output.Id == "" {
collection, err := app.GetApp().Dao().FindCollectionByNameOrId("workflow_output")
if err != nil {
return err
}
record = models.NewRecord(collection)
} else {
record, err = app.GetApp().Dao().FindRecordById("workflow_output", output.Id)
if err != nil {
return err
}
}
record.Set("workflow", output.Workflow)
record.Set("nodeId", output.NodeId)
record.Set("node", output.Node)
record.Set("output", output.Output)
if err := app.GetApp().Dao().SaveRecord(record); err != nil {
return err
}
if cb != nil && certificate != nil {
if err := cb(record.GetId()); err != nil {
return err
}
certCollection, err := app.GetApp().Dao().FindCollectionByNameOrId("certificate")
if err != nil {
return err
}
certRecord := models.NewRecord(certCollection)
certRecord.Set("certificate", certificate.Certificate)
certRecord.Set("privateKey", certificate.PrivateKey)
certRecord.Set("issuerCertificate", certificate.IssuerCertificate)
certRecord.Set("san", certificate.SAN)
certRecord.Set("workflowOutput", certificate.Output)
certRecord.Set("expireAt", certificate.ExpireAt)
certRecord.Set("certUrl", certificate.CertUrl)
certRecord.Set("certStableUrl", certificate.CertStableUrl)
if err := app.GetApp().Dao().SaveRecord(certRecord); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,103 @@
package nodeprocessor
import (
"context"
"github.com/usual2970/certimate/internal/applicant"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
"github.com/usual2970/certimate/internal/repository"
"github.com/usual2970/certimate/internal/utils/xtime"
)
type applyNode struct {
node *domain.WorkflowNode
outputRepo WorkflowOutputRepository
*Logger
}
func NewApplyNode(node *domain.WorkflowNode) *applyNode {
return &applyNode{
node: node,
Logger: NewLogger(node),
outputRepo: repository.NewWorkflowOutputRepository(),
}
}
type WorkflowOutputRepository interface {
// 查询节点输出
Get(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error)
// 保存节点输出
Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error
}
// 申请节点根据申请类型执行不同的操作
func (a *applyNode) Run(ctx context.Context) error {
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "开始执行")
// 查询是否申请过,已申请过则直接返回(先保持和 v0.2 一致)
output, err := a.outputRepo.Get(ctx, a.node.Id)
if err != nil && !domain.IsRecordNotFound(err) {
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "查询申请记录失败", err.Error())
return err
}
if output != nil && output.Succeed {
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "已申请过")
return nil
}
// 获取Applicant
apply, err := applicant.GetWithApplyNode(a.node)
if err != nil {
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "获取申请对象失败", err.Error())
return err
}
// 申请
certificate, err := apply.Apply()
if err != nil {
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "申请失败", err.Error())
return err
}
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "申请成功")
// 记录申请结果
output = &domain.WorkflowOutput{
Workflow: GetWorkflowId(ctx),
NodeId: a.node.Id,
Node: a.node,
Succeed: true,
}
cert, err := x509.ParseCertificateFromPEM(certificate.Certificate)
if err != nil {
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "解析证书失败", err.Error())
return err
}
certificateRecord := &domain.Certificate{
SAN: cert.Subject.CommonName,
Certificate: certificate.Certificate,
PrivateKey: certificate.PrivateKey,
IssuerCertificate: certificate.IssuerCertificate,
CertUrl: certificate.CertUrl,
CertStableUrl: certificate.CertStableUrl,
ExpireAt: cert.NotAfter,
}
if err := a.outputRepo.Save(ctx, output, certificateRecord, func(id string) error {
if certificateRecord != nil {
certificateRecord.Id = id
}
return nil
}); err != nil {
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "保存申请记录失败", err.Error())
return err
}
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "保存申请记录成功")
return nil
}

View File

@ -0,0 +1,29 @@
package nodeprocessor
import (
"context"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/xtime"
)
type conditionNode struct {
node *domain.WorkflowNode
*Logger
}
func NewConditionNode(node *domain.WorkflowNode) *conditionNode {
return &conditionNode{
node: node,
Logger: NewLogger(node),
}
}
// 条件节点没有任何操作
func (c *conditionNode) Run(ctx context.Context) error {
c.AddOutput(ctx, xtime.BeijingTimeStr(),
c.node.Name,
"完成",
)
return nil
}

View File

@ -0,0 +1 @@
package nodeprocessor

View File

@ -0,0 +1 @@
package nodeprocessor

View File

@ -0,0 +1,69 @@
package nodeprocessor
import (
"context"
"errors"
"github.com/usual2970/certimate/internal/domain"
)
type RunLog struct {
NodeName string `json:"node_name"`
Err string `json:"err"`
Outputs []RunLogOutput `json:"outputs"`
}
type RunLogOutput struct {
Time string `json:"time"`
Title string `json:"title"`
Content string `json:"content"`
Error string `json:"error"`
}
type NodeProcessor interface {
Run(ctx context.Context) error
Log(ctx context.Context) *RunLog
AddOutput(ctx context.Context, time, title, content string, err ...string)
}
type Logger struct {
log *RunLog
}
func NewLogger(node *domain.WorkflowNode) *Logger {
return &Logger{
log: &RunLog{
NodeName: node.Name,
Outputs: make([]RunLogOutput, 0),
},
}
}
func (l *Logger) Log(ctx context.Context) *RunLog {
return l.log
}
func (l *Logger) AddOutput(ctx context.Context, time, title, content string, err ...string) {
output := RunLogOutput{
Time: time,
Title: title,
Content: content,
}
if len(err) > 0 {
output.Error = err[0]
l.log.Err = err[0]
}
l.log.Outputs = append(l.log.Outputs, output)
}
func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) {
switch node.Type {
case domain.WorkflowNodeTypeStart:
return NewStartNode(node), nil
case domain.WorkflowNodeTypeCondition:
return NewConditionNode(node), nil
case domain.WorkflowNodeTypeApply:
return NewApplyNode(node), nil
}
return nil, errors.New("not implemented")
}

View File

@ -0,0 +1,29 @@
package nodeprocessor
import (
"context"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/xtime"
)
type startNode struct {
node *domain.WorkflowNode
*Logger
}
func NewStartNode(node *domain.WorkflowNode) *startNode {
return &startNode{
node: node,
Logger: NewLogger(node),
}
}
// 开始节点没有任何操作
func (s *startNode) Run(ctx context.Context) error {
s.AddOutput(ctx, xtime.BeijingTimeStr(),
s.node.Name,
"完成",
)
return nil
}

View File

@ -0,0 +1,64 @@
package nodeprocessor
import (
"context"
"github.com/usual2970/certimate/internal/domain"
)
type workflowProcessor struct {
workflow *domain.Workflow
logs []RunLog
}
func NewWorkflowProcessor(workflow *domain.Workflow) *workflowProcessor {
return &workflowProcessor{
workflow: workflow,
}
}
func (w *workflowProcessor) Run(ctx context.Context) error {
ctx = WithWorkflowId(ctx, w.workflow.Id)
return w.runNode(ctx, w.workflow.Content)
}
func (w *workflowProcessor) runNode(ctx context.Context, node *domain.WorkflowNode) error {
current := node
for current != nil {
if current.Type == domain.WorkflowNodeTypeBranch {
for _, branch := range current.Branches {
if err := w.runNode(ctx, &branch); err != nil {
continue
}
}
}
if current.Type != domain.WorkflowNodeTypeBranch {
processor, err := GetProcessor(current)
if err != nil {
return err
}
err = processor.Run(ctx)
log := processor.Log(ctx)
if log != nil {
w.logs = append(w.logs, *log)
}
if err != nil {
return err
}
}
}
return nil
}
func WithWorkflowId(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, "workflow_id", id)
}
func GetWorkflowId(ctx context.Context) string {
return ctx.Value("workflow_id").(string)
}

View File

@ -0,0 +1,52 @@
package workflow
import (
"context"
"fmt"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/app"
nodeprocessor "github.com/usual2970/certimate/internal/workflow/node-processor"
)
type WorkflowRepository interface {
Get(ctx context.Context, id string) (*domain.Workflow, error)
}
type WorkflowService struct {
repo WorkflowRepository
}
func NewWorkflowService(repo WorkflowRepository) *WorkflowService {
return &WorkflowService{
repo: repo,
}
}
func (s *WorkflowService) Run(ctx context.Context, req *domain.WorkflowRunReq) error {
// 查询
if req.Id == "" {
return domain.ErrInvalidParams
}
workflow, err := s.repo.Get(ctx, req.Id)
if err != nil {
app.GetApp().Logger().Error("failed to get workflow", "id", req.Id, "err", err)
return err
}
// 执行
if !workflow.Enabled {
app.GetApp().Logger().Error("workflow is disabled", "id", req.Id)
return fmt.Errorf("workflow is disabled")
}
processor := nodeprocessor.NewWorkflowProcessor(workflow)
if err := processor.Run(ctx); err != nil {
return fmt.Errorf("failed to run workflow: %w", err)
}
// 保存执行日志
return nil
}

View File

@ -8,7 +8,8 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
import { Input } from "../ui/input"; import { Input } from "../ui/input";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { memo, useState } from "react"; import { memo, useEffect, useState } from "react";
import { Textarea } from "../ui/textarea";
type WorkflowNameEditDialogProps = { type WorkflowNameEditDialogProps = {
trigger: React.ReactNode; trigger: React.ReactNode;
@ -30,6 +31,10 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
}); });
useEffect(() => {
form.reset({ name: workflow.name, description: workflow.description });
}, [workflow]);
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -71,6 +76,7 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
<Input <Input
placeholder="请输入流程名称" placeholder="请输入流程名称"
{...field} {...field}
value={field.value}
defaultValue={workflow.name} defaultValue={workflow.name}
onChange={(e) => { onChange={(e) => {
form.setValue("name", e.target.value); form.setValue("name", e.target.value);
@ -90,9 +96,10 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel></FormLabel>
<FormControl> <FormControl>
<Input <Textarea
placeholder="请输入流程说明" placeholder="请输入流程说明"
{...field} {...field}
value={field.value}
defaultValue={workflow.description} defaultValue={workflow.description}
onChange={(e) => { onChange={(e) => {
form.setValue("description", e.target.value); form.setValue("description", e.target.value);

View File

@ -155,6 +155,10 @@ export const newWorkflowNode = (type: WorkflowNodeType, options: NewWorkflowNode
}; };
} }
if (type == WorkflowNodeType.Condition) {
rs.validated = true;
}
if (type === WorkflowNodeType.Branch) { if (type === WorkflowNodeType.Branch) {
rs = { rs = {
...rs, ...rs,
@ -350,6 +354,20 @@ export const allNodesValidated = (node: WorkflowNode | WorkflowBranchNode): bool
return true; return true;
}; };
export const getExecuteMethod = (node: WorkflowNode): { type: string; crontab: string } => {
if (node.type === WorkflowNodeType.Start) {
return {
type: (node.config?.executionMethod as string) ?? "",
crontab: (node.config?.crontab as string) ?? "",
};
} else {
return {
type: "",
crontab: "",
};
}
};
export type WorkflowBranchNode = { export type WorkflowBranchNode = {
id: string; id: string;
name: string; name: string;
@ -428,4 +446,3 @@ export const workflowNodeDropdownList: WorkflowwNodeDropdwonItem[] = [
}, },
}, },
]; ];

View File

@ -96,8 +96,8 @@ const WorkflowDetail = () => {
<WorkflowBaseInfoEditDialog <WorkflowBaseInfoEditDialog
trigger={ trigger={
<div className="flex flex-col space-y-1 cursor-pointer items-start"> <div className="flex flex-col space-y-1 cursor-pointer items-start">
<div className="">{workflow.name ? workflow.name : "未命名工作流"}</div> <div className="truncate max-w-[200px]">{workflow.name ? workflow.name : "未命名工作流"}</div>
<div className="text-sm text-muted-foreground">{workflow.description ? workflow.description : "添加流程说明"}</div> <div className="text-sm text-muted-foreground truncate max-w-[200px]">{workflow.description ? workflow.description : "添加流程说明"}</div>
</div> </div>
} }
/> />

View File

@ -43,7 +43,7 @@ const Workflow = () => {
if (!name) { if (!name) {
name = "未命名工作流"; name = "未命名工作流";
} }
return <div className="flex items-center">{name}</div>; return <div className="max-w-[150px] truncate">{name}</div>;
}, },
}, },
{ {
@ -54,7 +54,7 @@ const Workflow = () => {
if (!description) { if (!description) {
description = "-"; description = "-";
} }
return description; return <div className="max-w-[200px] truncate">{description}</div>;
}, },
}, },
{ {
@ -211,4 +211,3 @@ const Workflow = () => {
}; };
export default Workflow; export default Workflow;

View File

@ -1,6 +1,7 @@
import { import {
addBranch, addBranch,
addNode, addNode,
getExecuteMethod,
getWorkflowOutputBeforeId, getWorkflowOutputBeforeId,
initWorkflow, initWorkflow,
removeBranch, removeBranch,
@ -76,11 +77,15 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}); });
}, },
switchEnable: async () => { switchEnable: async () => {
const root = get().workflow.draft as WorkflowNode;
const executeMethod = getExecuteMethod(root);
const resp = await save({ const resp = await save({
id: (get().workflow.id as string) ?? "", id: (get().workflow.id as string) ?? "",
content: get().workflow.draft as WorkflowNode, content: root,
enabled: !get().workflow.enabled, enabled: !get().workflow.enabled,
hasDraft: false, hasDraft: false,
type: executeMethod.type,
crontab: executeMethod.crontab,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
@ -90,15 +95,21 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
content: resp.content, content: resp.content,
enabled: resp.enabled, enabled: resp.enabled,
hasDraft: false, hasDraft: false,
type: resp.type,
crontab: resp.crontab,
}, },
}; };
}); });
}, },
save: async () => { save: async () => {
const root = get().workflow.draft as WorkflowNode;
const executeMethod = getExecuteMethod(root);
const resp = await save({ const resp = await save({
id: (get().workflow.id as string) ?? "", id: (get().workflow.id as string) ?? "",
content: get().workflow.draft as WorkflowNode, content: root,
hasDraft: false, hasDraft: false,
type: executeMethod.type,
crontab: executeMethod.crontab,
}); });
set((state: WorkflowState) => { set((state: WorkflowState) => {
return { return {
@ -107,6 +118,8 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
id: resp.id, id: resp.id,
content: resp.content, content: resp.content,
hasDraft: false, hasDraft: false,
type: resp.type,
crontab: resp.crontab,
}, },
}; };
}); });
@ -205,4 +218,3 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type); return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
}, },
})); }));