From bc80477b1a643adeceb211274109eab6fd568016 Mon Sep 17 00:00:00 2001 From: Seefs Date: Tue, 17 Mar 2026 16:22:26 +0800 Subject: [PATCH] feat: simplify param override audit UI and operation labels --- relay/common/override.go | 239 ++++++++------ relay/common/override_test.go | 100 ++++++ .../components/ParamOverrideEntry.jsx | 54 ++++ .../usage-logs/modals/ParamOverrideModal.jsx | 304 +++++++++--------- web/src/hooks/usage-logs/useUsageLogsData.jsx | 33 +- 5 files changed, 448 insertions(+), 282 deletions(-) create mode 100644 web/src/components/table/usage-logs/components/ParamOverrideEntry.jsx diff --git a/relay/common/override.go b/relay/common/override.go index 17b0769b..d75216d1 100644 --- a/relay/common/override.go +++ b/relay/common/override.go @@ -230,28 +230,14 @@ func getParamOverrideAuditRecorder(context map[string]interface{}) *paramOverrid 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 { return } - path = strings.TrimSpace(path) - if path == "" { + line := buildParamOverrideAuditLine(mode, path, from, to, value) + if line == "" { return } - if !shouldAuditParamPath(path) { - return - } - - beforeText := "" - if beforeExists { - beforeText = formatParamOverrideAuditValue(beforeValue) - } - afterText := "" - if afterExists { - afterText = formatParamOverrideAuditValue(afterValue) - } - - line := fmt.Sprintf("%s: %s -> %s", path, beforeText, afterText) if lo.Contains(r.lines, line) { return } @@ -270,23 +256,16 @@ func shouldAuditParamPath(path string) bool { return ok } -func applyAuditedPathMutation(result, path string, auditRecorder *paramOverrideAuditRecorder, mutate func(string) (string, error)) (string, error) { - needAudit := auditRecorder != nil && shouldAuditParamPath(path) - var beforeResult gjson.Result - if needAudit { - beforeResult = gjson.Get(result, path) +func shouldAuditOperation(mode, path, from, to string) bool { + if common.DebugEnabled { + return true } - - next, err := mutate(result) - if err != nil { - return next, err + for _, candidate := range []string{path, to} { + if shouldAuditParamPath(candidate) { + return true + } } - - if needAudit { - afterResult := gjson.Get(next, path) - auditRecorder.record(path, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value()) - } - return next, nil + return false } 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{} { if info == nil || info.ChannelMeta == nil { return nil @@ -594,9 +661,8 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{} } for key, value := range paramOverride { - beforeValue, beforeExists := reqMap[key] reqMap[key] = value - auditRecorder.record(key, beforeExists, beforeValue, true, value) + auditRecorder.recordOperation("set", key, "", "", value) } return common.Marshal(reqMap) @@ -636,47 +702,29 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte switch op.Mode { case "delete": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return deleteValue(current, path) - }) + result, err = deleteValue(result, path) if err != nil { break } + auditRecorder.recordOperation("delete", path, "", "", nil) } case "set": for _, path := range opPaths { if op.KeepOrigin && gjson.Get(result, path).Exists() { continue } - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return sjson.Set(current, path, op.Value) - }) + result, err = sjson.Set(result, path, op.Value) if err != nil { break } + auditRecorder.recordOperation("set", path, "", "", op.Value) } case "move": opFrom := processNegativeIndex(result, op.From) 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) if err == nil { - if needAuditTo { - 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) - } + auditRecorder.recordOperation("move", "", opFrom, opTo, nil) } case "copy": if op.From == "" || op.To == "" { @@ -684,116 +732,100 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte } opFrom := processNegativeIndex(result, op.From) 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) - if err == nil && needAudit { - afterResult := gjson.Get(result, opTo) - auditRecorder.record(opTo, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value()) + if err == nil { + auditRecorder.recordOperation("copy", "", opFrom, opTo, nil) } case "prepend": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return modifyValue(current, path, op.Value, op.KeepOrigin, true) - }) + result, err = modifyValue(result, path, op.Value, op.KeepOrigin, true) if err != nil { break } + auditRecorder.recordOperation("prepend", path, "", "", op.Value) } case "append": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return modifyValue(current, path, op.Value, op.KeepOrigin, false) - }) + result, err = modifyValue(result, path, op.Value, op.KeepOrigin, false) if err != nil { break } + auditRecorder.recordOperation("append", path, "", "", op.Value) } case "trim_prefix": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return trimStringValue(current, path, op.Value, true) - }) + result, err = trimStringValue(result, path, op.Value, true) if err != nil { break } + auditRecorder.recordOperation("trim_prefix", path, "", "", op.Value) } case "trim_suffix": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return trimStringValue(current, path, op.Value, false) - }) + result, err = trimStringValue(result, path, op.Value, false) if err != nil { break } + auditRecorder.recordOperation("trim_suffix", path, "", "", op.Value) } case "ensure_prefix": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return ensureStringAffix(current, path, op.Value, true) - }) + result, err = ensureStringAffix(result, path, op.Value, true) if err != nil { break } + auditRecorder.recordOperation("ensure_prefix", path, "", "", op.Value) } case "ensure_suffix": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return ensureStringAffix(current, path, op.Value, false) - }) + result, err = ensureStringAffix(result, path, op.Value, false) if err != nil { break } + auditRecorder.recordOperation("ensure_suffix", path, "", "", op.Value) } case "trim_space": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return transformStringValue(current, path, strings.TrimSpace) - }) + result, err = transformStringValue(result, path, strings.TrimSpace) if err != nil { break } + auditRecorder.recordOperation("trim_space", path, "", "", nil) } case "to_lower": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return transformStringValue(current, path, strings.ToLower) - }) + result, err = transformStringValue(result, path, strings.ToLower) if err != nil { break } + auditRecorder.recordOperation("to_lower", path, "", "", nil) } case "to_upper": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return transformStringValue(current, path, strings.ToUpper) - }) + result, err = transformStringValue(result, path, strings.ToUpper) if err != nil { break } + auditRecorder.recordOperation("to_upper", path, "", "", nil) } case "replace": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return replaceStringValue(current, path, op.From, op.To) - }) + result, err = replaceStringValue(result, path, op.From, op.To) if err != nil { break } + auditRecorder.recordOperation("replace", path, op.From, op.To, nil) } case "regex_replace": for _, path := range opPaths { - result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { - return regexReplaceStringValue(current, path, op.From, op.To) - }) + result, err = regexReplaceStringValue(result, path, op.From, op.To) if err != nil { break } + auditRecorder.recordOperation("regex_replace", path, op.From, op.To, nil) } case "return_error": + auditRecorder.recordOperation("return_error", op.Path, "", "", op.Value) returnErr, parseErr := parseParamOverrideReturnError(op.Value) if parseErr != nil { return "", parseErr @@ -809,11 +841,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte case "set_header": err = setHeaderOverrideInContext(context, op.Path, op.Value, op.KeepOrigin) if err == nil { + auditRecorder.recordOperation("set_header", op.Path, "", "", op.Value) contextJSON, err = marshalContextJSON(context) } case "delete_header": err = deleteHeaderOverrideInContext(context, op.Path) if err == nil { + auditRecorder.recordOperation("delete_header", op.Path, "", "", nil) contextJSON, err = marshalContextJSON(context) } case "copy_header": @@ -830,6 +864,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte err = nil } if err == nil { + auditRecorder.recordOperation("copy_header", "", sourceHeader, targetHeader, nil) contextJSON, err = marshalContextJSON(context) } case "move_header": @@ -846,6 +881,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte err = nil } if err == nil { + auditRecorder.recordOperation("move_header", "", sourceHeader, targetHeader, nil) contextJSON, err = marshalContextJSON(context) } case "pass_headers": @@ -863,11 +899,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte } } if err == nil { + auditRecorder.recordOperation("pass_headers", "", "", "", headerNames) contextJSON, err = marshalContextJSON(context) } case "sync_fields": result, err = syncFieldsBetweenTargets(result, context, op.From, op.To) if err == nil { + auditRecorder.recordOperation("sync_fields", "", op.From, op.To, nil) contextJSON, err = marshalContextJSON(context) } default: @@ -985,7 +1023,6 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin } rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride) - beforeRaw, beforeExists := rawHeaders[headerName] if keepOrigin { if existing, ok := rawHeaders[headerName]; ok { existingValue := strings.TrimSpace(fmt.Sprintf("%v", existing)) @@ -1001,12 +1038,10 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin } if !hasValue { delete(rawHeaders, headerName) - getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, false, nil) return nil } rawHeaders[headerName] = headerValue - getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, true, headerValue) return nil } @@ -1178,9 +1213,7 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st return fmt.Errorf("header name is required") } rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride) - beforeRaw, beforeExists := rawHeaders[headerName] delete(rawHeaders, headerName) - getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, false, nil) return nil } diff --git a/relay/common/override_test.go b/relay/common/override_test.go index c41be219..1a7793ba 100644 --- a/relay/common/override_test.go +++ b/relay/common/override_test.go @@ -6,6 +6,7 @@ import ( "reflect" "testing" + common2 "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/types" "github.com/QuantumNous/new-api/dto" @@ -2066,6 +2067,105 @@ func TestRemoveDisabledFieldsAllowInferenceGeo(t *testing.T) { 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) { t.Helper() diff --git a/web/src/components/table/usage-logs/components/ParamOverrideEntry.jsx b/web/src/components/table/usage-logs/components/ParamOverrideEntry.jsx new file mode 100644 index 00000000..aa7c7499 --- /dev/null +++ b/web/src/components/table/usage-logs/components/ParamOverrideEntry.jsx @@ -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 . + +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 ( +
+ + {t('{{count}} 项操作', { count })} + + + {t('查看详情')} + +
+ ); +}; + +export default React.memo(ParamOverrideEntry); diff --git a/web/src/components/table/usage-logs/modals/ParamOverrideModal.jsx b/web/src/components/table/usage-logs/modals/ParamOverrideModal.jsx index 4d14d1ea..0b0c6ec8 100644 --- a/web/src/components/table/usage-logs/modals/ParamOverrideModal.jsx +++ b/web/src/components/table/usage-logs/modals/ParamOverrideModal.jsx @@ -22,8 +22,7 @@ import { Modal, Button, Empty, - Space, - Tag, + Divider, Typography, } from '@douyinfe/semi-ui'; import { IconCopy } from '@douyinfe/semi-icons'; @@ -35,59 +34,66 @@ const parseAuditLine = (line) => { if (typeof line !== 'string') { return null; } - const colonIndex = line.indexOf(': '); - const arrowIndex = line.indexOf(' -> ', colonIndex + 2); - if (colonIndex <= 0 || arrowIndex <= colonIndex) { - return null; + const firstSpaceIndex = line.indexOf(' '); + if (firstSpaceIndex <= 0) { + return { action: line, content: line }; } - return { - field: line.slice(0, colonIndex), - before: line.slice(colonIndex + 2, arrowIndex), - after: line.slice(arrowIndex + 4), - raw: line, + action: line.slice(0, firstSpaceIndex), + content: line.slice(firstSpaceIndex + 1), }; }; -const ValuePanel = ({ label, value, tone }) => ( -
-
- {label} -
- - {value} - -
-); +const getActionLabel = (action, t) => { + switch ((action || '').toLowerCase()) { + case 'set': + return t('设置'); + case 'delete': + return t('删除'); + case 'copy': + return t('复制'); + case 'move': + return t('移动'); + case 'append': + return t('追加'); + case 'prepend': + return t('前置'); + case 'trim_prefix': + return t('去前缀'); + case 'trim_suffix': + return t('去后缀'); + case 'ensure_prefix': + return t('保前缀'); + case 'ensure_suffix': + return t('保后缀'); + case 'trim_space': + return t('去空格'); + case 'to_lower': + return t('转小写'); + case 'to_upper': + return t('转大写'); + case 'replace': + return t('替换'); + case 'regex_replace': + return t('正则替换'); + case 'set_header': + return t('设请求头'); + case 'delete_header': + return t('删请求头'); + case 'copy_header': + 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 = ({ showParamOverrideModal, @@ -124,147 +130,135 @@ const ParamOverrideModal = ({ centered closable maskClosable - width={760} + width={640} > -
+
-
-
-
- {t('已应用参数覆盖')} -
- - - {t('{{count}} 项变更', { count: lines.length })} - - {paramOverrideTarget?.modelName ? ( - - {paramOverrideTarget.modelName} - - ) : null} - {paramOverrideTarget?.requestId ? ( - - {t('Request ID')}: {paramOverrideTarget.requestId} - - ) : null} - -
- - -
- - {paramOverrideTarget?.requestPath ? ( -
- - {t('请求路径')}: {paramOverrideTarget.requestPath} +
+
+ + {t('{{count}} 项操作', { count: lines.length })}
- ) : null} +
+ {paramOverrideTarget?.modelName ? ( + + {paramOverrideTarget.modelName} + + ) : null} + {paramOverrideTarget?.requestId ? ( + + {t('Request ID')}: {paramOverrideTarget.requestId} + + ) : null} + {paramOverrideTarget?.requestPath ? ( + + {t('请求路径')}: {paramOverrideTarget.requestPath} + + ) : null} +
+
+ +
+ + {lines.length === 0 ? ( ) : (
{parsedLines.map((item, index) => { if (!item) { - return ( -
- - {lines[index]} - -
- ); + return null; } return (
-
- - {item.field} - -
- - + + {getActionLabel(item.action, t)} +
+ + {item.content} +
); })} diff --git a/web/src/hooks/usage-logs/useUsageLogsData.jsx b/web/src/hooks/usage-logs/useUsageLogsData.jsx index 566602df..d4ac9df4 100644 --- a/web/src/hooks/usage-logs/useUsageLogsData.jsx +++ b/web/src/hooks/usage-logs/useUsageLogsData.jsx @@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Modal, Button, Tag } from '@douyinfe/semi-ui'; +import { Modal } from '@douyinfe/semi-ui'; import { API, getTodayStartTimestamp, @@ -39,6 +39,7 @@ import { } from '../../helpers'; import { ITEMS_PER_PAGE } from '../../constants'; import { useTableCompactMode } from '../common/useTableCompactMode'; +import ParamOverrideEntry from '../../components/table/usage-logs/components/ParamOverrideEntry'; export const useLogsData = () => { const { t } = useTranslation(); @@ -604,30 +605,14 @@ export const useLogsData = () => { expandDataLocal.push({ key: t('参数覆盖'), value: ( -
{ + event.stopPropagation(); + openParamOverrideModal(logs[i], other); }} - > - - {t('{{count}} 项变更', { count: other.po.length })} - - -
+ /> ), }); }