feat: simplify param override audit UI and operation labels
This commit is contained in:
+136
-103
@@ -230,28 +230,14 @@ func getParamOverrideAuditRecorder(context map[string]interface{}) *paramOverrid
|
|||||||
return recorder
|
return recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paramOverrideAuditRecorder) record(path string, beforeExists bool, beforeValue interface{}, afterExists bool, afterValue interface{}) {
|
func (r *paramOverrideAuditRecorder) recordOperation(mode, path, from, to string, value interface{}) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
path = strings.TrimSpace(path)
|
line := buildParamOverrideAuditLine(mode, path, from, to, value)
|
||||||
if path == "" {
|
if line == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !shouldAuditParamPath(path) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeText := "<empty>"
|
|
||||||
if beforeExists {
|
|
||||||
beforeText = formatParamOverrideAuditValue(beforeValue)
|
|
||||||
}
|
|
||||||
afterText := "<deleted>"
|
|
||||||
if afterExists {
|
|
||||||
afterText = formatParamOverrideAuditValue(afterValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
line := fmt.Sprintf("%s: %s -> %s", path, beforeText, afterText)
|
|
||||||
if lo.Contains(r.lines, line) {
|
if lo.Contains(r.lines, line) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -270,23 +256,16 @@ func shouldAuditParamPath(path string) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyAuditedPathMutation(result, path string, auditRecorder *paramOverrideAuditRecorder, mutate func(string) (string, error)) (string, error) {
|
func shouldAuditOperation(mode, path, from, to string) bool {
|
||||||
needAudit := auditRecorder != nil && shouldAuditParamPath(path)
|
if common.DebugEnabled {
|
||||||
var beforeResult gjson.Result
|
return true
|
||||||
if needAudit {
|
|
||||||
beforeResult = gjson.Get(result, path)
|
|
||||||
}
|
}
|
||||||
|
for _, candidate := range []string{path, to} {
|
||||||
next, err := mutate(result)
|
if shouldAuditParamPath(candidate) {
|
||||||
if err != nil {
|
return true
|
||||||
return next, err
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
if needAudit {
|
|
||||||
afterResult := gjson.Get(next, path)
|
|
||||||
auditRecorder.record(path, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value())
|
|
||||||
}
|
|
||||||
return next, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatParamOverrideAuditValue(value interface{}) string {
|
func formatParamOverrideAuditValue(value interface{}) string {
|
||||||
@@ -300,6 +279,94 @@ func formatParamOverrideAuditValue(value interface{}) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildParamOverrideAuditLine(mode, path, from, to string, value interface{}) string {
|
||||||
|
mode = strings.TrimSpace(mode)
|
||||||
|
path = strings.TrimSpace(path)
|
||||||
|
from = strings.TrimSpace(from)
|
||||||
|
to = strings.TrimSpace(to)
|
||||||
|
|
||||||
|
if !shouldAuditOperation(mode, path, from, to) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case "set":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("set %s = %s", path, formatParamOverrideAuditValue(value))
|
||||||
|
case "delete":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("delete %s", path)
|
||||||
|
case "copy":
|
||||||
|
if from == "" || to == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("copy %s -> %s", from, to)
|
||||||
|
case "move":
|
||||||
|
if from == "" || to == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("move %s -> %s", from, to)
|
||||||
|
case "prepend":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("prepend %s with %s", path, formatParamOverrideAuditValue(value))
|
||||||
|
case "append":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("append %s with %s", path, formatParamOverrideAuditValue(value))
|
||||||
|
case "trim_prefix", "trim_suffix", "ensure_prefix", "ensure_suffix":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s with %s", mode, path, formatParamOverrideAuditValue(value))
|
||||||
|
case "trim_space", "to_lower", "to_upper":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", mode, path)
|
||||||
|
case "replace", "regex_replace":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s from %s to %s", mode, path, from, to)
|
||||||
|
case "set_header":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("set_header %s = %s", path, formatParamOverrideAuditValue(value))
|
||||||
|
case "delete_header":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("delete_header %s", path)
|
||||||
|
case "copy_header", "move_header":
|
||||||
|
if from == "" || to == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s -> %s", mode, from, to)
|
||||||
|
case "pass_headers":
|
||||||
|
return fmt.Sprintf("pass_headers %s", formatParamOverrideAuditValue(value))
|
||||||
|
case "sync_fields":
|
||||||
|
if from == "" || to == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("sync_fields %s -> %s", from, to)
|
||||||
|
case "return_error":
|
||||||
|
return fmt.Sprintf("return_error %s", formatParamOverrideAuditValue(value))
|
||||||
|
default:
|
||||||
|
if path == "" {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", mode, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getParamOverrideMap(info *RelayInfo) map[string]interface{} {
|
func getParamOverrideMap(info *RelayInfo) map[string]interface{} {
|
||||||
if info == nil || info.ChannelMeta == nil {
|
if info == nil || info.ChannelMeta == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -594,9 +661,8 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range paramOverride {
|
for key, value := range paramOverride {
|
||||||
beforeValue, beforeExists := reqMap[key]
|
|
||||||
reqMap[key] = value
|
reqMap[key] = value
|
||||||
auditRecorder.record(key, beforeExists, beforeValue, true, value)
|
auditRecorder.recordOperation("set", key, "", "", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return common.Marshal(reqMap)
|
return common.Marshal(reqMap)
|
||||||
@@ -636,47 +702,29 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
switch op.Mode {
|
switch op.Mode {
|
||||||
case "delete":
|
case "delete":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = deleteValue(result, path)
|
||||||
return deleteValue(current, path)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("delete", path, "", "", nil)
|
||||||
}
|
}
|
||||||
case "set":
|
case "set":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
if op.KeepOrigin && gjson.Get(result, path).Exists() {
|
if op.KeepOrigin && gjson.Get(result, path).Exists() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = sjson.Set(result, path, op.Value)
|
||||||
return sjson.Set(current, path, op.Value)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("set", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "move":
|
case "move":
|
||||||
opFrom := processNegativeIndex(result, op.From)
|
opFrom := processNegativeIndex(result, op.From)
|
||||||
opTo := processNegativeIndex(result, op.To)
|
opTo := processNegativeIndex(result, op.To)
|
||||||
needAuditTo := auditRecorder != nil && shouldAuditParamPath(opTo)
|
|
||||||
needAuditFrom := auditRecorder != nil && shouldAuditParamPath(opFrom)
|
|
||||||
var beforeResult gjson.Result
|
|
||||||
var fromResult gjson.Result
|
|
||||||
if needAuditTo {
|
|
||||||
beforeResult = gjson.Get(result, opTo)
|
|
||||||
}
|
|
||||||
if needAuditFrom {
|
|
||||||
fromResult = gjson.Get(result, opFrom)
|
|
||||||
}
|
|
||||||
result, err = moveValue(result, opFrom, opTo)
|
result, err = moveValue(result, opFrom, opTo)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if needAuditTo {
|
auditRecorder.recordOperation("move", "", opFrom, opTo, nil)
|
||||||
afterResult := gjson.Get(result, opTo)
|
|
||||||
auditRecorder.record(opTo, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value())
|
|
||||||
}
|
|
||||||
if needAuditFrom && common.DebugEnabled {
|
|
||||||
auditRecorder.record(opFrom, fromResult.Exists(), fromResult.Value(), false, nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case "copy":
|
case "copy":
|
||||||
if op.From == "" || op.To == "" {
|
if op.From == "" || op.To == "" {
|
||||||
@@ -684,116 +732,100 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
}
|
}
|
||||||
opFrom := processNegativeIndex(result, op.From)
|
opFrom := processNegativeIndex(result, op.From)
|
||||||
opTo := processNegativeIndex(result, op.To)
|
opTo := processNegativeIndex(result, op.To)
|
||||||
needAudit := auditRecorder != nil && shouldAuditParamPath(opTo)
|
|
||||||
var beforeResult gjson.Result
|
|
||||||
if needAudit {
|
|
||||||
beforeResult = gjson.Get(result, opTo)
|
|
||||||
}
|
|
||||||
result, err = copyValue(result, opFrom, opTo)
|
result, err = copyValue(result, opFrom, opTo)
|
||||||
if err == nil && needAudit {
|
if err == nil {
|
||||||
afterResult := gjson.Get(result, opTo)
|
auditRecorder.recordOperation("copy", "", opFrom, opTo, nil)
|
||||||
auditRecorder.record(opTo, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value())
|
|
||||||
}
|
}
|
||||||
case "prepend":
|
case "prepend":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = modifyValue(result, path, op.Value, op.KeepOrigin, true)
|
||||||
return modifyValue(current, path, op.Value, op.KeepOrigin, true)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("prepend", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "append":
|
case "append":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = modifyValue(result, path, op.Value, op.KeepOrigin, false)
|
||||||
return modifyValue(current, path, op.Value, op.KeepOrigin, false)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("append", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "trim_prefix":
|
case "trim_prefix":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = trimStringValue(result, path, op.Value, true)
|
||||||
return trimStringValue(current, path, op.Value, true)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("trim_prefix", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "trim_suffix":
|
case "trim_suffix":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = trimStringValue(result, path, op.Value, false)
|
||||||
return trimStringValue(current, path, op.Value, false)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("trim_suffix", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "ensure_prefix":
|
case "ensure_prefix":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = ensureStringAffix(result, path, op.Value, true)
|
||||||
return ensureStringAffix(current, path, op.Value, true)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("ensure_prefix", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "ensure_suffix":
|
case "ensure_suffix":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = ensureStringAffix(result, path, op.Value, false)
|
||||||
return ensureStringAffix(current, path, op.Value, false)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("ensure_suffix", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "trim_space":
|
case "trim_space":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = transformStringValue(result, path, strings.TrimSpace)
|
||||||
return transformStringValue(current, path, strings.TrimSpace)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("trim_space", path, "", "", nil)
|
||||||
}
|
}
|
||||||
case "to_lower":
|
case "to_lower":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = transformStringValue(result, path, strings.ToLower)
|
||||||
return transformStringValue(current, path, strings.ToLower)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("to_lower", path, "", "", nil)
|
||||||
}
|
}
|
||||||
case "to_upper":
|
case "to_upper":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = transformStringValue(result, path, strings.ToUpper)
|
||||||
return transformStringValue(current, path, strings.ToUpper)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("to_upper", path, "", "", nil)
|
||||||
}
|
}
|
||||||
case "replace":
|
case "replace":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = replaceStringValue(result, path, op.From, op.To)
|
||||||
return replaceStringValue(current, path, op.From, op.To)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("replace", path, op.From, op.To, nil)
|
||||||
}
|
}
|
||||||
case "regex_replace":
|
case "regex_replace":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
result, err = regexReplaceStringValue(result, path, op.From, op.To)
|
||||||
return regexReplaceStringValue(current, path, op.From, op.To)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("regex_replace", path, op.From, op.To, nil)
|
||||||
}
|
}
|
||||||
case "return_error":
|
case "return_error":
|
||||||
|
auditRecorder.recordOperation("return_error", op.Path, "", "", op.Value)
|
||||||
returnErr, parseErr := parseParamOverrideReturnError(op.Value)
|
returnErr, parseErr := parseParamOverrideReturnError(op.Value)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
return "", parseErr
|
return "", parseErr
|
||||||
@@ -809,11 +841,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
case "set_header":
|
case "set_header":
|
||||||
err = setHeaderOverrideInContext(context, op.Path, op.Value, op.KeepOrigin)
|
err = setHeaderOverrideInContext(context, op.Path, op.Value, op.KeepOrigin)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("set_header", op.Path, "", "", op.Value)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
case "delete_header":
|
case "delete_header":
|
||||||
err = deleteHeaderOverrideInContext(context, op.Path)
|
err = deleteHeaderOverrideInContext(context, op.Path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("delete_header", op.Path, "", "", nil)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
case "copy_header":
|
case "copy_header":
|
||||||
@@ -830,6 +864,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("copy_header", "", sourceHeader, targetHeader, nil)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
case "move_header":
|
case "move_header":
|
||||||
@@ -846,6 +881,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("move_header", "", sourceHeader, targetHeader, nil)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
case "pass_headers":
|
case "pass_headers":
|
||||||
@@ -863,11 +899,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("pass_headers", "", "", "", headerNames)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
case "sync_fields":
|
case "sync_fields":
|
||||||
result, err = syncFieldsBetweenTargets(result, context, op.From, op.To)
|
result, err = syncFieldsBetweenTargets(result, context, op.From, op.To)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("sync_fields", "", op.From, op.To, nil)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -985,7 +1023,6 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
|
rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
|
||||||
beforeRaw, beforeExists := rawHeaders[headerName]
|
|
||||||
if keepOrigin {
|
if keepOrigin {
|
||||||
if existing, ok := rawHeaders[headerName]; ok {
|
if existing, ok := rawHeaders[headerName]; ok {
|
||||||
existingValue := strings.TrimSpace(fmt.Sprintf("%v", existing))
|
existingValue := strings.TrimSpace(fmt.Sprintf("%v", existing))
|
||||||
@@ -1001,12 +1038,10 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
|
|||||||
}
|
}
|
||||||
if !hasValue {
|
if !hasValue {
|
||||||
delete(rawHeaders, headerName)
|
delete(rawHeaders, headerName)
|
||||||
getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, false, nil)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rawHeaders[headerName] = headerValue
|
rawHeaders[headerName] = headerValue
|
||||||
getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, true, headerValue)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,9 +1213,7 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st
|
|||||||
return fmt.Errorf("header name is required")
|
return fmt.Errorf("header name is required")
|
||||||
}
|
}
|
||||||
rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
|
rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
|
||||||
beforeRaw, beforeExists := rawHeaders[headerName]
|
|
||||||
delete(rawHeaders, headerName)
|
delete(rawHeaders, headerName)
|
||||||
getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, false, nil)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
common2 "github.com/QuantumNous/new-api/common"
|
||||||
"github.com/QuantumNous/new-api/types"
|
"github.com/QuantumNous/new-api/types"
|
||||||
|
|
||||||
"github.com/QuantumNous/new-api/dto"
|
"github.com/QuantumNous/new-api/dto"
|
||||||
@@ -2066,6 +2067,105 @@ func TestRemoveDisabledFieldsAllowInferenceGeo(t *testing.T) {
|
|||||||
assertJSONEqual(t, `{"inference_geo":"eu","store":true}`, string(out))
|
assertJSONEqual(t, `{"inference_geo":"eu","store":true}`, string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyParamOverrideWithRelayInfoRecordsOperationAuditInDebugMode(t *testing.T) {
|
||||||
|
originalDebugEnabled := common2.DebugEnabled
|
||||||
|
common2.DebugEnabled = true
|
||||||
|
t.Cleanup(func() {
|
||||||
|
common2.DebugEnabled = originalDebugEnabled
|
||||||
|
})
|
||||||
|
|
||||||
|
info := &RelayInfo{
|
||||||
|
ChannelMeta: &ChannelMeta{
|
||||||
|
ParamOverride: map[string]interface{}{
|
||||||
|
"operations": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"mode": "copy",
|
||||||
|
"from": "metadata.target_model",
|
||||||
|
"to": "model",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"mode": "set",
|
||||||
|
"path": "service_tier",
|
||||||
|
"value": "flex",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"mode": "set",
|
||||||
|
"path": "temperature",
|
||||||
|
"value": 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := ApplyParamOverrideWithRelayInfo([]byte(`{
|
||||||
|
"model":"gpt-4.1",
|
||||||
|
"temperature":0.7,
|
||||||
|
"metadata":{"target_model":"gpt-4.1-mini"}
|
||||||
|
}`), info)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyParamOverrideWithRelayInfo returned error: %v", err)
|
||||||
|
}
|
||||||
|
assertJSONEqual(t, `{
|
||||||
|
"model":"gpt-4.1-mini",
|
||||||
|
"temperature":0.1,
|
||||||
|
"service_tier":"flex",
|
||||||
|
"metadata":{"target_model":"gpt-4.1-mini"}
|
||||||
|
}`, string(out))
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"copy metadata.target_model -> model",
|
||||||
|
"set service_tier = flex",
|
||||||
|
"set temperature = 0.1",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(info.ParamOverrideAudit, expected) {
|
||||||
|
t.Fatalf("unexpected param override audit, got %#v", info.ParamOverrideAudit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyParamOverrideWithRelayInfoRecordsOnlyKeyOperationsWhenDebugDisabled(t *testing.T) {
|
||||||
|
originalDebugEnabled := common2.DebugEnabled
|
||||||
|
common2.DebugEnabled = false
|
||||||
|
t.Cleanup(func() {
|
||||||
|
common2.DebugEnabled = originalDebugEnabled
|
||||||
|
})
|
||||||
|
|
||||||
|
info := &RelayInfo{
|
||||||
|
ChannelMeta: &ChannelMeta{
|
||||||
|
ParamOverride: map[string]interface{}{
|
||||||
|
"operations": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"mode": "copy",
|
||||||
|
"from": "metadata.target_model",
|
||||||
|
"to": "model",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"mode": "set",
|
||||||
|
"path": "temperature",
|
||||||
|
"value": 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ApplyParamOverrideWithRelayInfo([]byte(`{
|
||||||
|
"model":"gpt-4.1",
|
||||||
|
"temperature":0.7,
|
||||||
|
"metadata":{"target_model":"gpt-4.1-mini"}
|
||||||
|
}`), info)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyParamOverrideWithRelayInfo returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"copy metadata.target_model -> model",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(info.ParamOverrideAudit, expected) {
|
||||||
|
t.Fatalf("unexpected param override audit, got %#v", info.ParamOverrideAudit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func assertJSONEqual(t *testing.T, want, got string) {
|
func assertJSONEqual(t *testing.T, want, got string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Typography } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const ParamOverrideEntry = ({ count, onOpen, t }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
type='tertiary'
|
||||||
|
size='small'
|
||||||
|
style={{ fontVariantNumeric: 'tabular-nums' }}
|
||||||
|
>
|
||||||
|
{t('{{count}} 项操作', { count })}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
link
|
||||||
|
size='small'
|
||||||
|
style={{ fontWeight: 600 }}
|
||||||
|
onClick={onOpen}
|
||||||
|
>
|
||||||
|
{t('查看详情')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ParamOverrideEntry);
|
||||||
@@ -22,8 +22,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
Empty,
|
Empty,
|
||||||
Space,
|
Divider,
|
||||||
Tag,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { IconCopy } from '@douyinfe/semi-icons';
|
import { IconCopy } from '@douyinfe/semi-icons';
|
||||||
@@ -35,59 +34,66 @@ const parseAuditLine = (line) => {
|
|||||||
if (typeof line !== 'string') {
|
if (typeof line !== 'string') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const colonIndex = line.indexOf(': ');
|
const firstSpaceIndex = line.indexOf(' ');
|
||||||
const arrowIndex = line.indexOf(' -> ', colonIndex + 2);
|
if (firstSpaceIndex <= 0) {
|
||||||
if (colonIndex <= 0 || arrowIndex <= colonIndex) {
|
return { action: line, content: line };
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
field: line.slice(0, colonIndex),
|
action: line.slice(0, firstSpaceIndex),
|
||||||
before: line.slice(colonIndex + 2, arrowIndex),
|
content: line.slice(firstSpaceIndex + 1),
|
||||||
after: line.slice(arrowIndex + 4),
|
|
||||||
raw: line,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const ValuePanel = ({ label, value, tone }) => (
|
const getActionLabel = (action, t) => {
|
||||||
<div
|
switch ((action || '').toLowerCase()) {
|
||||||
style={{
|
case 'set':
|
||||||
flex: 1,
|
return t('设置');
|
||||||
minWidth: 0,
|
case 'delete':
|
||||||
padding: 12,
|
return t('删除');
|
||||||
borderRadius: 12,
|
case 'copy':
|
||||||
border: '1px solid var(--semi-color-border)',
|
return t('复制');
|
||||||
background:
|
case 'move':
|
||||||
tone === 'after'
|
return t('移动');
|
||||||
? 'rgba(var(--semi-blue-5), 0.08)'
|
case 'append':
|
||||||
: 'var(--semi-color-fill-0)',
|
return t('追加');
|
||||||
}}
|
case 'prepend':
|
||||||
>
|
return t('前置');
|
||||||
<div
|
case 'trim_prefix':
|
||||||
style={{
|
return t('去前缀');
|
||||||
marginBottom: 6,
|
case 'trim_suffix':
|
||||||
fontSize: 12,
|
return t('去后缀');
|
||||||
fontWeight: 600,
|
case 'ensure_prefix':
|
||||||
color: 'var(--semi-color-text-2)',
|
return t('保前缀');
|
||||||
}}
|
case 'ensure_suffix':
|
||||||
>
|
return t('保后缀');
|
||||||
{label}
|
case 'trim_space':
|
||||||
</div>
|
return t('去空格');
|
||||||
<Text
|
case 'to_lower':
|
||||||
style={{
|
return t('转小写');
|
||||||
display: 'block',
|
case 'to_upper':
|
||||||
fontFamily:
|
return t('转大写');
|
||||||
'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
|
case 'replace':
|
||||||
fontSize: 13,
|
return t('替换');
|
||||||
lineHeight: 1.65,
|
case 'regex_replace':
|
||||||
whiteSpace: 'pre-wrap',
|
return t('正则替换');
|
||||||
wordBreak: 'break-word',
|
case 'set_header':
|
||||||
}}
|
return t('设请求头');
|
||||||
>
|
case 'delete_header':
|
||||||
{value}
|
return t('删请求头');
|
||||||
</Text>
|
case 'copy_header':
|
||||||
</div>
|
return t('复制请求头');
|
||||||
);
|
case 'move_header':
|
||||||
|
return t('移动请求头');
|
||||||
|
case 'pass_headers':
|
||||||
|
return t('透传请求头');
|
||||||
|
case 'sync_fields':
|
||||||
|
return t('同步字段');
|
||||||
|
case 'return_error':
|
||||||
|
return t('返回错误');
|
||||||
|
default:
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const ParamOverrideModal = ({
|
const ParamOverrideModal = ({
|
||||||
showParamOverrideModal,
|
showParamOverrideModal,
|
||||||
@@ -124,147 +130,135 @@ const ParamOverrideModal = ({
|
|||||||
centered
|
centered
|
||||||
closable
|
closable
|
||||||
maskClosable
|
maskClosable
|
||||||
width={760}
|
width={640}
|
||||||
>
|
>
|
||||||
<div style={{ padding: 20 }}>
|
<div style={{ padding: '8px 20px 20px' }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginBottom: 16,
|
display: 'flex',
|
||||||
padding: 16,
|
justifyContent: 'space-between',
|
||||||
borderRadius: 16,
|
alignItems: 'flex-start',
|
||||||
background:
|
gap: 12,
|
||||||
'linear-gradient(135deg, rgba(var(--semi-blue-5), 0.08), rgba(var(--semi-teal-5), 0.12))',
|
marginBottom: 10,
|
||||||
border: '1px solid rgba(var(--semi-blue-5), 0.16)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div style={{ minWidth: 0 }}>
|
||||||
style={{
|
<div style={{ marginBottom: 4 }}>
|
||||||
display: 'flex',
|
<Text style={{ fontWeight: 600 }}>
|
||||||
justifyContent: 'space-between',
|
{t('{{count}} 项操作', { count: lines.length })}
|
||||||
gap: 12,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: 'var(--semi-color-text-0)',
|
|
||||||
marginBottom: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('已应用参数覆盖')}
|
|
||||||
</div>
|
|
||||||
<Space wrap spacing={8}>
|
|
||||||
<Tag color='blue' size='large'>
|
|
||||||
{t('{{count}} 项变更', { count: lines.length })}
|
|
||||||
</Tag>
|
|
||||||
{paramOverrideTarget?.modelName ? (
|
|
||||||
<Tag color='cyan' size='large'>
|
|
||||||
{paramOverrideTarget.modelName}
|
|
||||||
</Tag>
|
|
||||||
) : null}
|
|
||||||
{paramOverrideTarget?.requestId ? (
|
|
||||||
<Tag color='grey' size='large'>
|
|
||||||
{t('Request ID')}: {paramOverrideTarget.requestId}
|
|
||||||
</Tag>
|
|
||||||
) : null}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
icon={<IconCopy />}
|
|
||||||
theme='solid'
|
|
||||||
type='tertiary'
|
|
||||||
onClick={copyAll}
|
|
||||||
disabled={lines.length === 0}
|
|
||||||
>
|
|
||||||
{t('复制全部')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{paramOverrideTarget?.requestPath ? (
|
|
||||||
<div style={{ marginTop: 12 }}>
|
|
||||||
<Text type='tertiary' size='small'>
|
|
||||||
{t('请求路径')}: {paramOverrideTarget.requestPath}
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8,
|
||||||
|
fontSize: 12,
|
||||||
|
color: 'var(--semi-color-text-2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{paramOverrideTarget?.modelName ? (
|
||||||
|
<Text type='tertiary' size='small'>
|
||||||
|
{paramOverrideTarget.modelName}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{paramOverrideTarget?.requestId ? (
|
||||||
|
<Text type='tertiary' size='small'>
|
||||||
|
{t('Request ID')}: {paramOverrideTarget.requestId}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{paramOverrideTarget?.requestPath ? (
|
||||||
|
<Text type='tertiary' size='small'>
|
||||||
|
{t('请求路径')}: {paramOverrideTarget.requestPath}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={<IconCopy />}
|
||||||
|
theme='borderless'
|
||||||
|
type='tertiary'
|
||||||
|
size='small'
|
||||||
|
onClick={copyAll}
|
||||||
|
disabled={lines.length === 0}
|
||||||
|
>
|
||||||
|
{t('复制')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Divider margin='12px' />
|
||||||
|
|
||||||
{lines.length === 0 ? (
|
{lines.length === 0 ? (
|
||||||
<Empty
|
<Empty
|
||||||
description={t('暂无参数覆盖记录')}
|
description={t('暂无参数覆盖记录')}
|
||||||
style={{ padding: '32px 0 12px' }}
|
style={{ padding: '24px 0 8px' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 12,
|
gap: 8,
|
||||||
maxHeight: '60vh',
|
maxHeight: '56vh',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
paddingRight: 4,
|
paddingRight: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{parsedLines.map((item, index) => {
|
{parsedLines.map((item, index) => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return (
|
return null;
|
||||||
<div
|
|
||||||
key={`raw-${index}`}
|
|
||||||
style={{
|
|
||||||
padding: 14,
|
|
||||||
borderRadius: 14,
|
|
||||||
border: '1px solid var(--semi-color-border)',
|
|
||||||
background: 'var(--semi-color-fill-0)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontFamily:
|
|
||||||
'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
|
|
||||||
fontSize: 13,
|
|
||||||
lineHeight: 1.65,
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{lines[index]}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${item.field}-${index}`}
|
key={`${item.action}-${index}`}
|
||||||
style={{
|
style={{
|
||||||
padding: 14,
|
padding: '10px 12px',
|
||||||
borderRadius: 16,
|
borderRadius: 10,
|
||||||
border: '1px solid var(--semi-color-border)',
|
border: '1px solid var(--semi-color-border)',
|
||||||
background: 'var(--semi-color-bg-0)',
|
background: 'var(--semi-color-fill-0)',
|
||||||
boxShadow: '0 8px 24px rgba(15, 23, 42, 0.04)',
|
display: 'flex',
|
||||||
|
gap: 12,
|
||||||
|
alignItems: 'flex-start',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: 12 }}>
|
|
||||||
<Tag color='blue' shape='circle' size='large'>
|
|
||||||
{item.field}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
flex: '0 0 auto',
|
||||||
gap: 12,
|
minWidth: 74,
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignItems: 'stretch',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ValuePanel label={t('变更前')} value={item.before} />
|
<Text
|
||||||
<ValuePanel label={t('变更后')} value={item.after} tone='after' />
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: '20px',
|
||||||
|
padding: '0 8px',
|
||||||
|
borderRadius: 999,
|
||||||
|
background: 'rgba(var(--semi-blue-5), 0.12)',
|
||||||
|
color: 'var(--semi-color-primary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getActionLabel(item.action, t)}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
fontFamily:
|
||||||
|
'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
color: 'var(--semi-color-text-0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.content}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
+9
-24
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal, Button, Tag } from '@douyinfe/semi-ui';
|
import { Modal } from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
getTodayStartTimestamp,
|
getTodayStartTimestamp,
|
||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
import { ITEMS_PER_PAGE } from '../../constants';
|
||||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||||
|
import ParamOverrideEntry from '../../components/table/usage-logs/components/ParamOverrideEntry';
|
||||||
|
|
||||||
export const useLogsData = () => {
|
export const useLogsData = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -604,30 +605,14 @@ export const useLogsData = () => {
|
|||||||
expandDataLocal.push({
|
expandDataLocal.push({
|
||||||
key: t('参数覆盖'),
|
key: t('参数覆盖'),
|
||||||
value: (
|
value: (
|
||||||
<div
|
<ParamOverrideEntry
|
||||||
style={{
|
count={other.po.length}
|
||||||
display: 'flex',
|
t={t}
|
||||||
alignItems: 'center',
|
onOpen={(event) => {
|
||||||
gap: 10,
|
event.stopPropagation();
|
||||||
flexWrap: 'wrap',
|
openParamOverrideModal(logs[i], other);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Tag color='blue' shape='circle'>
|
|
||||||
{t('{{count}} 项变更', { count: other.po.length })}
|
|
||||||
</Tag>
|
|
||||||
<Button
|
|
||||||
theme='borderless'
|
|
||||||
type='primary'
|
|
||||||
size='small'
|
|
||||||
style={{ paddingLeft: 0 }}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
openParamOverrideModal(logs[i], other);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('查看详情')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user