feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { JsonField } from './json-field';
|
||||
|
||||
export { Line } from './line';
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { last } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconTreeTriangleRight } from '@douyinfe/semi-icons';
|
||||
|
||||
import { Line } from '../line';
|
||||
import { type Field } from '../../types';
|
||||
import { useValue } from '../../hooks/use-value';
|
||||
import { useExpand } from '../../hooks';
|
||||
import { LogObjSpecialKey, LogValueStyleType } from '../../constants';
|
||||
|
||||
import styles from './json-field.module.less';
|
||||
|
||||
/* JSON 类型数据渲染 */
|
||||
const FieldValue: React.FC<{
|
||||
value: Field['value'];
|
||||
}> = ({ value }) => {
|
||||
const { value: current, type } = useValue(value);
|
||||
return (
|
||||
<span
|
||||
data-testid="json-viewer-field-value"
|
||||
className={cls({
|
||||
[styles['field-value-number']]: type === LogValueStyleType.Number,
|
||||
[styles['field-value-boolean']]: type === LogValueStyleType.Boolean,
|
||||
})}
|
||||
>
|
||||
{current}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const JsonField: React.FC<{ field: Field }> = ({ field }) => {
|
||||
const { lines, children, path, isObj } = field;
|
||||
const echoLines = useMemo(() => lines.slice(1), [lines]);
|
||||
|
||||
const pathStr = useMemo(() => path.join('.'), [path]);
|
||||
|
||||
const isError = useMemo(() => pathStr === LogObjSpecialKey.Error, [pathStr]);
|
||||
const isWarning = useMemo(
|
||||
() => pathStr === LogObjSpecialKey.Warning,
|
||||
[pathStr],
|
||||
);
|
||||
|
||||
const key = useMemo(() => last(path), [path]);
|
||||
const keyWithColon = useMemo(() => {
|
||||
if (isError) {
|
||||
return I18n.t('workflow_detail_testrun_error_front');
|
||||
}
|
||||
if (isWarning) {
|
||||
return I18n.t('workflow_detail_testrun_warning_front');
|
||||
}
|
||||
return key ? `${key} : ` : '';
|
||||
}, [key, isError, isWarning]);
|
||||
|
||||
const { expand, onChange } = useExpand(path.join('.'));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles['json-viewer-field']}>
|
||||
{echoLines.map((l, idx) => (
|
||||
<Line status={l} key={idx} />
|
||||
))}
|
||||
<div
|
||||
data-testid="json-viewer-field-content"
|
||||
className={cls('field-content', styles['field-content'], {
|
||||
[styles['is-error']]: isError,
|
||||
[styles['is-warning']]: isWarning,
|
||||
})}
|
||||
onClick={isObj ? onChange : undefined}
|
||||
>
|
||||
{isObj ? (
|
||||
<>
|
||||
<span
|
||||
data-testid="json-viewer-json-field-expander"
|
||||
className={cls('field-icon', styles['field-icon'], {
|
||||
[styles.expand]: expand,
|
||||
})}
|
||||
>
|
||||
<IconTreeTriangleRight size="inherit" />
|
||||
</span>
|
||||
<span className={cls('field-key', styles['field-key'])}>
|
||||
{key}
|
||||
</span>
|
||||
<span className={cls('field-len', styles['field-len'])}>
|
||||
{` {${children.length}}`}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
className={cls('field-block', styles['field-block'])}
|
||||
></span>
|
||||
{keyWithColon ? (
|
||||
<span className={cls('field-key', styles['field-key'])}>
|
||||
{keyWithColon}
|
||||
</span>
|
||||
) : null}
|
||||
<span
|
||||
className={cls('field-value', styles['field-value'], {
|
||||
'whitespace-pre-wrap': !isObj,
|
||||
})}
|
||||
>
|
||||
<FieldValue value={field.value} />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{expand
|
||||
? children.map(i => <JsonField field={i} key={i.path.join('.')} />)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { JsonField };
|
||||
@@ -0,0 +1,59 @@
|
||||
.json-viewer-field {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
|
||||
.field-content {
|
||||
flex: 1 1 0;
|
||||
padding: 2px 0;
|
||||
user-select: auto;
|
||||
|
||||
&.is-error .field-key,
|
||||
&.is-error .field-value {
|
||||
color: #FF441E;
|
||||
}
|
||||
|
||||
&.is-warning .field-key,
|
||||
&.is-warning .field-value {
|
||||
color: #FF9600;
|
||||
}
|
||||
|
||||
.field-value-number {
|
||||
color: #03B6D0;
|
||||
}
|
||||
|
||||
.field-value-boolean {
|
||||
color: #BB2BC9;
|
||||
}
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
color: #4D53E8;
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&.expand>span {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.field-block {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.field-key {
|
||||
color: #4D53E8;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.field-len {
|
||||
color: rgba(29, 28, 35, 0.35);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
.json-viewer-line {
|
||||
position: relative;
|
||||
|
||||
// 一般来说一行高度为 24px,但由于支持换行所以这里只能自适应,因此 line 不再具有撑高元素的能力,需要在外部去设置 minHeight
|
||||
// height: 100%;
|
||||
width: 24px;
|
||||
|
||||
display: inline-block;
|
||||
|
||||
// 不能被压缩也不能扩展,防止样式错乱
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
// 竖线
|
||||
&::before {
|
||||
top: 0;
|
||||
// 左边留 6px 用于和顶部 icon 对齐
|
||||
left: 6px;
|
||||
width: 0;
|
||||
// 这里同理,集成父元素的高度
|
||||
height: 100%;
|
||||
border-left: 1px solid transparent;
|
||||
}
|
||||
|
||||
// 横线
|
||||
&::after {
|
||||
top: 5px;
|
||||
left: 6px;
|
||||
width: 12px;
|
||||
height: 6px;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-left: 1px solid transparent;
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
|
||||
&.visible::before,
|
||||
&.visible::after,
|
||||
&.last::before,
|
||||
&.last::after {
|
||||
border-color: #C6C6CD;
|
||||
}
|
||||
|
||||
// 最后的元素不需要延伸的竖线,缩短竖线
|
||||
&.last::before {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&.half::before {
|
||||
border-color: #C6C6CD;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
|
||||
import { LineStatus } from '../../types';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const Line: React.FC<{ status: LineStatus }> = ({ status }) => (
|
||||
<div
|
||||
className={cls(styles['json-viewer-line'], {
|
||||
[styles.hidden]: status === LineStatus.Hidden,
|
||||
[styles.visible]: status === LineStatus.Visible,
|
||||
[styles.half]: status === LineStatus.Half,
|
||||
[styles.last]: status === LineStatus.Last,
|
||||
})}
|
||||
></div>
|
||||
);
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Typography } from '@coze-arch/bot-semi';
|
||||
|
||||
import { generateStrAvoidEscape } from '../utils/generate-str-avoid-escape';
|
||||
|
||||
export const MAX_LENGTH = 10000;
|
||||
|
||||
export const LongStrValue: React.FC<{ str: string }> = ({ str }) => {
|
||||
const [more, setMore] = useState(false);
|
||||
|
||||
const echoStr = useMemo(() => {
|
||||
const current = more ? str : str.slice(0, MAX_LENGTH);
|
||||
return generateStrAvoidEscape(current);
|
||||
}, [str, more]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{echoStr}
|
||||
{!more && (
|
||||
<Typography.Text link onClick={() => setMore(true)}>
|
||||
{I18n.t('see_more')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
|
||||
const TextField: React.FC<{ text: string }> = ({ text }) => {
|
||||
const paragraphs = text.split('\n');
|
||||
|
||||
return (
|
||||
<div className={'flex'}>
|
||||
<div className={cls('select-auto', 'py-[2px] px-0', 'text-sm')}>
|
||||
{paragraphs.map(paragraph => (
|
||||
<div className="pl-4" data-testid="json-viewer-text-field-paragraph">
|
||||
<span className={'whitespace-pre-wrap'}>
|
||||
<span>{paragraph}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { TextField };
|
||||
28
frontend/packages/components/json-viewer/src/constants.ts
Normal file
28
frontend/packages/components/json-viewer/src/constants.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** 预置的特殊的 key */
|
||||
export enum LogObjSpecialKey {
|
||||
Error = '$error',
|
||||
Warning = '$warning',
|
||||
}
|
||||
|
||||
/** log 中 value 的显示样式类型 */
|
||||
export enum LogValueStyleType {
|
||||
Default,
|
||||
Number,
|
||||
Boolean,
|
||||
}
|
||||
100
frontend/packages/components/json-viewer/src/context.tsx
Normal file
100
frontend/packages/components/json-viewer/src/context.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
type PropsWithChildren,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import { createContext } from 'use-context-selector';
|
||||
import { noop } from 'lodash-es';
|
||||
|
||||
import { type Field } from './types';
|
||||
|
||||
interface JsonViewerContextType {
|
||||
expand: Record<string, boolean> | null;
|
||||
onExpand: (path: string, val: boolean) => void;
|
||||
}
|
||||
interface JsonViewerProviderProps {
|
||||
fields: Field[];
|
||||
defaultExpandAllFields?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根只有一项且其可以下钻时,默认展开它
|
||||
*/
|
||||
const generateInitialExpandValue = (fields: Field[], expandAll?: boolean) => {
|
||||
if (expandAll) {
|
||||
return setExpandAllFields(fields);
|
||||
}
|
||||
if (fields.length === 1 && fields[0]?.isObj) {
|
||||
return {
|
||||
[fields[0].path.join('.')]: true,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const setExpandAllFields = (fields: Field[]) =>
|
||||
fields.reduce(
|
||||
(acc, field) => ({
|
||||
...acc,
|
||||
[field.path.join('.')]: true,
|
||||
...setExpandAllFields(field.children),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
export const JsonViewerContext = createContext<JsonViewerContextType>({
|
||||
expand: {},
|
||||
onExpand: noop,
|
||||
});
|
||||
|
||||
export const JsonViewerProvider: React.FC<
|
||||
PropsWithChildren<JsonViewerProviderProps>
|
||||
> = ({ fields, children, defaultExpandAllFields }) => {
|
||||
/** 因为存在不属于单项的逻辑,所以集中管理展开折叠的状态 */
|
||||
const [expand, setExpand] = useState<JsonViewerContextType['expand'] | null>(
|
||||
null,
|
||||
);
|
||||
const handleExpand = useCallback(
|
||||
(path: string, val: boolean) => setExpand(e => ({ ...e, [path]: val })),
|
||||
[setExpand],
|
||||
);
|
||||
|
||||
/**
|
||||
* fields 是动态更新的,这里要注意固化 expand 数据,因为 fields 总是由少增多
|
||||
* 由于存在自动展开逻辑,所以从 0 => 1 变化时需要赋值
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!expand) {
|
||||
const autoExpand = generateInitialExpandValue(
|
||||
fields,
|
||||
defaultExpandAllFields,
|
||||
);
|
||||
if (autoExpand) {
|
||||
setExpand(autoExpand);
|
||||
}
|
||||
}
|
||||
}, [expand, fields, setExpand, defaultExpandAllFields]);
|
||||
return (
|
||||
<JsonViewerContext.Provider value={{ expand, onExpand: handleExpand }}>
|
||||
{children}
|
||||
</JsonViewerContext.Provider>
|
||||
);
|
||||
};
|
||||
20
frontend/packages/components/json-viewer/src/declarations.d.ts
vendored
Normal file
20
frontend/packages/components/json-viewer/src/declarations.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare module '*.less' {
|
||||
const resource: { [key: string]: string };
|
||||
export = resource;
|
||||
}
|
||||
18
frontend/packages/components/json-viewer/src/hooks/index.ts
Normal file
18
frontend/packages/components/json-viewer/src/hooks/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { useExpand } from './use-expand';
|
||||
export { useValue } from './use-value';
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
|
||||
import { JsonViewerContext } from '../context';
|
||||
|
||||
export const useExpand = (path: string) => {
|
||||
const expand = useContextSelector(
|
||||
JsonViewerContext,
|
||||
v => v.expand?.[path] || false,
|
||||
);
|
||||
const setExpand = useContextSelector(JsonViewerContext, v => v.onExpand);
|
||||
const handleExpandChange = useCallback(() => {
|
||||
setExpand(path, !expand);
|
||||
}, [path, expand, setExpand]);
|
||||
return {
|
||||
expand,
|
||||
onChange: handleExpandChange,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { isBoolean, isNull, isNumber, isObject, isString } from 'lodash-es';
|
||||
|
||||
import { isBigNumber, bigNumbertoString } from '../utils/big-number';
|
||||
import { generateStrAvoidEscape } from '../utils';
|
||||
import { type Field } from '../types';
|
||||
import { LogValueStyleType } from '../constants';
|
||||
import { LongStrValue, MAX_LENGTH } from '../components/long-str-value';
|
||||
|
||||
export const useValue = (value: Field['value']) => {
|
||||
const v = useMemo(() => {
|
||||
if (isNull(value)) {
|
||||
return {
|
||||
value: 'null',
|
||||
type: LogValueStyleType.Default,
|
||||
};
|
||||
} else if (isObject(value)) {
|
||||
// 大数字返回数字类型,值用字符串
|
||||
if (isBigNumber(value)) {
|
||||
return {
|
||||
value: bigNumbertoString(value),
|
||||
type: LogValueStyleType.Number,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: '',
|
||||
type: LogValueStyleType.Default,
|
||||
};
|
||||
} else if (isBoolean(value)) {
|
||||
return {
|
||||
value: value.toString(),
|
||||
type: LogValueStyleType.Boolean,
|
||||
};
|
||||
} else if (isString(value)) {
|
||||
if (value === '') {
|
||||
return {
|
||||
value: '""',
|
||||
type: LogValueStyleType.Default,
|
||||
};
|
||||
}
|
||||
if (value.length > MAX_LENGTH) {
|
||||
return {
|
||||
value: <LongStrValue str={value} />,
|
||||
type: LogValueStyleType.Default,
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: generateStrAvoidEscape(value),
|
||||
// value: generateStr2Link(value, avoidEscape), 先取消做 link 解析
|
||||
type: LogValueStyleType.Default,
|
||||
};
|
||||
} else if (isNumber(value)) {
|
||||
return {
|
||||
value,
|
||||
type: LogValueStyleType.Number,
|
||||
};
|
||||
}
|
||||
return {
|
||||
value,
|
||||
type: LogValueStyleType.Default,
|
||||
};
|
||||
}, [value]);
|
||||
return v;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
.json-viewer-wrapper {
|
||||
user-select: text;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
|
||||
/** 高度限制 */
|
||||
max-height: 272px;
|
||||
padding: 6px 12px;
|
||||
|
||||
background: rgba(46, 46, 56, 4%);
|
||||
border: 1px solid rgba(29, 28, 35, 8%);
|
||||
border-radius: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(29, 28, 35, 30%);
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(29, 28, 35, 60%);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
frontend/packages/components/json-viewer/src/index.tsx
Normal file
97
frontend/packages/components/json-viewer/src/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { isNil, isString } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
|
||||
import { generateFields } from './utils/generate-field';
|
||||
import type { JsonValueType } from './types';
|
||||
import { JsonViewerProvider } from './context';
|
||||
import { TextField } from './components/text-field';
|
||||
import { JsonField } from './components';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export type { JsonValueType };
|
||||
|
||||
export interface JsonViewerProps {
|
||||
/** 支持对象或者纯文本渲染 */
|
||||
data: JsonValueType;
|
||||
className?: React.HTMLAttributes<HTMLDivElement>['className'];
|
||||
/** 默认展开所有字段 */
|
||||
defaultExpandAllFields?: boolean;
|
||||
}
|
||||
|
||||
export const JsonViewer: React.FC<JsonViewerProps> = ({
|
||||
data,
|
||||
className,
|
||||
defaultExpandAllFields,
|
||||
}) => {
|
||||
const render = () => {
|
||||
// 兜底展示 null
|
||||
if (isNil(data)) {
|
||||
return (
|
||||
<JsonField
|
||||
field={{
|
||||
path: [],
|
||||
lines: [],
|
||||
value: 'Null',
|
||||
isObj: false,
|
||||
children: [],
|
||||
}}
|
||||
key={'Null'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 文本类结果展示
|
||||
const isStr = isString(data);
|
||||
if (isStr) {
|
||||
return <TextField text={data} />;
|
||||
}
|
||||
|
||||
// 其他json类型数据展示
|
||||
const fields = generateFields(data);
|
||||
return (
|
||||
<JsonViewerProvider
|
||||
fields={fields}
|
||||
defaultExpandAllFields={defaultExpandAllFields}
|
||||
>
|
||||
{fields.map(i => (
|
||||
<JsonField field={i} key={i.path.join('.')} />
|
||||
))}
|
||||
</JsonViewerProvider>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="json-viewer-wrapper"
|
||||
className={cls(styles['json-viewer-wrapper'], className)}
|
||||
draggable
|
||||
onDragStart={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{render()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { LogObjSpecialKey, LogValueStyleType } from './constants';
|
||||
50
frontend/packages/components/json-viewer/src/types.ts
Normal file
50
frontend/packages/components/json-viewer/src/types.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
* log 相关的类型
|
||||
*/
|
||||
/** 线可能存在的几种状态 */
|
||||
export enum LineStatus {
|
||||
/** 完全隐藏,最后一个父属性嵌套的子属性同列将不会有线 */
|
||||
Hidden,
|
||||
/** 完全显示,仅出现在属性相邻的线 */
|
||||
Visible,
|
||||
/** 半显示,非相邻的线 */
|
||||
Half,
|
||||
/** 最后属性的相邻线 */
|
||||
Last,
|
||||
}
|
||||
|
||||
/** JsonViewer 中的 value 可能值 */
|
||||
export type JsonValueType =
|
||||
| string
|
||||
| null
|
||||
| number
|
||||
| object
|
||||
| boolean
|
||||
| undefined;
|
||||
|
||||
export interface Field {
|
||||
/** 使用数组而不是 'a.b.c' 是因为可能存在 key='a.b' 会产生错误嵌套 */
|
||||
path: string[];
|
||||
lines: LineStatus[];
|
||||
/** 这里 value 可能是任意值,这里是不完全枚举 */
|
||||
value: JsonValueType;
|
||||
children: Field[];
|
||||
/** 是否是可下钻的对象(包含数组) */
|
||||
isObj: boolean;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
/**
|
||||
* 是不是大数字
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
export function isBigNumber(value: unknown): value is BigNumber {
|
||||
return !!(value && value instanceof BigNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 大数字转字符串
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
export function bigNumbertoString(value: BigNumber): string {
|
||||
return value.toFixed();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isObject } from 'lodash-es';
|
||||
|
||||
import { LineStatus, type JsonValueType, type Field } from '../types';
|
||||
import { isBigNumber } from './big-number';
|
||||
|
||||
/**
|
||||
* 通过父元素的线条状态推到子元素的线条状态
|
||||
*/
|
||||
const getLineByParent2Child = (pLine: LineStatus): LineStatus => {
|
||||
switch (pLine) {
|
||||
/** 表示父节点也是从父父节点下钻而来,此处的子节点只需要把线延续下去即可 */
|
||||
case LineStatus.Visible:
|
||||
return LineStatus.Half;
|
||||
/** 表示父节点是父父节点的最后一个节点,子节点无需再延续,渲染空白即可 */
|
||||
case LineStatus.Last:
|
||||
return LineStatus.Hidden;
|
||||
/** 其他的情况完全继承父节点的线 */
|
||||
default:
|
||||
return pLine;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 object 解析成可以循环渲染的 fields
|
||||
* 1. 若 object 非复杂类型,则返回长度为 1 的 fields 只渲染一项
|
||||
* 2. 若 object = {},则返回长度为 0 的 fields,渲染层需要做好兜底
|
||||
*/
|
||||
const generateFields = (object: JsonValueType): Field[] => {
|
||||
/** 若 object 非复杂类型 */
|
||||
if (!isObject(object) || isBigNumber(object)) {
|
||||
return [
|
||||
{
|
||||
path: [],
|
||||
lines: [],
|
||||
value: object,
|
||||
isObj: false,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 递归计算时缓存一下计算好的线,没别的意义,降低一些时间复杂度 */
|
||||
const lineMap = new Map<string[], LineStatus[]>();
|
||||
|
||||
/** 递归解析 object 为 fields */
|
||||
const dfs = ($object: object, $parentPath: string[] = []): Field[] => {
|
||||
// 如果不是对象,直接返回空数组,兜底异常情况
|
||||
if (!isObject($object)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 如果是大数字,直接返回空数组
|
||||
if (isBigNumber($object)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parentLines = lineMap.get($parentPath) || [];
|
||||
|
||||
const keys = Object.keys($object);
|
||||
|
||||
return keys.map((key, idx) => {
|
||||
const value = $object[key];
|
||||
const path = $parentPath.concat(key);
|
||||
const last = idx === keys.length - 1;
|
||||
/**
|
||||
* 根据父节点的线推导子节点的线
|
||||
*/
|
||||
const lines = parentLines
|
||||
.map<LineStatus>(getLineByParent2Child)
|
||||
/**
|
||||
* 最后拼接上子节点自己的线,最后一个节点和普通的节点有样式区分
|
||||
*/
|
||||
.concat(last ? LineStatus.Last : LineStatus.Visible);
|
||||
lineMap.set(path, lines);
|
||||
return {
|
||||
path,
|
||||
lines,
|
||||
value,
|
||||
children: dfs(value, path),
|
||||
isObj: isObject(value) && !isBigNumber(value),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return dfs(object);
|
||||
};
|
||||
|
||||
export { generateFields };
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 对象中的文本,避免字符被转译
|
||||
export const generateStrAvoidEscape = (str: string) => {
|
||||
const characters = {
|
||||
'\\': '\\\\',
|
||||
'\n': '\\n',
|
||||
'\r': '\\r',
|
||||
'\t': '\\t',
|
||||
};
|
||||
|
||||
let next = '';
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str[i];
|
||||
next += characters[char] || char;
|
||||
}
|
||||
|
||||
return next;
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { isString } from 'lodash-es';
|
||||
import { Typography } from '@coze-arch/bot-semi';
|
||||
|
||||
import { generateStrAvoidEscape } from './generate-str-avoid-escape';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const generateStr2Link = (str: string, avoidEscape?: boolean) => {
|
||||
if (str === '') {
|
||||
return [''];
|
||||
}
|
||||
|
||||
if (avoidEscape) {
|
||||
str = generateStrAvoidEscape(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更严格的 url 匹配规则,防止过度匹配
|
||||
* 协议:http、https
|
||||
* 域名:允许使用 -、a-z、A-Z、0-9,其中 - 不能开头,每一级域名长度不会超过 63
|
||||
* 端口:支持带端口 0 - 65535
|
||||
* URL:严格型不匹配中文等转译前的文字,否则一旦命中将会识别整段字符串
|
||||
*/
|
||||
const urlReg = new RegExp(
|
||||
'http(s)?://' +
|
||||
'[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+' +
|
||||
'(:[0-9]{1,5})?' +
|
||||
'[-a-zA-Z0-9()@:%_\\+.~#?&//=]*',
|
||||
'g',
|
||||
);
|
||||
const matches = [...str.matchAll(urlReg)];
|
||||
/**
|
||||
* 切割字符串,url 嵌套为 link 的样式,切割步骤:
|
||||
* 1. 匹配字符串中所有的 url
|
||||
* 2. 倒序 matches,从末尾开始切,原因是 match.index 是从头开始计数,从头切增加计算量
|
||||
* 3. 每一个 match 切两刀成三段,头尾是普通字符串,中间为 url
|
||||
* 4. 按照 end、url、start 的顺序 push 到栈中,下次 match 会直接取 start 继续切
|
||||
* 5. 切割完成后做一次倒序
|
||||
*/
|
||||
return matches
|
||||
.reverse()
|
||||
.reduce<ReactNode[]>(
|
||||
(nodes, match) => {
|
||||
const lastNode = nodes.pop();
|
||||
if (!isString(lastNode)) {
|
||||
return nodes.concat(lastNode);
|
||||
}
|
||||
const startIdx = match.index || 0;
|
||||
const endIdx = startIdx + match[0].length;
|
||||
const startStr = lastNode.slice(0, startIdx);
|
||||
const endStr = lastNode.slice(endIdx);
|
||||
return nodes.concat(
|
||||
endStr,
|
||||
<Text link={{ href: match[0], target: '_blank' }}>{match[0]}</Text>,
|
||||
startStr,
|
||||
);
|
||||
},
|
||||
[str],
|
||||
)
|
||||
.reverse();
|
||||
};
|
||||
19
frontend/packages/components/json-viewer/src/utils/index.ts
Normal file
19
frontend/packages/components/json-viewer/src/utils/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { generateFields } from './generate-field';
|
||||
export { generateStr2Link } from './generate-str-to-link';
|
||||
export { generateStrAvoidEscape } from './generate-str-avoid-escape';
|
||||
Reference in New Issue
Block a user