From e95591f6c895ee8ad72be45b14855a6ffb667606 Mon Sep 17 00:00:00 2001 From: jsers Date: Sat, 4 Jul 2026 21:38:16 +0800 Subject: [PATCH 01/13] feat(log-explorer): support organizeFields URL parameter across all datasources Parse organize_fields from URL search params and pass through to all datasource ExplorerNG components. When present in URL, organizeFields takes priority over localStorage values. --- .gitignore | 2 ++ .../utils/getFormValuesBySearchParams.ts | 35 +++++++++++++++++++ src/plugins/doris/ExplorerNG/index.tsx | 29 +++++++++++---- .../elasticsearch/ExplorerNG/index.tsx | 26 ++++++++++---- 4 files changed, 79 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 2caf52a70..9589496e7 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ public/bound .worktrees/ coverage/ .playwright-mcp/ +docs/ +midscene_run/ diff --git a/src/pages/logExplorer/utils/getFormValuesBySearchParams.ts b/src/pages/logExplorer/utils/getFormValuesBySearchParams.ts index 7adef5d05..af3d0b272 100644 --- a/src/pages/logExplorer/utils/getFormValuesBySearchParams.ts +++ b/src/pages/logExplorer/utils/getFormValuesBySearchParams.ts @@ -71,6 +71,12 @@ export default function getFormValuesBySearchParams(params: { [index: string]: s ? { start: !isMathString(range_start) ? moment(Number(range_start)) : range_start, end: !isMathString(range_end) ? moment(Number(range_end)) : range_end } : undefined; + let organizeFields: string[] = []; + const rawOrganizeFields = params?.organize_fields; + if (rawOrganizeFields) { + organizeFields = _.isArray(rawOrganizeFields) ? rawOrganizeFields : [rawOrganizeFields]; + } + if (data_source_name === DatasourceCateEnum.doris) { const navMode = _.get(params, 'navMode'); const syntax = _.get(params, 'syntax'); @@ -98,6 +104,7 @@ export default function getFormValuesBySearchParams(params: { [index: string]: s defaultSearchField, query, sql, + organizeFields, keys: { labelKey: _.isArray(labelKey) ? labelKey : [labelKey], valueKey: _.isArray(valueKey) ? valueKey : [valueKey], @@ -126,6 +133,7 @@ export default function getFormValuesBySearchParams(params: { [index: string]: s logstore, query, power_sql, + organizeFields, keys: { labelKey: _.isArray(labelKey) ? labelKey : [labelKey], valueKey: _.isArray(valueKey) ? valueKey : [valueKey], @@ -175,6 +183,7 @@ export default function getFormValuesBySearchParams(params: { [index: string]: s filters, sql, sqlVizType, + organizeFields, keys: { labelKey: _.isArray(labelKey) ? labelKey : [labelKey], valueKey: _.isArray(valueKey) ? valueKey : [valueKey], @@ -195,6 +204,7 @@ export default function getFormValuesBySearchParams(params: { [index: string]: s filters, sql, sqlVizType, + organizeFields, keys: { labelKey: _.isArray(labelKey) ? labelKey : [labelKey], valueKey: _.isArray(valueKey) ? valueKey : [valueKey], @@ -221,6 +231,7 @@ export default function getFormValuesBySearchParams(params: { [index: string]: s group_id, stream_id, query, + organizeFields, keys: { labelKey: _.isArray(labelKey) ? labelKey : [labelKey], valueKey: _.isArray(valueKey) ? valueKey : [valueKey], @@ -248,6 +259,7 @@ export default function getFormValuesBySearchParams(params: { [index: string]: s logNamespace, logSource, query, + organizeFields, keys: { labelKey: _.isArray(labelKey) ? labelKey : [labelKey], valueKey: _.isArray(valueKey) ? valueKey : [valueKey], @@ -277,6 +289,7 @@ export default function getFormValuesBySearchParams(params: { [index: string]: s logstore, logstream, query, + organizeFields, keys: { labelKey: _.isArray(labelKey) ? labelKey : [labelKey], valueKey: _.isArray(valueKey) ? valueKey : [valueKey], @@ -305,6 +318,7 @@ export default function getFormValuesBySearchParams(params: { [index: string]: s query_language, vizType, query, + organizeFields, keys: { labelKey: _.isArray(labelKey) ? labelKey : [labelKey], valueKey: _.isArray(valueKey) ? valueKey : [valueKey], @@ -354,6 +368,9 @@ export function getLocationSearchByFormValues(formValues: FormValue) { query.sql = formValues.query?.sql; query.labelKey = formValues.query?.keys?.labelKey; query.valueKey = formValues.query?.keys?.valueKey; + if (formValues.query?.organizeFields && _.isArray(formValues.query?.organizeFields) && formValues.query?.organizeFields.length > 0) { + query.organize_fields = formValues.query?.organizeFields; + } return queryString.stringify(query); } if (data_source_name === DatasourceCateEnum.aliyunSLS) { @@ -367,6 +384,9 @@ export function getLocationSearchByFormValues(formValues: FormValue) { query.valueKey = formValues.query?.keys?.valueKey; query.timeKey = formValues.query?.keys?.timeKey; query.timeFormat = formValues.query?.keys?.timeFormat; + if (formValues.query?.organizeFields && _.isArray(formValues.query?.organizeFields) && formValues.query?.organizeFields.length > 0) { + query.organize_fields = formValues.query?.organizeFields; + } return queryString.stringify(query); } if (data_source_name === DatasourceCateEnum.elasticsearch) { @@ -392,6 +412,9 @@ export function getLocationSearchByFormValues(formValues: FormValue) { query.sqlVizType = formValues.query?.sqlVizType; query.labelKey = formValues.query?.keys?.labelKey; query.valueKey = formValues.query?.keys?.valueKey; + if (formValues.query?.organizeFields && _.isArray(formValues.query?.organizeFields) && formValues.query?.organizeFields.length > 0) { + query.organize_fields = formValues.query?.organizeFields; + } return queryString.stringify(query); } if (data_source_name === DatasourceCateEnum.huaweiLTS) { @@ -403,6 +426,9 @@ export function getLocationSearchByFormValues(formValues: FormValue) { query.labelKey = formValues.query?.keys?.labelKey; query.valueKey = formValues.query?.keys?.valueKey; query.timeKey = formValues.query?.keys?.timeKey; + if (formValues.query?.organizeFields && _.isArray(formValues.query?.organizeFields) && formValues.query?.organizeFields.length > 0) { + query.organize_fields = formValues.query?.organizeFields; + } return queryString.stringify(query); } if (data_source_name === DatasourceCateEnum.tencentCLS) { @@ -414,6 +440,9 @@ export function getLocationSearchByFormValues(formValues: FormValue) { query.labelKey = formValues.query?.keys?.labelKey; query.valueKey = formValues.query?.keys?.valueKey; query.timeKey = formValues.query?.keys?.timeKey; + if (formValues.query?.organizeFields && _.isArray(formValues.query?.organizeFields) && formValues.query?.organizeFields.length > 0) { + query.organize_fields = formValues.query?.organizeFields; + } return queryString.stringify(query); } if (data_source_name === DatasourceCateEnum.bceBLS) { @@ -426,6 +455,9 @@ export function getLocationSearchByFormValues(formValues: FormValue) { query.labelKey = formValues.query?.keys?.labelKey; query.valueKey = formValues.query?.keys?.valueKey; query.timeKey = formValues.query?.keys?.timeKey; + if (formValues.query?.organizeFields && _.isArray(formValues.query?.organizeFields) && formValues.query?.organizeFields.length > 0) { + query.organize_fields = formValues.query?.organizeFields; + } return queryString.stringify(query); } if (data_source_name === DatasourceCateEnum.cloudwatchLogs) { @@ -438,6 +470,9 @@ export function getLocationSearchByFormValues(formValues: FormValue) { query.labelKey = formValues.query?.keys?.labelKey; query.valueKey = formValues.query?.keys?.valueKey; query.timeKey = formValues.query?.keys?.timeKey; + if (formValues.query?.organizeFields && _.isArray(formValues.query?.organizeFields) && formValues.query?.organizeFields.length > 0) { + query.organize_fields = formValues.query?.organizeFields; + } return queryString.stringify(query); } return ''; diff --git a/src/plugins/doris/ExplorerNG/index.tsx b/src/plugins/doris/ExplorerNG/index.tsx index 5f21996d0..4f13c96c2 100644 --- a/src/plugins/doris/ExplorerNG/index.tsx +++ b/src/plugins/doris/ExplorerNG/index.tsx @@ -143,13 +143,28 @@ export default function index(props: Props) { if (defaultFormValuesControl?.isInited) { const datasourceValue = form.getFieldValue('datasourceValue'); const queryValues = form.getFieldValue('query'); - setOrganizeFields( - getOrganizeFieldsFromLocalstorage({ - datasourceValue, - database: queryValues?.database, - table: queryValues?.table, - }), - ); + + // 优先使用 URL 传入的 organizeFields + const urlOrganizeFields = queryValues?.organizeFields; + if (urlOrganizeFields && _.isArray(urlOrganizeFields) && urlOrganizeFields.length > 0) { + setOrganizeFields(urlOrganizeFields); + setOrganizeFieldsToLocalstorage( + { + datasourceValue, + database: queryValues?.database, + table: queryValues?.table, + }, + urlOrganizeFields, + ); + } else { + setOrganizeFields( + getOrganizeFieldsFromLocalstorage({ + datasourceValue, + database: queryValues?.database, + table: queryValues?.table, + }), + ); + } } }, [defaultFormValuesControl?.isInited]); diff --git a/src/plugins/elasticsearch/ExplorerNG/index.tsx b/src/plugins/elasticsearch/ExplorerNG/index.tsx index b5db3577d..6f6782176 100644 --- a/src/plugins/elasticsearch/ExplorerNG/index.tsx +++ b/src/plugins/elasticsearch/ExplorerNG/index.tsx @@ -150,12 +150,26 @@ export default function index(props: Props) { if (defaultFormValuesControl?.isInited) { const datasourceValue = form.getFieldValue('datasourceValue'); const queryValues = form.getFieldValue('query'); - setOrganizeFields( - getOrganizeFieldsFromLocalstorage({ - datasourceValue, - index: queryValues?.index, - }), - ); + + // 优先使用 URL 传入的 organizeFields + const urlOrganizeFields = queryValues?.organizeFields; + if (urlOrganizeFields && _.isArray(urlOrganizeFields) && urlOrganizeFields.length > 0) { + setOrganizeFields(urlOrganizeFields); + setOrganizeFieldsToLocalstorage( + { + datasourceValue, + index: queryValues?.index, + }, + urlOrganizeFields, + ); + } else { + setOrganizeFields( + getOrganizeFieldsFromLocalstorage({ + datasourceValue, + index: queryValues?.index, + }), + ); + } } }, [defaultFormValuesControl?.isInited]); From 2914e9d9237bc50ace158391bc707e12de5529fb Mon Sep 17 00:00:00 2001 From: jsers Date: Sat, 4 Jul 2026 21:47:05 +0800 Subject: [PATCH 02/13] feat(explorer): support URL query/start/end parameters for CK datasource --- src/pages/explorer/Explorer.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/pages/explorer/Explorer.tsx b/src/pages/explorer/Explorer.tsx index 98f66cb25..fa75fcc1c 100644 --- a/src/pages/explorer/Explorer.tsx +++ b/src/pages/explorer/Explorer.tsx @@ -119,6 +119,13 @@ const Panel = (props: IProps) => { const params = new URLSearchParams(location.search); const defaultDatasourceCate = params.get('data_source_name') || getDefaultDatasourceCate(datasourceList, defaultCate, type); const defaultDatasourceValue = params.get('data_source_id') ? _.toNumber(params.get('data_source_id')) : getDefaultDatasourceValue(defaultDatasourceCate, groupedDatasourceList); + // 从 URL 参数中提取 query/range,目前仅 CK 数据源支持从 URL 填充查询语句 + const urlQuery = params.get('query'); + const rangeStart = params.get('start'); + const rangeEnd = params.get('end'); + const urlRange = rangeStart && rangeEnd ? { start: rangeStart, end: rangeEnd } : undefined; + // 仅 CK 数据源从 URL 参数初始化 query,其他数据源暂不涉及以免影响面过大 + const isCKUrlShare = defaultDatasourceCate === DatasourceCateEnum.ck && (urlQuery || urlRange); const datasourceCate = Form.useWatch('datasourceCate', form); const explorerContainerRef = useRef(null); const [promql, setPromql] = React.useState(); @@ -140,6 +147,15 @@ const Panel = (props: IProps) => { initialValues={{ datasourceCate: defaultDatasourceCate, datasourceValue: defaultDatasourceValue, + // 仅 CK 数据源支持从 URL 参数填充 query/range,避免影响其他数据源 + ...(isCKUrlShare + ? { + query: { + ...(urlQuery ? { query: urlQuery } : {}), + ...(urlRange ? { range: urlRange } : {}), + }, + } + : {}), }} >
From 4746f23c4cb782d23349e96d095983972ae2df6f Mon Sep 17 00:00:00 2001 From: jsers Date: Sat, 4 Jul 2026 22:10:33 +0800 Subject: [PATCH 03/13] feat(log-explorer): support tab drag-reorder and rename --- src/pages/logExplorer/Header.tsx | 168 +++++++++++++++++++++++++- src/pages/logExplorer/locale/en_US.ts | 3 + src/pages/logExplorer/locale/ja_JP.ts | 3 + src/pages/logExplorer/locale/ru_RU.ts | 3 + src/pages/logExplorer/locale/zh_CN.ts | 3 + src/pages/logExplorer/locale/zh_HK.ts | 3 + src/pages/logExplorer/style.less | 47 ++++++- src/pages/logExplorer/types.ts | 1 + 8 files changed, 226 insertions(+), 5 deletions(-) diff --git a/src/pages/logExplorer/Header.tsx b/src/pages/logExplorer/Header.tsx index 5c4a18b26..080e97202 100644 --- a/src/pages/logExplorer/Header.tsx +++ b/src/pages/logExplorer/Header.tsx @@ -1,6 +1,10 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Tabs } from 'antd'; +import { Input, Modal, Tabs, Tooltip } from 'antd'; +import { EditOutlined } from '@ant-design/icons'; +import { DndContext, DragEndEvent, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core'; +import { SortableContext, horizontalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import _ from 'lodash'; import { CommonStateContext } from '@/App'; @@ -20,10 +24,102 @@ interface Props { defaultDatasourceValue: number; } +function SortableTabNode(props: { id: string; children: React.ReactElement }) { + const { id, children } = props; + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); + const horizontalTransform = transform + ? { + ...transform, + y: 0, + } + : null; + + const style: React.CSSProperties = { + ...children.props.style, + transform: CSS.Translate.toString(horizontalTransform), + transition, + cursor: 'move', + opacity: isDragging ? 0.6 : children.props.style?.opacity, + }; + + return React.cloneElement(children, { + ref: setNodeRef, + style, + ...attributes, + ...listeners, + }); +} + export default function Header(props: Props) { const { t } = useTranslation(NAME_SPACE); const { logsDefaultRange } = useContext(CommonStateContext); const { items, setItems, activeKey, setActiveKey, defaultDatasourceCate, defaultDatasourceValue } = props; + const [renameModalState, setRenameModalState] = useState<{ + visible: boolean; + key?: string; + name: string; + }>({ + visible: false, + name: '', + }); + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }), + ); + + const getTabName = (item: LogExplorerTabItem, idx: number) => item.name || `Query ${idx + 1}`; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) { + return; + } + + setItems((prev) => { + const activeId = String(active.id); + const overId = String(over.id); + const oldIndex = _.findIndex(prev, { key: activeId }); + const newIndex = _.findIndex(prev, { key: overId }); + if (oldIndex === -1 || newIndex === -1) { + return prev; + } + + const newItems = [...prev]; + const [movedItem] = newItems.splice(oldIndex, 1); + newItems.splice(newIndex, 0, movedItem); + setLocalItems(newItems); + return newItems; + }); + }; + + const handleRename = () => { + const tabKey = renameModalState.key; + if (!tabKey) { + return; + } + + setItems((prev) => { + const newItems = _.map(prev, (item, idx) => { + if (item.key !== tabKey) { + return item; + } + const nextName = renameModalState.name.trim(); + return { + ...item, + name: nextName || getTabName(item, idx), + }; + }); + setLocalItems(newItems); + return newItems; + }); + setRenameModalState({ + visible: false, + name: '', + }); + }; return (
@@ -33,6 +129,19 @@ export default function Header(props: Props) { size='small' type='editable-card' activeKey={activeKey} + renderTabBar={(tabBarProps, DefaultTabBar) => ( + + + + {(node) => ( + + {node} + + )} + + + + )} onEdit={(targetKey: string, action: 'add' | 'remove') => { if (action === 'add') { const newActiveKey = getUUID(); @@ -40,7 +149,7 @@ export default function Header(props: Props) { const activeItem = _.find(prev, { key: activeKey }); const newItem = activeItem ? { - ...activeItem, + ..._.omit(activeItem, ['name']), key: newActiveKey, isInited: false, } @@ -83,13 +192,64 @@ export default function Header(props: Props) { }} > {_.map(items, (item, idx) => { + const tabName = getTabName(item, idx); return ( - + + + {tabName} + + + { + event.stopPropagation(); + setRenameModalState({ + visible: true, + key: item.key, + name: tabName, + }); + }} + > + + + + + } + key={item.key} + > <> ); })} + { + setRenameModalState({ + visible: false, + name: '', + }); + }} + > + { + setRenameModalState((prev) => ({ + ...prev, + name: event.target.value, + })); + }} + onPressEnter={handleRename} + /> +
); } diff --git a/src/pages/logExplorer/locale/en_US.ts b/src/pages/logExplorer/locale/en_US.ts index 10567c589..13eb2b9f4 100644 --- a/src/pages/logExplorer/locale/en_US.ts +++ b/src/pages/logExplorer/locale/en_US.ts @@ -1,5 +1,8 @@ const en_US = { title: 'Log Explorer', + tab: { + rename: 'Rename', + }, query: 'Query Conditions', query_is_required: 'Query conditions cannot be empty', execute: 'Query', diff --git a/src/pages/logExplorer/locale/ja_JP.ts b/src/pages/logExplorer/locale/ja_JP.ts index 90d022fbc..621059c05 100644 --- a/src/pages/logExplorer/locale/ja_JP.ts +++ b/src/pages/logExplorer/locale/ja_JP.ts @@ -1,5 +1,8 @@ const ja_JP = { title: 'ログ検索', + tab: { + rename: '名前を変更', + }, query: 'クエリ条件', query_is_required: 'クエリ条件は空にできません', execute: '検索', diff --git a/src/pages/logExplorer/locale/ru_RU.ts b/src/pages/logExplorer/locale/ru_RU.ts index 458f5dd82..dcf785c77 100644 --- a/src/pages/logExplorer/locale/ru_RU.ts +++ b/src/pages/logExplorer/locale/ru_RU.ts @@ -1,5 +1,8 @@ const ru_RU = { title: 'Поиск по логам', + tab: { + rename: 'Переименовать', + }, query: 'Условия запроса', query_is_required: 'Условия запроса не могут быть пустыми', execute: 'Запрос', diff --git a/src/pages/logExplorer/locale/zh_CN.ts b/src/pages/logExplorer/locale/zh_CN.ts index 6d5f2a214..9819f2a6f 100644 --- a/src/pages/logExplorer/locale/zh_CN.ts +++ b/src/pages/logExplorer/locale/zh_CN.ts @@ -1,5 +1,8 @@ const zh_CN = { title: '日志检索', + tab: { + rename: '重命名', + }, query: '查询条件', query_is_required: '查询条件不能为空', execute: '查询', diff --git a/src/pages/logExplorer/locale/zh_HK.ts b/src/pages/logExplorer/locale/zh_HK.ts index 11acbced6..13705fcc4 100644 --- a/src/pages/logExplorer/locale/zh_HK.ts +++ b/src/pages/logExplorer/locale/zh_HK.ts @@ -1,5 +1,8 @@ const zh_HK = { title: '日誌檢索', + tab: { + rename: '重命名', + }, query: '查詢條件', query_is_required: '查詢條件不能為空', execute: '查詢', diff --git a/src/pages/logExplorer/style.less b/src/pages/logExplorer/style.less index 5ff739da4..ed8b76731 100644 --- a/src/pages/logExplorer/style.less +++ b/src/pages/logExplorer/style.less @@ -12,6 +12,16 @@ margin-left: 0 !important; margin-right: -1px !important; background-color: var(--fc-fill-2) !important; + + &:hover { + .log-explorer-ng-tab-edit { + opacity: 1; + } + } + + .anticon-edit { + margin-right: 0 !important; + } } .ant-tabs-tab-active { @@ -19,5 +29,40 @@ border-bottom: none !important; background-color: var(--fc-fill-1) !important; } + + .ant-tabs-tab-remove { + margin-left: 0 !important; + } + + .log-explorer-ng-tab-label { + display: inline-flex; + align-items: center; + gap: 4px; + max-width: 180px; + min-width: 0; + line-height: 1; + } + + .log-explorer-ng-tab-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .log-explorer-ng-tab-edit { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + color: var(--fc-text-3); + cursor: pointer; + opacity: 0; + transition: opacity 0.2s; + + &:hover { + color: var(--fc-text-1); + } + } } -} \ No newline at end of file +} diff --git a/src/pages/logExplorer/types.ts b/src/pages/logExplorer/types.ts index eb240371f..974df5279 100644 --- a/src/pages/logExplorer/types.ts +++ b/src/pages/logExplorer/types.ts @@ -20,6 +20,7 @@ export interface Query { export interface LogExplorerTabItem { key: string; + name?: string; isInited?: boolean; formValues?: any; } From ac0ac9e82cc3856c203eac441707fef57809bf77 Mon Sep 17 00:00:00 2001 From: jsers Date: Sat, 4 Jul 2026 22:32:26 +0800 Subject: [PATCH 04/13] feat(dashboard): add legend table sort column and direction configuration --- .../dashboard/Editor/Fields/Legend/index.tsx | 29 +++++++++++++++++++ src/pages/dashboard/Editor/config.tsx | 2 ++ .../components/Legend/LegendTable.tsx | 5 +++- .../Renderer/Renderer/TimeSeriesNG/index.tsx | 10 +++++-- src/pages/dashboard/locale/en_US.ts | 5 ++++ src/pages/dashboard/locale/ja_JP.ts | 5 ++++ src/pages/dashboard/locale/ru_RU.ts | 5 ++++ src/pages/dashboard/locale/zh_CN.ts | 5 ++++ src/pages/dashboard/locale/zh_HK.ts | 5 ++++ src/pages/dashboard/types.ts | 2 ++ 10 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/pages/dashboard/Editor/Fields/Legend/index.tsx b/src/pages/dashboard/Editor/Fields/Legend/index.tsx index 71e9bedc1..57ce8ca94 100644 --- a/src/pages/dashboard/Editor/Fields/Legend/index.tsx +++ b/src/pages/dashboard/Editor/Fields/Legend/index.tsx @@ -71,6 +71,35 @@ export default function index() { + + + + +