Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ export default forwardRef(function OriginSettings(
showPageLoadMode?: boolean;
showJSONSettings?: boolean;
showTopNSettings?: boolean;
showFlattenSettings?: boolean;
organizeFields?: string[];
setOrganizeFields?: (value?: string[]) => void;
},
ref,
) {
const { t } = useTranslation(NAME_SPACE);
const { options, updateOptions, fields, showDateField, showMoreSettings, showJSONSettings, showPageLoadMode, showTopNSettings } = props;
const { options, updateOptions, fields, showDateField, showMoreSettings, showJSONSettings, showPageLoadMode, showTopNSettings, showFlattenSettings } = props;

const [organizeFieldsModalVisible, setOrganizeFieldsModalVisible] = useState(false);
const [organizeFields, setOrganizeFields] = useState<string[] | undefined>(props.organizeFields);
Expand All @@ -48,13 +49,17 @@ export default forwardRef(function OriginSettings(
const [topNSettingsModalVisible, setTopNSettingsModalVisible] = useState(false);
const [topNumber, setTopNumber] = useState(options.topNumber ?? 5);

const [flattenSettingsModalVisible, setFlattenSettingsModalVisible] = useState(false);
const [flattenDepth, setFlattenDepth] = useState<number>(options.flattenDepth ?? 1);

useEffect(() => {
setJsonSettings({
jsonDisplaType: options.jsonDisplaType,
jsonExpandLevel: options.jsonExpandLevel,
});
setPageLoadMode(options.pageLoadMode || 'pagination');
setTopNumber(options.topNumber || 5);
setFlattenDepth(options.flattenDepth ?? 1);
}, [JSON.stringify(options)]);

useEffect(() => {
Expand Down Expand Up @@ -184,6 +189,22 @@ export default forwardRef(function OriginSettings(
},
]
: [],
showFlattenSettings
? [
{
key: 'flattenSettings',
label: (
<a
onClick={() => {
setFlattenSettingsModalVisible(true);
}}
>
{t('logs.settings.flattenSettings.title')}
</a>
),
},
]
: [],
)}
/>
}
Expand Down Expand Up @@ -335,6 +356,40 @@ export default forwardRef(function OriginSettings(
</Form.Item>
</Form>
</Modal>
<Modal
title={t('logs.settings.flattenSettings.title')}
visible={flattenSettingsModalVisible}
onOk={() => {
updateOptions(
{
flattenDepth,
},
true,
);
setFlattenSettingsModalVisible(false);
}}
onCancel={() => {
setFlattenDepth(options.flattenDepth ?? 1);
setFlattenSettingsModalVisible(false);
}}
>
<Form>
<Form.Item label={t('logs.settings.flattenSettings.title')} extra={<span style={{ whiteSpace: 'pre-line' }}>{t('logs.settings.flattenSettings.extra')}</span>}>
<InputNumber
className='w-full'
min={0}
max={10}
precision={0}
value={flattenDepth}
onChange={(val) => {
if (typeof val === 'number') {
setFlattenDepth(val);
}
}}
/>
</Form.Item>
</Form>
</Modal>
</>
);
});
3 changes: 3 additions & 0 deletions src/pages/logExplorer/components/LogsViewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ interface Props {
showPageLoadMode?: boolean;
showJSONSettings?: boolean;
showTopNSettings?: boolean;
showFlattenSettings?: boolean;
showLogMode?: boolean;
addonBefore?: React.ReactNode;
timeColumnWidth?: number;
Expand Down Expand Up @@ -166,6 +167,7 @@ export default function LogsViewer(props: Props) {
showPageLoadMode,
showJSONSettings,
showTopNSettings,
showFlattenSettings,
showLogMode = true,
addonBefore,
timeColumnWidth,
Expand Down Expand Up @@ -378,6 +380,7 @@ export default function LogsViewer(props: Props) {
showPageLoadMode={showPageLoadMode}
showJSONSettings={showJSONSettings}
showTopNSettings={showTopNSettings}
showFlattenSettings={showFlattenSettings}
organizeFields={organizeFields}
setOrganizeFields={setOrganizeFields}
/>
Expand Down
1 change: 1 addition & 0 deletions src/pages/logExplorer/components/LogsViewer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface OptionsType {
jsonExpandLevel?: number | null;
pageLoadMode?: 'pagination' | 'infiniteScroll'; // 默认 pagination
topNumber?: number; // 默认 5
flattenDepth?: number; // 默认 1,0 = 不 parse、不拍平
}

export type FieldValueType = string | number | boolean | null;
Expand Down
49 changes: 49 additions & 0 deletions src/pages/logExplorer/components/LogsViewer/utils/flatten.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import flatten from './flatten';

describe('flatten maxDepth', () => {
test('maxDepth=1 does not recurse, keeps nested object as object', () => {
const result = flatten({ a: { b: 1 } }, { maxDepth: 1 });
expect(result).toEqual({ a: { b: 1 } });
expect(result['a.b']).toBeUndefined();
});

test('maxDepth=2 expands one level and keeps deeper parts whole', () => {
const result = flatten({ a: { b: { c: 1 } } }, { maxDepth: 2 });
expect(result).toEqual({ 'a.b': { c: 1 } });
});

test('maxDepth=2 expands all sibling nested fields, not just the first', () => {
expect(flatten({ labels: { a: 1 }, meta: { b: 2 } }, { maxDepth: 2 })).toEqual({ 'labels.a': 1, 'meta.b': 2 });
});

test('sibling expansion is correct at deeper levels too', () => {
expect(flatten({ a: { b: { c: 1 }, d: { e: 2 } } }, { maxDepth: 3 })).toEqual({ 'a.b.c': 1, 'a.d.e': 2 });
});

test('no opts expands to unlimited depth (legacy behavior)', () => {
expect(flatten({ a: { b: { c: { d: 1 } } } })).toEqual({ 'a.b.c.d': 1 });
});

test('flattenDepth=1 convention maps to maxDepth=2 (labels.rule_note)', () => {
// 调用方约定:flattenDepth = N 对应 flatten 的 maxDepth = N + 1
expect(flatten({ labels: { rule_note: 'value' } }, { maxDepth: 2 })).toEqual({ 'labels.rule_note': 'value' });
});

test('explicit maxDepth wins over internal default 3', () => {
const deep = { a: { b: { c: { d: 1 } } } };
expect(flatten(deep, { maxDepth: 2 })).toEqual({ 'a.b': { c: { d: 1 } } });
expect(flatten(deep, { maxDepth: 4 })).toEqual({ 'a.b.c.d': 1 });
});

test('nested empty object is stringified', () => {
expect(flatten({ a: {} }, { maxDepth: 1 })).toEqual({ a: '{}' });
});

test('arrays are stringified', () => {
expect(flatten({ a: [1, 2] }, { maxDepth: 1 })).toEqual({ a: '[1,2]' });
});

test('flat values pass through untouched', () => {
expect(flatten({ a: 'x', b: 1, c: null }, { maxDepth: 1 })).toEqual({ a: 'x', b: 1, c: null });
});
});
17 changes: 6 additions & 11 deletions src/pages/logExplorer/components/LogsViewer/utils/flatten.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ export default function flatten(target: object, opts?: { delimiter?: any; maxDep
opts = opts || {};

const delimiter = opts.delimiter || '.';
let maxDepth = opts.maxDepth || 3;
let currentDepth = 1;
// 未显式指定 maxDepth(含传 0 的 falsy 场景)时不限深;depth 随递归入参传递,回溯天然正确
const maxDepth = opts.maxDepth || Infinity;
const output: any = {};

function step(object: any, prev: string | null) {
function step(object: any, prev: string | null, depth: number) {
Object.keys(object).forEach((key) => {
const value = object[key];
const isarray = opts?.safe && Array.isArray(value);
Expand All @@ -18,13 +18,8 @@ export default function flatten(target: object, opts?: { delimiter?: any; maxDep

const newKey = prev ? prev + delimiter + key : key;

if (!opts?.maxDepth) {
maxDepth = currentDepth + 1;
}

if (!isarray && isobject && Object.keys(value).length && currentDepth < maxDepth) {
++currentDepth;
return step(value, newKey);
if (!isarray && isobject && Object.keys(value).length && depth < maxDepth) {
return step(value, newKey, depth + 1);
}

if (isobject && Object.keys(value).length === 0) {
Expand All @@ -35,7 +30,7 @@ export default function flatten(target: object, opts?: { delimiter?: any; maxDep
});
}

step(target, null);
step(target, null, 1);

return output;
}
3 changes: 3 additions & 0 deletions src/pages/logExplorer/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const TYPE_MAP: Record<string, string> = {
boolean: 'boolean',
};

export const DEFAULT_FLATTEN_DEPTH = 1;

export const DEFAULT_OPTIONS: OptionsType = {
logMode: 'origin',
lineBreak: 'false',
Expand All @@ -37,4 +39,5 @@ export const DEFAULT_OPTIONS: OptionsType = {
jsonDisplaType: 'string',
jsonExpandLevel: 1,
topNumber: 5,
flattenDepth: DEFAULT_FLATTEN_DEPTH,
};
4 changes: 4 additions & 0 deletions src/pages/logExplorer/locale/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ const en_US = {
topNSettings: {
title: 'Top N values settings',
},
flattenSettings: {
title: 'Flatten depth',
extra: '0: no parsing or flattening, field values are displayed as raw log strings\nN (>=1): nested JSON is expanded up to N levels, deeper parts are displayed as a whole',
},
},
fieldLabelTip: 'Field statistics not enabled, unable to perform statistical analysis',
filterAnd: 'Add "{{token}}" to current query',
Expand Down
4 changes: 4 additions & 0 deletions src/pages/logExplorer/locale/zh_CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ const zh_CN = {
topNSettings: {
title: '前 N 个值设置',
},
flattenSettings: {
title: '字段拍平层级',
extra: '0: 不解析、不拍平,字段值按日志原始字符串展示\nN(≥1): 嵌套 JSON 最多展开 N 层,超出部分整体展示',
},
},
fieldLabelTip: '字段未开启统计,无法进行统计分析',
filterAnd: '添加 "{{token}}" 到本次检索',
Expand Down
33 changes: 31 additions & 2 deletions src/plugins/clickHouse/ExplorerNG/Main/Query/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useRequest, useGetState } from 'ahooks';

import { DatasourceCateEnum, IS_PLUS } from '@/utils/constant';
import { parseRange } from '@/components/TimeRangePicker';
import { NAME_SPACE as logExplorerNS } from '@/pages/logExplorer/constants';
import { NAME_SPACE as logExplorerNS, DEFAULT_FLATTEN_DEPTH } from '@/pages/logExplorer/constants';
import LogsViewer from '@/pages/logExplorer/components/LogsViewer';
import calcColWidthByData from '@/pages/logExplorer/components/LogsViewer/utils/calcColWidthByData';
import flatten from '@/pages/logExplorer/components/LogsViewer/utils/flatten';
Expand Down Expand Up @@ -103,6 +103,12 @@ export default function index(props: Props) {
};
setOptions(mergedOptions);
setOptionsToLocalstorage(NG_QUERY_LOGS_OPTIONS_CACHE_KEY, mergedOptions);
// flattenDepth 需随视图保存,同步写入表单隐藏字段
form.setFieldsValue({
query: {
flattenDepth: mergedOptions.flattenDepth,
},
});
// 只有在修改了 pageLoadMode 时才重置分页参数
if (reload) {
setServiceParams({
Expand All @@ -114,6 +120,16 @@ export default function index(props: Props) {
}
};

// 视图恢复时 form 中的 flattenDepth 即时最新,同步回 options(不触发重查,重查由 refreshFlag 自身触发)
useEffect(() => {
if (typeof queryValues?.flattenDepth === 'number' && queryValues.flattenDepth !== options.flattenDepth) {
setOptions({
...options,
flattenDepth: queryValues.flattenDepth,
});
}
}, [queryValues?.flattenDepth, options.flattenDepth]);

// 分页时的时间范围不变
const fixedRangeRef = useRef<boolean>(false);
const loadTimeRef = useRef<number | null>(null);
Expand All @@ -122,6 +138,8 @@ export default function index(props: Props) {

const service = () => {
const queryValues = form.getFieldValue('query'); // 实时获取最新的查询条件
// 优先 form(视图恢复时 form 即时最新),其次 options,最后默认值
const flattenDepth = typeof queryValues?.flattenDepth === 'number' ? queryValues.flattenDepth : options.flattenDepth ?? DEFAULT_FLATTEN_DEPTH;
Comment on lines +141 to +142
if (refreshFlag && datasourceValue && queryValues?.database && queryValues?.table && queryValues?.time_field && queryValues.range) {
const range = parseRange(queryValues.range);
let timeParams =
Expand Down Expand Up @@ -160,9 +178,19 @@ export default function index(props: Props) {
loadTimeRef.current = Date.now() - queryStart;
}
const newLogs = _.map(res.list, (item) => {
if (flattenDepth === 0) {
// 不 parse、不拍平,行数据 = 原始字段值(剔除内部高亮字段,与 depth>0 的 ___raw___ 语义一致)
const rawItem = _.omit(item, [HIGHLIGHT_FIELD]);
return {
...rawItem,
___raw___: rawItem,
___id___: _.uniqueId('log_id_'),
};
}
const normalizedItem = normalizeLogStructures(_.omit(item, [HIGHLIGHT_FIELD]));
return {
...(flatten(normalizedItem) || {}),
// 层级语义:flattenDepth = N 表示嵌套 JSON 展开 N 层,对应 flatten 的 maxDepth = N + 1
...(flatten(normalizedItem, { maxDepth: flattenDepth + 1 }) || {}),
___raw___: normalizedItem,
___id___: _.uniqueId('log_id_'),
};
Expand Down Expand Up @@ -463,6 +491,7 @@ export default function index(props: Props) {
</Space>
}
showPageLoadMode
showFlattenSettings
onOptionsChange={updateOptions}
onAddToQuery={handleValueFilter}
onRangeChange={(range) => {
Expand Down
3 changes: 1 addition & 2 deletions src/plugins/clickHouse/ExplorerNG/Main/SQL/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { useTranslation, Trans } from 'react-i18next';

import { parseRange } from '@/components/TimeRangePicker';
import { NAME_SPACE as logExplorerNS } from '@/pages/logExplorer/constants';
import flatten from '@/pages/logExplorer/components/LogsViewer/utils/flatten';
import getFieldsFromTableData from '@/pages/logExplorer/components/LogsViewer/utils/getFieldsFromTableData';
import LogsViewer from '@/pages/logExplorer/components/LogsViewer';
import calcColWidthByData from '@/pages/logExplorer/components/LogsViewer/utils/calcColWidthByData';
Expand Down Expand Up @@ -108,7 +107,7 @@ export default function Table(props: IProps) {
loadTimeRef.current = Date.now() - queryStart;
const newLogs = _.map(res.list, (item) => {
return {
...(flatten(item) || {}),
...item,
___raw___: item,
___id___: _.uniqueId('log_id_'),
};
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/clickHouse/ExplorerNG/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ export default function index(props: Props) {
<Form.Item name={['query', 'stackByField']} hidden>
<div />
</Form.Item>
<Form.Item name={['query', 'flattenDepth']} hidden>
<div />
</Form.Item>
<div className='h-full flex'>
<SideBar ns={NAME_SPACE}>
{renderCommonSettings({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function getOptionsFromLocalstorage(logsOptionsCacheKey: string, options?

if (optionsLocalStorage) {
try {
return JSON.parse(optionsLocalStorage);
return { ...defaultOptions, ...JSON.parse(optionsLocalStorage) };
} catch (e) {
return defaultOptions;
}
Expand Down
Loading