diff --git a/sqle/api/controller/v2/dashboard/workflow_manage.go b/sqle/api/controller/v2/dashboard/workflow_manage.go index 67eeb9fed..edc2aa542 100644 --- a/sqle/api/controller/v2/dashboard/workflow_manage.go +++ b/sqle/api/controller/v2/dashboard/workflow_manage.go @@ -120,6 +120,8 @@ type ExportGlobalWorkflowReqV2 struct { FilterCreateTimeTo string `json:"filter_create_time_to" query:"filter_create_time_to"` FilterUpdateTimeFrom string `json:"filter_update_time_from" query:"filter_update_time_from"` FilterUpdateTimeTo string `json:"filter_update_time_to" query:"filter_update_time_to"` + // FilterByOpsTypeUID 按运维类型单选筛选(空则不筛;不提供「未设置」;对 SQL 上线与数据导出均生效;语义与 GetGlobalWorkflowListReqV2 对齐) + FilterByOpsTypeUID string `json:"filter_by_ops_type_uid" query:"filter_by_ops_type_uid"` FilterCard dashboard_svc.GlobalWorkflowFilterCard `json:"filter_card" query:"filter_card" valid:"omitempty,oneof=archived pending_for_me initiated_by_me view_all" enums:"archived,pending_for_me,initiated_by_me,view_all"` @@ -148,6 +150,7 @@ type ExportGlobalWorkflowReqV2 struct { // @Param filter_create_user_id query string false "filter by create user id" // @Param filter_create_time_from query string false "filter create time from" // @Param filter_create_time_to query string false "filter create time to" +// @Param filter_by_ops_type_uid query string false "filter by ops type dictionary item uid; empty means no filter; applies to sql_release and data_export; same semantics as GET /v2/dashboard/workflows" // @Param export_format query string false "export format: csv or excel, default csv" Enums(csv,excel) // @Success 200 {file} file "export workflow" // @Router /v2/dashboard/workflows/exports [get] diff --git a/sqle/docs/docs.go b/sqle/docs/docs.go index 28a9eb8eb..5a5fa145a 100644 --- a/sqle/docs/docs.go +++ b/sqle/docs/docs.go @@ -11677,6 +11677,12 @@ var doc = `{ "name": "filter_create_time_to", "in": "query" }, + { + "type": "string", + "description": "filter by ops type dictionary item uid; empty means no filter; applies to sql_release and data_export; same semantics as GET /v2/dashboard/workflows", + "name": "filter_by_ops_type_uid", + "in": "query" + }, { "enum": [ "csv", @@ -14326,10 +14332,6 @@ var doc = `{ "dashboard.GlobalWorkflowListItem": { "type": "object", "properties": { - "assignee": { - "description": "当前处理人姓名", - "type": "string" - }, "create_user_name": { "type": "string" }, @@ -16762,9 +16764,6 @@ var doc = `{ "db_type": { "type": "string" }, - "default_port": { - "type": "integer" - }, "params": { "type": "array", "items": { diff --git a/sqle/docs/swagger.json b/sqle/docs/swagger.json index 3e3b735fa..9a2e8781a 100644 --- a/sqle/docs/swagger.json +++ b/sqle/docs/swagger.json @@ -11661,6 +11661,12 @@ "name": "filter_create_time_to", "in": "query" }, + { + "type": "string", + "description": "filter by ops type dictionary item uid; empty means no filter; applies to sql_release and data_export; same semantics as GET /v2/dashboard/workflows", + "name": "filter_by_ops_type_uid", + "in": "query" + }, { "enum": [ "csv", @@ -14310,10 +14316,6 @@ "dashboard.GlobalWorkflowListItem": { "type": "object", "properties": { - "assignee": { - "description": "当前处理人姓名", - "type": "string" - }, "create_user_name": { "type": "string" }, @@ -16746,9 +16748,6 @@ "db_type": { "type": "string" }, - "default_port": { - "type": "integer" - }, "params": { "type": "array", "items": { diff --git a/sqle/docs/swagger.yaml b/sqle/docs/swagger.yaml index 4419f8709..25e891842 100644 --- a/sqle/docs/swagger.yaml +++ b/sqle/docs/swagger.yaml @@ -184,9 +184,6 @@ definitions: type: object dashboard.GlobalWorkflowListItem: properties: - assignee: - description: 当前处理人姓名 - type: string create_user_name: type: string created_at: @@ -1860,8 +1857,6 @@ definitions: properties: db_type: type: string - default_port: - type: integer params: items: $ref: '#/definitions/v1.InstanceAdditionalParamResV1' @@ -15779,6 +15774,11 @@ paths: in: query name: filter_create_time_to type: string + - description: filter by ops type dictionary item uid; empty means no filter; + applies to sql_release and data_export; same semantics as GET /v2/dashboard/workflows + in: query + name: filter_by_ops_type_uid + type: string - description: 'export format: csv or excel, default csv' enum: - csv diff --git a/sqle/server/workflowexport/export.go b/sqle/server/workflowexport/export.go index 69b7ba506..21632415f 100644 --- a/sqle/server/workflowexport/export.go +++ b/sqle/server/workflowexport/export.go @@ -110,11 +110,22 @@ func BuildGlobalSQLReleaseExport(ctx context.Context, workflowIDs []string, proj return nil, nil, err } header := buildGlobalSQLReleaseHeader(ctx) + opsTypeNameByProject := map[string]map[string]string{} rows := make([][]string, 0) for _, workflow := range workflows { + projectUID := string(workflow.ProjectId) projectName := "" if projectNameByUID != nil { - projectName = projectNameByUID[string(workflow.ProjectId)] + projectName = projectNameByUID[projectUID] + } + opsTypeNameByUID, ok := opsTypeNameByProject[projectUID] + if !ok { + opsTypeNameByUID = dms.BuildOpsTypeNameMap(ctx, projectUID) + opsTypeNameByProject[projectUID] = opsTypeNameByUID + } + opsTypeName := "" + if ot := dms.ResolveOpsTypeFromMap(workflow.OpsTypeUID, opsTypeNameByUID); ot != nil { + opsTypeName = ot.Name } auditRecord := formatAuditRecordFromSQLSteps(ctx, workflow.AuditStepList()) for _, instanceRecord := range workflow.Record.InstanceRecords { @@ -124,6 +135,7 @@ func BuildGlobalSQLReleaseExport(ctx context.Context, workflowIDs []string, proj workflow.WorkflowId, workflow.Subject, workflow.Desc, + opsTypeName, instanceName, workflow.Model.CreatedAt.Format(timeLayout), dms.GetUserNameWithDelTag(workflow.CreateUserId), @@ -153,6 +165,7 @@ type DataExportExportRecord struct { CreatedAt string CreatorName string UnifiedStatus string + OpsTypeName string AuditRecord string SQLContent string ExportExecTime string @@ -168,6 +181,7 @@ func FromListDataExportWorkflow(r *dmsV1.ListDataExportWorkflow) DataExportExpor CreatedAt: r.CreatedAt.Format(timeLayout), CreatorName: r.Creater.Name, //nolint:misspell // DMS API field is Creater UnifiedStatus: mapDataExportStatus(r.Status), + OpsTypeName: opsTypeNameFromListDataExport(r), } if r.ProjectInfo != nil { out.ProjectName = r.ProjectInfo.ProjectName @@ -235,6 +249,7 @@ func BuildGlobalDataExportExport(ctx context.Context, records []DataExportExport r.WorkflowID, r.WorkflowName, r.Description, + r.OpsTypeName, strings.Join(r.DBServiceNames, ","), r.CreatedAt, r.CreatorName, @@ -258,6 +273,7 @@ type CommonExportRow struct { CreatorName string CreatedAt string Status string + OpsTypeName string DataSource string SQLContent string SortKey string @@ -277,6 +293,7 @@ func BuildGlobalCommonExport(ctx context.Context, rowsIn []CommonExportRow) ([]s r.CreatorName, r.CreatedAt, r.Status, + r.OpsTypeName, r.DataSource, r.SQLContent, }) @@ -363,6 +380,7 @@ func buildGlobalSQLReleaseHeader(ctx context.Context) []string { locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowNumber), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowName), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowDescription), + locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportOpsType), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportDataSource), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportCreateTime), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportCreator), @@ -382,6 +400,7 @@ func buildGlobalDataExportHeader(ctx context.Context) []string { locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowNumber), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowName), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowDescription), + locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportOpsType), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportDataSource), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportCreateTime), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportCreator), @@ -403,6 +422,7 @@ func buildGlobalCommonHeader(ctx context.Context) []string { locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportCreator), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportCreateTime), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportTaskOrderStatus), + locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportOpsType), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportDataSource), locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportSQLContentPlain), } @@ -608,6 +628,7 @@ func BuildCommonRowsFromSQLRelease(ctx context.Context, workflowIDs []string, pr return nil, err } typeLabel := LocalizeWorkflowTypeSQLRelease(ctx) + opsTypeNameByProject := map[string]map[string]string{} out := make([]CommonExportRow, 0, len(workflows)) for _, workflow := range workflows { names := make([]string, 0, len(workflow.Record.InstanceRecords)) @@ -620,9 +641,19 @@ func BuildCommonRowsFromSQLRelease(ctx context.Context, workflowIDs []string, pr sqlBuilder.WriteString(getExecuteSqlList(ir.Task.ExecuteSQLs)) } } + projectUID := string(workflow.ProjectId) projectName := "" if projectNameByUID != nil { - projectName = projectNameByUID[string(workflow.ProjectId)] + projectName = projectNameByUID[projectUID] + } + opsTypeNameByUID, ok := opsTypeNameByProject[projectUID] + if !ok { + opsTypeNameByUID = dms.BuildOpsTypeNameMap(ctx, projectUID) + opsTypeNameByProject[projectUID] = opsTypeNameByUID + } + opsTypeName := "" + if ot := dms.ResolveOpsTypeFromMap(workflow.OpsTypeUID, opsTypeNameByUID); ot != nil { + opsTypeName = ot.Name } created := workflow.Model.CreatedAt.Format(timeLayout) out = append(out, CommonExportRow{ @@ -634,6 +665,7 @@ func BuildCommonRowsFromSQLRelease(ctx context.Context, workflowIDs []string, pr CreatorName: dms.GetUserNameWithDelTag(workflow.CreateUserId), CreatedAt: created, Status: localizeUnifiedStatus(ctx, mapSQLWorkflowStatus(workflow.Record.Status)), + OpsTypeName: opsTypeName, DataSource: strings.Join(names, ","), SQLContent: sqlBuilder.String(), SortKey: created, @@ -656,6 +688,7 @@ func BuildCommonRowsFromDataExport(ctx context.Context, records []DataExportExpo CreatorName: r.CreatorName, CreatedAt: r.CreatedAt, Status: localizeUnifiedStatus(ctx, r.UnifiedStatus), + OpsTypeName: r.OpsTypeName, DataSource: strings.Join(r.DBServiceNames, ","), SQLContent: r.SQLContent, SortKey: r.CreatedAt, diff --git a/sqle/server/workflowexport/export_list_ops_type_ce.go b/sqle/server/workflowexport/export_list_ops_type_ce.go new file mode 100644 index 000000000..1be86620a --- /dev/null +++ b/sqle/server/workflowexport/export_list_ops_type_ce.go @@ -0,0 +1,11 @@ +//go:build !enterprise +// +build !enterprise + +package workflowexport + +import dmsV1 "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" + +// opsTypeNameFromListDataExport is a CE stub: CE vendor ListDataExportWorkflow has no OpsType field. +func opsTypeNameFromListDataExport(_ *dmsV1.ListDataExportWorkflow) string { + return "" +} diff --git a/sqle/server/workflowexport/export_test.go b/sqle/server/workflowexport/export_test.go index 38910504f..2adb73218 100644 --- a/sqle/server/workflowexport/export_test.go +++ b/sqle/server/workflowexport/export_test.go @@ -47,6 +47,15 @@ func TestBuildHeaderForLayoutGlobalSQLRelease(t *testing.T) { if header[0] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportProjectName) { t.Fatalf("first col want project name, got %s", header[0]) } + if header[3] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowDescription) { + t.Fatalf("col4 want description, got %s", header[3]) + } + if header[4] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportOpsType) { + t.Fatalf("col5 want ops type after description, got %s", header[4]) + } + if header[5] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportDataSource) { + t.Fatalf("col6 want data source after ops type, got %s", header[5]) + } if header[len(header)-1] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportSQLContent) { t.Fatalf("last col want SQL content, got %s", header[len(header)-1]) } @@ -87,8 +96,8 @@ func TestBuildHeaderForLayoutProjectFrozen(t *testing.T) { func TestBuildGlobalCommonHeader(t *testing.T) { ctx := context.Background() header := BuildHeaderForLayout(ctx, LayoutGlobalCommon, 0) - if len(header) != 10 { - t.Fatalf("common header len=%d want 10: %v", len(header), header) + if len(header) != 11 { + t.Fatalf("common header len=%d want 11: %v", len(header), header) } if header[1] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowType) { t.Fatalf("col2 want workflow type, got %s", header[1]) @@ -96,8 +105,17 @@ func TestBuildGlobalCommonHeader(t *testing.T) { if header[4] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowDescription) { t.Fatalf("col5 want description, got %s", header[4]) } - if header[9] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportSQLContentPlain) { - t.Fatalf("last col want SQL content plain, got %s", header[9]) + if header[7] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportTaskOrderStatus) { + t.Fatalf("col8 want status, got %s", header[7]) + } + if header[8] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportOpsType) { + t.Fatalf("col9 want ops type after status, got %s", header[8]) + } + if header[9] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportDataSource) { + t.Fatalf("col10 want data source after ops type, got %s", header[9]) + } + if header[10] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportSQLContentPlain) { + t.Fatalf("last col want SQL content plain, got %s", header[10]) } } @@ -105,6 +123,15 @@ func TestBuildGlobalDataExportHeader(t *testing.T) { ctx := context.Background() header := BuildHeaderForLayout(ctx, LayoutGlobalDataExport, 1) joined := strings.Join(header, "|") + if header[3] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowDescription) { + t.Fatalf("col4 want description, got %s", header[3]) + } + if header[4] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportOpsType) { + t.Fatalf("col5 want ops type after description, got %s", header[4]) + } + if header[5] != locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportDataSource) { + t.Fatalf("col6 want data source after ops type, got %s", header[5]) + } if !strings.Contains(joined, locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportDataExportExecTime)) { t.Fatalf("want export exec time col") } @@ -163,10 +190,13 @@ func TestBuildGlobalDataExportPrunesEmptyOptionalCols(t *testing.T) { CreatorName: "u", UnifiedStatus: "completed", SQLContent: "SELECT 1", - // AuditRecord / ExportExecTime / ExportResult empty → pruned + // OpsTypeName / AuditRecord / ExportExecTime / ExportResult empty → pruned }, }) joined := strings.Join(header, "|") + if strings.Contains(joined, locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportOpsType)) { + t.Fatalf("empty ops type col should be pruned: %v", header) + } if strings.Contains(joined, locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportAuditRecord)) { t.Fatalf("empty audit col should be pruned: %v", header) } @@ -181,6 +211,64 @@ func TestBuildGlobalDataExportPrunesEmptyOptionalCols(t *testing.T) { } } +func TestBuildGlobalDataExportKeepsOpsTypeWhenPresent(t *testing.T) { + ctx := context.Background() + opsType := "共享运维类型-导出验收" + header, rows := BuildGlobalDataExportExport(ctx, []DataExportExportRecord{ + { + ProjectName: "p", + WorkflowID: "1", + WorkflowName: "n", + Description: "d", + OpsTypeName: opsType, + DBServiceNames: []string{"db"}, + CreatedAt: "2026-01-01 00:00:00", + CreatorName: "u", + UnifiedStatus: "completed", + SQLContent: "SELECT 1", + }, + { + ProjectName: "p2", + WorkflowID: "2", + WorkflowName: "n2", + Description: "d2", + OpsTypeName: "", + DBServiceNames: []string{"db2"}, + CreatedAt: "2026-01-02 00:00:00", + CreatorName: "u2", + UnifiedStatus: "completed", + SQLContent: "SELECT 2", + }, + }) + opsCol := locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportOpsType) + descCol := locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportWorkflowDescription) + dsCol := locale.Bundle.LocalizeMsgByCtx(ctx, locale.WFExportDataSource) + opsIdx, descIdx, dsIdx := -1, -1, -1 + for i, h := range header { + switch h { + case opsCol: + opsIdx = i + case descCol: + descIdx = i + case dsCol: + dsIdx = i + } + } + if opsIdx < 0 { + t.Fatalf("ops type col must remain when any row has value: %v", header) + } + if !(descIdx >= 0 && opsIdx == descIdx+1 && dsIdx == opsIdx+1) { + t.Fatalf("want description, ops type, data source consecutive, got desc=%d ops=%d ds=%d header=%v", + descIdx, opsIdx, dsIdx, header) + } + if rows[0][opsIdx] != opsType { + t.Fatalf("row0 ops=%q want %q", rows[0][opsIdx], opsType) + } + if rows[1][opsIdx] != "" { + t.Fatalf("row1 empty ops must be empty cell, got %q", rows[1][opsIdx]) + } +} + func TestMapSQLWorkflowStatusToUnified(t *testing.T) { cases := []struct { native string