diff --git a/src/pages/alertRules/Form/utils.test.ts b/src/pages/alertRules/Form/utils.test.ts index 26dcf04437..19a5ce34b6 100644 --- a/src/pages/alertRules/Form/utils.test.ts +++ b/src/pages/alertRules/Form/utils.test.ts @@ -8,7 +8,7 @@ jest.mock('lodash', () => { }); import _ from 'lodash'; -import { processInitialValues, parseTimeToValueAndUnit } from './utils'; +import { processInitialValues, processFormValues, parseTimeToValueAndUnit } from './utils'; // ---------- dependency mocks ---------- @@ -261,3 +261,67 @@ describe('processInitialValues', () => { expect(result.callbacks).toEqual([{ url: 'http://a.com' }, { url: 'http://b.com' }]); }); }); + +// ---------- processFormValues 回归测试 ---------- + +const baseFormValues = { + effective_time: [ + { + enable_days_of_week: ['0', '1', '2', '3', '4', '5', '6'], + enable_stime: { format: jest.fn().mockReturnValue('00:00') }, + enable_etime: { format: jest.fn().mockReturnValue('23:59') }, + }, + ], +}; + +describe('processFormValues', () => { + /** + * 核心回归测试:editMode 记录 SQL 的 Builder/Code 视图状态,需随规则配置保存, + * 再次编辑时才能还原视图;builderConfig 是 Builder 的草稿配置,SQL 才是编译产物,不落库 + */ + it('ES SQL 查询应保留 editMode 并剥离 builderConfig', () => { + const result = processFormValues({ + cate: 'elasticsearch', + rule_config: { + queries: [ + { + ref: 'A', + syntax: 'sql', + editMode: 'builder', + builderConfig: { index: 'logs', date_field: '@timestamp' }, + sql: 'SELECT count(*) FROM logs', + interval: 5, + interval_unit: 'min', + }, + ], + }, + ...baseFormValues, + }); + const query = result.rule_config.queries[0]; + expect(query.editMode).toBe('builder'); + expect(query.builderConfig).toBeUndefined(); + expect(query.sql).toBe('SELECT count(*) FROM logs'); + expect(query.interval).toBe(300); + expect(query.interval_unit).toBeUndefined(); + }); + + it('ES DSL 查询仍应清除 SQL 模式遗留的 sql/keys', () => { + const result = processFormValues({ + cate: 'elasticsearch', + rule_config: { + queries: [ + { + ref: 'A', + syntax: 'dsl', + sql: 'SELECT 1', + keys: { valueKey: 'count' }, + }, + ], + }, + ...baseFormValues, + }); + const query = result.rule_config.queries[0]; + expect(query.sql).toBeUndefined(); + expect(query.keys).toBeUndefined(); + }); +}); diff --git a/src/pages/alertRules/Form/utils.ts b/src/pages/alertRules/Form/utils.ts index 189a564b02..23379fbc4d 100644 --- a/src/pages/alertRules/Form/utils.ts +++ b/src/pages/alertRules/Form/utils.ts @@ -97,6 +97,7 @@ export function processFormValues(values) { } if (values?.rule_config?.queries) { values.rule_config.queries = _.map(values.rule_config.queries, (item) => { + const isESDSLQuery = ['elasticsearch', 'opensearch'].includes(values.cate) && item.syntax !== 'sql'; let parsedRange; if (item.range) { parsedRange = mapOptionToRelativeTimeRange(item.range); @@ -110,8 +111,15 @@ export function processFormValues(values) { if (_.isArray(item?.keys?.metricKey)) { item.keys.metricKey = _.join(item.keys.metricKey, ' '); } + // 提交前剥离表单态字段: + // - interval_unit/range 换算成引擎消费的秒数和 from/to; + // - builderConfig 是 Builder 的草稿配置,SQL 才是编译产物,不落库; + // - editMode 记录 SQL 的 Builder/Code 视图状态,需随规则配置保存,再次编辑时才能还原; + // 查询接口(数据预览、模拟触发等)不消费它,由查询侧自行构造 payload。 + // ES/OpenSearch 的 DSL 查询(syntax !== 'sql')还会清除 SQL 模式遗留的 sql/keys, + // 避免两套语义并存。 return { - ..._.omit(item, ['interval_unit', 'range']), + ..._.omit(item, ['interval_unit', 'range', 'builderConfig', ...(isESDSLQuery ? ['sql', 'keys'] : [])]), interval: item.interval_unit ? normalizeTime(item.interval, item.interval_unit) : undefined, from: parsedRange?.start, to: parsedRange?.end, diff --git a/src/pages/dashboard/Detail/utils/dashboardMigrator.test.ts b/src/pages/dashboard/Detail/utils/dashboardMigrator.test.ts index 070dc1b9c8..fab4222bdd 100644 --- a/src/pages/dashboard/Detail/utils/dashboardMigrator.test.ts +++ b/src/pages/dashboard/Detail/utils/dashboardMigrator.test.ts @@ -268,6 +268,28 @@ describe('dashboard v4 migration', () => { expect(migrated.panels[0].targets[0].query).not.toHaveProperty('syntax'); }); + it('preserves the Elasticsearch SQL query mode', () => { + const migrated = dashboardMigrator({ + version: '4.0.0', + panels: [{ + id: 'panel-es-sql', + version: '4.0.0', + type: 'tableNG', + datasourceCate: 'elasticsearch', + datasourceValue: 12, + targets: [{ + refId: 'A', + kind: 'query', + query: { syntax: 'sql', mode: 'timeSeries', sql: 'SELECT time, value FROM logs', keys: { valueKey: ['value'], timeKey: 'time' } }, + }], + custom: {}, + options: {}, + }], + }); + + expect(migrated.panels[0].targets[0].query).toMatchObject({ syntax: 'sql', sql: 'SELECT time, value FROM logs' }); + }); + it('is idempotent', () => { const once = dashboardMigrator({ version: '3.4.0', @@ -344,9 +366,9 @@ describe('dashboard v4 migration', () => { expect(migrated.options?.thresholds).toEqual({ mode: 'percentage' }); expect(migrated.targets).toMatchObject([ - { refId: 'A', resultType: 'logs', query: { filter_language: 'lucene' } }, + { refId: 'A', resultType: 'logs', query: { syntax: 'sql', mode: 'logs' } }, { refId: 'B', resultType: 'logs' }, ]); - expect(migrated.targets[0].query).not.toHaveProperty('syntax'); + expect(migrated.targets[0].query).not.toHaveProperty('filter_language'); }); }); diff --git a/src/pages/dashboard/Detail/utils/dashboardMigrator.ts b/src/pages/dashboard/Detail/utils/dashboardMigrator.ts index 852f1a0a43..aa6a15f354 100644 --- a/src/pages/dashboard/Detail/utils/dashboardMigrator.ts +++ b/src/pages/dashboard/Detail/utils/dashboardMigrator.ts @@ -142,8 +142,13 @@ const migratePanelToV4 = (panel: LegacyPanel): LegacyPanel => { } const datasourceCate = targetCopy.datasource?.cate ?? panelCopy.datasourceCate; if ((datasourceCate === 'elasticsearch' || datasourceCate === 'opensearch') && targetCopy.query) { - targetCopy.query.filter_language = targetCopy.query.filter_language ?? (targetCopy.query.syntax === 'kuery' || targetCopy.query.syntax === 'kql' ? 'kql' : 'lucene'); - delete targetCopy.query.syntax; + // SQL is a persisted query mode. Only migrate the legacy DSL syntax + // field into filter_language; otherwise an ES SQL dashboard reopens in + // DSL mode after every load. + if (targetCopy.query.syntax !== 'sql') { + targetCopy.query.filter_language = targetCopy.query.filter_language ?? (targetCopy.query.syntax === 'kuery' || targetCopy.query.syntax === 'kql' ? 'kql' : 'lucene'); + delete targetCopy.query.syntax; + } } targetCopy.resultType = inferTargetResultType(targetCopy); } diff --git a/src/pages/dashboard/Editor/QueryEditor/Elasticsearch/QueryPanel.tsx b/src/pages/dashboard/Editor/QueryEditor/Elasticsearch/QueryPanel.tsx index 3bb6e92253..dbc19d4d47 100644 --- a/src/pages/dashboard/Editor/QueryEditor/Elasticsearch/QueryPanel.tsx +++ b/src/pages/dashboard/Editor/QueryEditor/Elasticsearch/QueryPanel.tsx @@ -1,16 +1,20 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { Form, Row, Col, Input, InputNumber, Space, Select, Tooltip, Radio } from 'antd'; +import React, { useState, useEffect, useMemo, useContext } from 'react'; +import { Form, Row, Col, Input, InputNumber, Space, Select, Tooltip, Radio, Segmented, Button, Modal } from 'antd'; import { DeleteOutlined, InfoCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { FormListFieldData, FormListOperation } from 'antd/lib/form/FormList'; import _ from 'lodash'; import { useTranslation } from 'react-i18next'; import QueryExtraActions from '@/pages/dashboard/Components/QueryExtraActions'; -import { IS_PLUS } from '@/utils/constant'; +import { IS_PLUS, DatasourceCateEnum } from '@/utils/constant'; import { generateQueryNameByIndex } from '@/components/QueryName/utils'; import InputGroupWithFormItem from '@/components/InputGroupWithFormItem'; import LegendInput from '@/pages/dashboard/Components/LegendInput'; import { getESIndexPatterns } from '@/pages/log/IndexPatterns/services'; +import { getESClusterInfo } from '@/plugins/elasticsearch/services'; +import { SqlMonacoEditor, SqlMonacoPreview } from '@fc-components/monaco-editor'; +import SQLBuilderModal from '@/plugins/elasticsearch/components/SQLBuilderModal'; +import { CommonStateContext } from '@/App'; import { Panel } from '../../Components/Collapse'; import DateField from './DateField'; @@ -21,6 +25,8 @@ import Time from './Time'; import IndexPatternSelect from './IndexPatternSelect'; import type { ElasticsearchIndexPattern } from './types'; +const DEFAULT_SQL_BUILDER_INTERVAL_SECONDS = 3600; + interface Props { fields: FormListFieldData[]; field: FormListFieldData; @@ -32,9 +38,13 @@ interface Props { export default function QueryPanel({ fields, field, index, add, remove, datasourceValue }: Props) { const { t } = useTranslation('dashboard'); + const { t: tES } = useTranslation('elasticsearch'); + const { darkMode } = useContext(CommonStateContext); const [indexPatterns, setIndexPatterns] = useState([]); + const [supportsSQL, setSupportsSQL] = useState(false); const prefixName = ['targets', field.name]; const datasourceCate = Form.useWatch('datasourceCate'); + const esDatasourceCate = datasourceCate === DatasourceCateEnum.opensearch ? DatasourceCateEnum.opensearch : DatasourceCateEnum.elasticsearch; const refId = Form.useWatch([...prefixName, 'refId']) || generateQueryNameByIndex(index); const indexType = Form.useWatch([...prefixName, 'query', 'index_type']); const indexValue = Form.useWatch([...prefixName, 'query', 'index']); @@ -54,6 +64,12 @@ export default function QueryPanel({ fields, field, index, add, remove, datasour }; }, [indexType, indexValue, indexPatternId, indexPatterns, dateField]); const targetQueryValues = Form.useWatch([...prefixName, 'query', 'values']); + const savedQuerySyntax = Form.useWatch([...prefixName, 'query', 'syntax']); + const querySyntax = supportsSQL && savedQuerySyntax === 'sql' ? 'sql' : 'dsl'; + const targetQuery = Form.useWatch([...prefixName, 'query']); + const editMode = targetQuery?.editMode ?? 'code'; + const [builderModalVisible, setBuilderModalVisible] = useState(false); + const form = Form.useFormInstance(); const isRawData = _.get(targetQueryValues, [0, 'func']) === 'rawData'; const { key: fieldKey, ...restField } = field; @@ -65,6 +81,14 @@ export default function QueryPanel({ fields, field, index, add, remove, datasour } }, [datasourceValue]); + useEffect(() => { + setSupportsSQL(false); + if (!IS_PLUS || !datasourceValue) return; + getESClusterInfo({ cate: esDatasourceCate, datasource_id: datasourceValue }) + .then((info) => setSupportsSQL(info?.is_sql_supported ?? false)) + .catch(() => setSupportsSQL(false)); + }, [datasourceValue, esDatasourceCate]); + return ( ); } diff --git a/src/pages/dashboard/Renderer/datasource/contract.test.ts b/src/pages/dashboard/Renderer/datasource/contract.test.ts index f692a2e6c7..abef66fc9f 100644 --- a/src/pages/dashboard/Renderer/datasource/contract.test.ts +++ b/src/pages/dashboard/Renderer/datasource/contract.test.ts @@ -158,6 +158,54 @@ describe('dashboard unified query contract', () => { expect(JSON.stringify(request)).not.toContain('"values"'); }); + it('builds Elasticsearch SQL requests for timeseries and raw data modes', () => { + const options = { + time: { + start: moment('2026-07-24T00:00:00.000Z'), + end: moment('2026-07-24T01:00:00.000Z'), + }, + datasourceList: [], + }; + + const timeseriesRequest = buildDashboardQueryRequest({ + ...options, + targets: [{ + refId: 'A', + kind: 'query', + datasource: { cate: 'elasticsearch', id: 12 }, + query: { + syntax: 'sql', + mode: 'timeSeries', + sql: 'SELECT time, value FROM logs', + keys: { valueKey: ['value'], timeKey: 'time' }, + builderConfig: { index: 'logs', date_field: '@timestamp' }, + }, + }], + }); + expect(timeseriesRequest.queries).toMatchObject([{ + ref_id: 'A', + result_type: 'time_series', + query: { syntax: 'sql', sql: 'SELECT time, value FROM logs', keys: { valueKey: 'value', timeKey: 'time' } }, + }]); + const timeseriesQuery = timeseriesRequest.queries[0]?.kind === 'query' ? timeseriesRequest.queries[0].query : {}; + expect(timeseriesQuery).not.toHaveProperty('builderConfig'); + + const rawRequest = buildDashboardQueryRequest({ + ...options, + targets: [{ + refId: 'B', + kind: 'query', + datasource: { cate: 'elasticsearch', id: 12 }, + query: { syntax: 'sql', mode: 'raw', sql: 'SELECT * FROM logs' }, + }], + }); + expect(rawRequest.queries).toMatchObject([{ + ref_id: 'B', + result_type: 'logs', + query: { sql: 'SELECT * FROM logs' }, + }]); + }); + it('normalizes Elasticsearch and OpenSearch interval values for query-batch v2', () => { const request = buildDashboardQueryRequest({ time: { diff --git a/src/pages/dashboard/Renderer/datasource/registry.ts b/src/pages/dashboard/Renderer/datasource/registry.ts index e8fe534fc5..de8fce749e 100644 --- a/src/pages/dashboard/Renderer/datasource/registry.ts +++ b/src/pages/dashboard/Renderer/datasource/registry.ts @@ -127,10 +127,20 @@ const hasQueryText = (target: ITarget, key: 'query' | 'sql' = 'query') => { return typeof value === 'string' && value.trim().length > 0; }; +const hasESValueKey = (keys: unknown) => { + if (!keys || typeof keys !== 'object' || Array.isArray(keys) || !('valueKey' in keys)) return false; + const valueKey = keys.valueKey; + return Array.isArray(valueKey) ? valueKey.length > 0 : typeof valueKey === 'string' && valueKey.trim().length > 0; +}; + // 沿用旧版各数据源查询函数的静默短路条件:未就绪的 target 不进入 query-batch,且不触发表单校验提示。 const QUERY_READINESS: Partial boolean>> = { elasticsearch: (target) => { const query = target.query ?? {}; + if (query.syntax === 'sql') { + if (query.mode === 'raw') return Boolean(typeof query.sql === 'string' && query.sql.trim()); + return Boolean(typeof query.sql === 'string' && query.sql.trim() && hasESValueKey(query.keys)); + } return query.index_type === 'index_pattern' ? Boolean(query.index_pattern) : Boolean(query.index && query.date_field); }, opensearch: (target) => { @@ -164,9 +174,14 @@ const serializeTarget = (target: ITarget, cate: string) => { if (target.queries) { payload.queries = _.cloneDeep(target.queries); } + // Builder configuration is editor state used to reopen the builder. The + // datasource query APIs only need the generated query fields. + delete payload.builderConfig; if (_.includes(['elasticsearch', 'opensearch'], cate)) { - payload.filter_language = payload.filter_language ?? (payload.syntax === 'kuery' || payload.syntax === 'kql' ? 'kql' : 'lucene'); - delete payload.syntax; + if (payload.syntax !== 'sql') { + payload.filter_language = payload.filter_language ?? (payload.syntax === 'kuery' || payload.syntax === 'kql' ? 'kql' : 'lucene'); + delete payload.syntax; + } } return payload; }; diff --git a/src/plugins/doris/AlertRule/Query.tsx b/src/plugins/doris/AlertRule/Query.tsx index c330345bb9..7974d337ee 100644 --- a/src/plugins/doris/AlertRule/Query.tsx +++ b/src/plugins/doris/AlertRule/Query.tsx @@ -84,6 +84,9 @@ export default function Query(props: Props) { { label: 'Code', value: 'code' }, ]} onChange={(value) => { + // 从 Code 切到 Builder 时清空已生成的 sql 和 builderConfig: + // Builder 配置是纯编辑态,重新进入需从空白生成, + // 避免残留配置生成的 SQL 与当前手写 SQL 不一致。 if (value === 'builder' && editMode === 'code') { const sqlValue = _.get(queries, [field.name, 'sql']); if (sqlValue) { diff --git a/src/plugins/doris/Dashboard/QueryBuilder.tsx b/src/plugins/doris/Dashboard/QueryBuilder.tsx index 87734221eb..0d88291fa6 100644 --- a/src/plugins/doris/Dashboard/QueryBuilder.tsx +++ b/src/plugins/doris/Dashboard/QueryBuilder.tsx @@ -101,6 +101,9 @@ export default function DorisQueryBuilder({ datasourceValue }) { ]} value={editMode} onChange={(value) => { + // 切到 Builder 时清空已生成的 sql(query) 和 builderConfig: + // Builder 配置是纯编辑态,重新进入需从空白生成, + // 避免残留配置生成的 SQL 与当前 SQL 不一致。 if (value === 'builder' && editMode === 'code') { const sqlValue = _.get(targets, [field.name, 'query', 'query']); if (sqlValue) { diff --git a/src/plugins/doris/RecordingRules/Queries/index.tsx b/src/plugins/doris/RecordingRules/Queries/index.tsx index a531eb6c08..15502a6015 100644 --- a/src/plugins/doris/RecordingRules/Queries/index.tsx +++ b/src/plugins/doris/RecordingRules/Queries/index.tsx @@ -78,6 +78,9 @@ export default function index(props: IProps) { { label: 'Code', value: 'code' }, ]} onChange={(value) => { + // 从 Code 切到 Builder 时清空已生成的 sql 和 builderConfig: + // Builder 配置是纯编辑态,重新进入需从空白生成, + // 避免残留配置生成的 SQL 与当前手写 SQL 不一致。 if (value === 'builder' && editMode === 'code') { const sqlValue = _.get(query, 'sql'); if (sqlValue) { diff --git a/src/plugins/elasticsearch/AlertRule/GraphPreview.tsx b/src/plugins/elasticsearch/AlertRule/GraphPreview.tsx index 898b4b17ac..fcfad9ad1e 100644 --- a/src/plugins/elasticsearch/AlertRule/GraphPreview.tsx +++ b/src/plugins/elasticsearch/AlertRule/GraphPreview.tsx @@ -6,9 +6,11 @@ import { useTranslation } from 'react-i18next'; import { CommonStateContext } from '@/App'; import TimeRangePicker, { IRawTimeRange, parseRange } from '@/components/TimeRangePicker'; +import { DatasourceCateEnum } from '@/utils/constant'; import { normalizeTime } from '../utils'; import { getDsQuery } from './services'; +import { esSQLDsQuery } from '../services'; interface IProps { datasourceValue: number; @@ -30,6 +32,7 @@ export default function GraphPreview(props: IProps) { const { data, disabled } = props; const divRef = useRef(null); const cate = Form.useWatch('cate'); + const esDatasourceCate = cate === DatasourceCateEnum.opensearch ? DatasourceCateEnum.opensearch : DatasourceCateEnum.elasticsearch; const datasource_values = Form.useWatch('datasource_values'); const [visible, setVisible] = useState(false); const [series, setSeries] = useState([]); @@ -45,7 +48,23 @@ export default function GraphPreview(props: IProps) { const start = moment(parsedRange.start).unix(); const end = moment(parsedRange.end).unix(); - getDsQuery( + const request = data?.syntax === 'sql' + ? esSQLDsQuery({ + cate: esDatasourceCate, + datasource_id: datasourceValue, + query: [{ + ref: data?.ref, + sql: data?.sql, + from: start, + to: end, + keys: { + ...data?.keys, + valueKey: Array.isArray(data?.keys?.valueKey) ? _.join(data.keys.valueKey, ' ') : data?.keys?.valueKey, + labelKey: Array.isArray(data?.keys?.labelKey) ? _.join(data.keys.labelKey, ' ') : data?.keys?.labelKey, + }, + }], + }).then((dat) => ({ dat })) + : getDsQuery( { cate, datasource_id: datasourceValue, @@ -68,7 +87,9 @@ export default function GraphPreview(props: IProps) { }), }, false, - ) + ); + + request .then((res) => { setSeries( _.map(res.dat, (item) => { diff --git a/src/plugins/elasticsearch/AlertRule/Queries/Query.tsx b/src/plugins/elasticsearch/AlertRule/Queries/Query.tsx index 46a942f7ec..5941ac11bf 100644 --- a/src/plugins/elasticsearch/AlertRule/Queries/Query.tsx +++ b/src/plugins/elasticsearch/AlertRule/Queries/Query.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useContext, useMemo } from 'react'; import { useTranslation, Trans } from 'react-i18next'; -import { Row, Col, Form, Tooltip, AutoComplete, InputNumber, Select, Space } from 'antd'; +import { Row, Col, Form, Tooltip, AutoComplete, InputNumber, Select, Space, Segmented, Button, Modal } from 'antd'; import { QuestionCircleOutlined } from '@ant-design/icons'; import _ from 'lodash'; @@ -12,8 +12,12 @@ import { useIsAuthorized } from '@/components/AuthorizationWrapper'; import IndexPatternSettingsBtn from '@/pages/explorer/Elasticsearch/components/IndexPatternSettingsBtn'; import { getESIndexPatterns } from '@/pages/log/IndexPatterns/services'; import CardContainer, { CardContainerHeader } from '@/pages/alertRules/FormNG/components/CardContainer'; +import UnitPicker from '@/pages/dashboard/Components/UnitPicker'; import LuceneInput from '@/plugins/elasticsearch/components/LuceneInput'; +import { SqlMonacoEditor, SqlMonacoPreview } from '@fc-components/monaco-editor'; +import SQLBuilderModal from '@/plugins/elasticsearch/components/SQLBuilderModal'; +import { normalizeTime } from '@/plugins/elasticsearch/utils'; import GraphPreview from '../GraphPreview'; import Value from './Value'; @@ -28,23 +32,31 @@ interface Props { datasourceValue: number; indexOptions: any[]; disabled?: boolean; + supportsSQL?: boolean; onClose?: () => void; } export default function Query(props: Props) { const { t, i18n } = useTranslation('alertRules'); + const { t: tES } = useTranslation('elasticsearch'); const { darkMode } = useContext(CommonStateContext); const { field } = props; - const { hideIndexPattern, datasourceValue, indexOptions, disabled, onClose } = props; + const { hideIndexPattern, datasourceValue, indexOptions, disabled, onClose, supportsSQL } = props; const indexPatternsAuthorized = useIsAuthorized(['/log/index-patterns']); const [indexSearch, setIndexSearch] = useState(''); const [indexPatternsRefreshFlag, setIndexPatternsRefreshFlag] = useState(_.uniqueId('indexPatternsRefreshFlag_')); const [indexPatterns, setIndexPatterns] = useState([]); const names = ['rule_config', 'queries']; const queries = Form.useWatch(names); + const savedSyntax = Form.useWatch([...names, field.name, 'syntax']); + const syntax = supportsSQL && savedSyntax === 'sql' ? 'sql' : 'dsl'; const indexType = Form.useWatch([...names, field.name, 'index_type']); const indexValue = Form.useWatch([...names, field.name, 'index']); const indexPatternId = Form.useWatch([...names, field.name, 'index_pattern']); + const query = queries?.[field.name]; + const editMode = query?.editMode ?? 'code'; + const [builderModalVisible, setBuilderModalVisible] = useState(false); + const form = Form.useFormInstance(); const curIndexValue = useMemo(() => { if (indexType === 'index') { return indexValue; @@ -61,15 +73,62 @@ export default function Query(props: Props) { }, [datasourceValue, indexPatternsRefreshFlag]); return ( - - - - - - - - - + + + + + + + + + {supportsSQL && ( + + + + + + )} + {syntax === 'sql' && ( + + { + // 从 Code 切到 Builder 且已有 SQL 时需确认丢弃: + // 清空 sql 和 builderConfig,强制用 Builder 重新生成, + // 避免残留配置生成的 SQL 与当前手写 SQL 不一致。 + if (value === 'builder' && editMode === 'code' && query?.sql) { + Modal.confirm({ + title: tES('builder.switch_to_builder_confirm_title'), + content: tES('builder.switch_to_builder_confirm_content'), + onOk: () => form.setFields([ + { name: [...names, field.name, 'editMode'], value: 'builder' }, + { name: [...names, field.name, 'sql'], value: undefined }, + { name: [...names, field.name, 'builderConfig'], value: undefined }, + ]), + }); + return; + } + form.setFields([{ name: [...names, field.name, 'editMode'], value }]); + }} + /> + + )} + {syntax === 'sql' && ( + + }> + + + + )} + {syntax !== 'sql' && - + } - + {syntax === 'sql' ? ( + <> + + {editMode === 'builder' && query?.sql && } + {editMode === 'builder' && } + {editMode === 'code' && ( + + + + )} + {editMode === 'builder' && ( + setBuilderModalVisible(false)} + onConfirm={(builderConfig, result) => { + form.setFields([ + { name: [...names, field.name, 'sql'], value: result.sql, errors: [] }, + { name: [...names, field.name, 'builderConfig'], value: builderConfig, errors: [] }, + { name: [...names, field.name, 'keys', 'valueKey'], value: result.value_key }, + { name: [...names, field.name, 'keys', 'labelKey'], value: result.label_key }, + ]); + setBuilderModalVisible(false); + }} + /> + )} + + + {tES('query.advancedSettings.valueKey')}} + rules={[{ required: true, message: tES('query.advancedSettings.valueKey_required') }]} + > + + + + + + + + + + + + ) : {indexType === 'index' && ( @@ -207,12 +322,12 @@ export default function Query(props: Props) { functions={['count', 'avg', 'sum', 'max', 'min', 'p90', 'p95', 'p99']} /> - -
+ } + {syntax !== 'sql' &&
-
- - +
} + {syntax !== 'sql' && } + {syntax !== 'sql' && }
); } diff --git a/src/plugins/elasticsearch/AlertRule/Queries/index.tsx b/src/plugins/elasticsearch/AlertRule/Queries/index.tsx index f0776d8587..cdd7b5c8d3 100644 --- a/src/plugins/elasticsearch/AlertRule/Queries/index.tsx +++ b/src/plugins/elasticsearch/AlertRule/Queries/index.tsx @@ -6,6 +6,8 @@ import { useTranslation } from 'react-i18next'; import { getIndices } from '@/pages/explorer/Elasticsearch/services'; import { generateQueryName } from '@/components/QueryName'; import FormItemLabel from '@/pages/alertRules/FormNG/components/FormItemLabel'; +import { IS_PLUS, DatasourceCateEnum } from '@/utils/constant'; +import { getESClusterInfo } from '@/plugins/elasticsearch/services'; import Query from './Query'; interface IProps { @@ -19,8 +21,11 @@ export default function index(props: IProps) { const { t } = useTranslation('alertRules'); const { hideIndexPattern, datasourceValue, form, disabled } = props; const [indexOptions, setIndexOptions] = useState([]); + const [supportsSQL, setSupportsSQL] = useState(false); const names = ['rule_config', 'queries']; const queries = Form.useWatch(names); + const datasourceCate = Form.useWatch('cate', form); + const esDatasourceCate = datasourceCate === DatasourceCateEnum.opensearch ? DatasourceCateEnum.opensearch : DatasourceCateEnum.elasticsearch; useEffect(() => { if (datasourceValue !== undefined) { @@ -36,6 +41,14 @@ export default function index(props: IProps) { } }, [datasourceValue]); + useEffect(() => { + setSupportsSQL(false); + if (!IS_PLUS || !datasourceValue) return; + getESClusterInfo({ cate: esDatasourceCate, datasource_id: datasourceValue }) + .then((info) => setSupportsSQL(info?.is_sql_supported ?? false)) + .catch(() => setSupportsSQL(false)); + }, [datasourceValue, esDatasourceCate]); + return ( 1 ? () => remove(field.name) : undefined} /> diff --git a/src/plugins/elasticsearch/RecordingRules/Queries/index.tsx b/src/plugins/elasticsearch/RecordingRules/Queries/index.tsx new file mode 100644 index 0000000000..cd7293ba95 --- /dev/null +++ b/src/plugins/elasticsearch/RecordingRules/Queries/index.tsx @@ -0,0 +1,51 @@ +import React, { useEffect, useState } from 'react'; +import { Col, Form, Row, Select } from 'antd'; +import { SqlMonacoEditor } from '@fc-components/monaco-editor'; +import { useTranslation } from 'react-i18next'; + +import { DatasourceCateEnum, IS_PLUS } from '@/utils/constant'; +import { getESClusterInfo } from '@/plugins/elasticsearch/services'; + +interface Props { + datasourceValue: number; + field: any; + prefixPath: (string | number)[]; +} + +// 记录规则由 plus 的通用表单按数据源类别加载。本组件只负责 ES SQL 的配置结构, +// 执行仍走 n9e-plus recordx 的通用 QueryData 链路。 +export default function ElasticsearchRecordingRuleQuery({ datasourceValue, field, prefixPath }: Props) { + const { t: tES } = useTranslation('elasticsearch'); + const [supportsSQL, setSupportsSQL] = useState(false); + const path = [field.name, 'config']; + + useEffect(() => { + setSupportsSQL(false); + if (!IS_PLUS || !datasourceValue) return; + getESClusterInfo({ cate: DatasourceCateEnum.elasticsearch, datasource_id: datasourceValue }) + .then((info) => setSupportsSQL(info?.is_sql_supported ?? false)) + .catch(() => setSupportsSQL(false)); + }, [datasourceValue]); + + if (!supportsSQL) return null; + + return ( + <> + + + + + + + + + + + + ); +} diff --git a/src/plugins/elasticsearch/components/SQLBuilderModal.tsx b/src/plugins/elasticsearch/components/SQLBuilderModal.tsx new file mode 100644 index 0000000000..6ef9d63b4b --- /dev/null +++ b/src/plugins/elasticsearch/components/SQLBuilderModal.tsx @@ -0,0 +1,182 @@ +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { AutoComplete, Col, Form, message, Modal, Row, Select } from 'antd'; +import moment from 'moment'; +import { useTranslation } from 'react-i18next'; + +import { CommonStateContext } from '@/App'; +import { getIndices, getFullFields } from '@/pages/explorer/Elasticsearch/services'; +import { getESIndexPatterns } from '@/pages/log/IndexPatterns/services'; +import { DatasourceCateEnum } from '@/utils/constant'; + +// @ts-ignore QueryBuilder is supplied by the commercial package. +import QueryBuilder from 'plus:/datasource/elasticsearch/ExplorerNG/components/QueryBuilder'; +// @ts-ignore QueryBuilder context is supplied by the commercial package. +import QueryBuilderCommonStateContext from 'plus:/datasource/elasticsearch/ExplorerNG/components/QueryBuilder/commonStateContext'; +// @ts-ignore esQueryBuilder is supplied by the commercial package. +import { esQueryBuilder } from 'plus:/datasource/elasticsearch/ExplorerNG/services'; + +export interface ESBuilderConfig { + index?: string; + date_field?: string; + filters?: any[]; + aggregates?: any[]; + mode?: 'table' | 'timeseries'; + group_by?: string[]; + order_by?: any[]; + limit?: number; +} + +interface Props { + visible: boolean; + datasourceValue: number; + interval: number; + builderConfig?: ESBuilderConfig; + onCancel: () => void; + onConfirm: (builderConfig: ESBuilderConfig, result: { sql: string; value_key: string[]; label_key: string[]; time_key?: string; mode: string }) => void; +} + +export default function SQLBuilderModal({ visible, datasourceValue, interval, builderConfig, onCancel, onConfirm }: Props) { + const { t } = useTranslation('elasticsearch'); + const [form] = Form.useForm(); + const [confirmLoading, setConfirmLoading] = useState(false); + const { esIndexMode } = useContext(CommonStateContext); + const [indices, setIndices] = useState([]); + const [indexPatterns, setIndexPatterns] = useState([]); + const [fields, setFields] = useState([]); + const index = Form.useWatch('index', form); + const dateField = Form.useWatch('date_field', form); + const searchMode = Form.useWatch('search_mode', form) || (esIndexMode !== 'all' ? esIndexMode : 'indices'); + const range = useMemo(() => ({ start: `now-${Math.max(interval || 60, 1)}s`, end: 'now' }), [interval]); + + useEffect(() => { + if (!visible) return; + form.resetFields(); + form.setFieldsValue({ + index: builderConfig?.index, + date_field: builderConfig?.date_field, + search_mode: esIndexMode !== 'all' ? esIndexMode : 'indices', + query: { range }, + }); + }, [visible, builderConfig, range, form]); + + useEffect(() => { + if (!visible || !datasourceValue) return; + getIndices(datasourceValue).then(setIndices).catch(() => setIndices([])); + }, [visible, datasourceValue]); + + useEffect(() => { + if (!visible || !datasourceValue) return; + getESIndexPatterns(datasourceValue).then(setIndexPatterns).catch(() => setIndexPatterns([])); + }, [visible, datasourceValue]); + + useEffect(() => { + if (!visible || !datasourceValue || !index) { + setFields([]); + return; + } + getFullFields(datasourceValue, index) + .then((res) => setFields(Array.isArray(res.allFields) ? res.allFields : [])) + .catch(() => setFields([])); + }, [visible, datasourceValue, index]); + + useEffect(() => { + if (searchMode !== 'index-patterns' || !index) return; + const pattern = indexPatterns.find((item) => item.name === index); + if (pattern?.time_field && pattern.time_field !== dateField) { + form.setFieldsValue({ date_field: pattern.time_field }); + } + }, [searchMode, index, indexPatterns, dateField, form]); + + const build = (values: ESBuilderConfig) => { + if (!index || !dateField) return Promise.resolve(); + const to = moment().unix(); + const from = to - Math.max(interval || 60, 1); + return esQueryBuilder({ + cate: DatasourceCateEnum.elasticsearch, + datasource_id: datasourceValue, + query: [{ ...values, index, time_field: dateField, from, to }], + }).then((result) => { + onConfirm({ ...values, index, date_field: dateField }, result); + }); + }; + + const handleOk = () => { + form.validateFields() + .then((values) => { + setConfirmLoading(true); + return build(values); + }) + .catch((error) => { + // Validation errors are rendered by Form.Item; only notify for builder + // execution failures to avoid an unhandled promise rejection. + if (!(error && typeof error === 'object' && 'errorFields' in error)) { + message.error(t('builder.preview_sql_failed')); + } + }) + .finally(() => setConfirmLoading(false)); + }; + + return ( + + {} }}> +
+ + + + + + + ) : ( + + + } + + + +
+
+ ); +} diff --git a/src/plugins/elasticsearch/index.tsx b/src/plugins/elasticsearch/index.tsx index d8b2f4c088..bd4d49ba45 100644 --- a/src/plugins/elasticsearch/index.tsx +++ b/src/plugins/elasticsearch/index.tsx @@ -3,6 +3,7 @@ import Event from './Event'; import EventLogs from './Event/Logs'; import EventPreview from './Event/Preview'; import VariableQuerybuilder from './Dashboard/VariableQuerybuilder'; +import RecordingRuleQueryBuilder from './RecordingRules/Queries'; import './locale'; -export { AlertRule, Event, EventLogs, EventPreview, VariableQuerybuilder }; +export { AlertRule, Event, EventLogs, EventPreview, VariableQuerybuilder, RecordingRuleQueryBuilder }; diff --git a/src/plugins/elasticsearch/locale/en_US.ts b/src/plugins/elasticsearch/locale/en_US.ts index a43e93d4cd..491b0eb430 100644 --- a/src/plugins/elasticsearch/locale/en_US.ts +++ b/src/plugins/elasticsearch/locale/en_US.ts @@ -1,5 +1,7 @@ const en_US = { query: { + range: 'Query range', + sql_required: 'SQL is required', mode: 'Mode', mode_indices: 'Indices', mode_index_patterns: 'Index patterns', @@ -37,6 +39,12 @@ const en_US = { table: 'Table', timeseries: 'Time series', }, + dashboard: { + mode: { + timeSeries: 'Time series data', + table: 'Non-time-series data', + }, + }, add_to: { btn: 'Add To', recording_rule: 'Recording rule', @@ -44,6 +52,11 @@ const en_US = { }, }, builder: { + title: 'Builder', + code: 'Code', + open_builder: 'Open builder', + switch_to_builder_confirm_title: 'Switch to Builder', + switch_to_builder_confirm_content: 'Switching will clear the current SQL and builder configuration.', to_pinned_btn: 'Pin', to_unpinned_btn: 'Unpin', filters: { diff --git a/src/plugins/elasticsearch/locale/es_ES.ts b/src/plugins/elasticsearch/locale/es_ES.ts index 42adf1d5b9..05095394ca 100644 --- a/src/plugins/elasticsearch/locale/es_ES.ts +++ b/src/plugins/elasticsearch/locale/es_ES.ts @@ -1,5 +1,7 @@ const es_ES = { "query": { + "range": "Query range", + "sql_required": "SQL is required", "mode": "Forma de búsqueda", "mode_indices": "Indices", "mode_index_patterns": "Index patterns", @@ -37,6 +39,12 @@ const es_ES = { "table": "Tabla", "timeseries": "Gráfico de series temporales" }, + "dashboard": { + "mode": { + "timeSeries": "Datos de series temporales", + "table": "Datos no temporales" + } + }, "add_to": { "btn": "Añadir a", "recording_rule": "Reglas de registro", @@ -44,6 +52,11 @@ const es_ES = { } }, "builder": { + "title": "Builder", + "code": "Code", + "open_builder": "Open builder", + "switch_to_builder_confirm_title": "Switch to Builder", + "switch_to_builder_confirm_content": "Switching will clear the current SQL and builder configuration.", "to_pinned_btn": "Fijo", "to_unpinned_btn": "Dejar de fijar", "filters": { diff --git a/src/plugins/elasticsearch/locale/fr_FR.ts b/src/plugins/elasticsearch/locale/fr_FR.ts index 43d30cc20e..8812dbd3ae 100644 --- a/src/plugins/elasticsearch/locale/fr_FR.ts +++ b/src/plugins/elasticsearch/locale/fr_FR.ts @@ -1,5 +1,7 @@ const fr_FR = { "query": { + "range": "Query range", + "sql_required": "SQL is required", "mode": "Mode de recherche", "mode_indices": "Indices", "mode_index_patterns": "Index patterns", @@ -37,6 +39,12 @@ const fr_FR = { "table": "Tableau", "timeseries": "Courbe temporelle" }, + "dashboard": { + "mode": { + "timeSeries": "Données de séries temporelles", + "table": "Données non temporelles" + } + }, "add_to": { "btn": "Ajouter à", "recording_rule": "Règles d'enregistrement", @@ -44,6 +52,11 @@ const fr_FR = { } }, "builder": { + "title": "Builder", + "code": "Code", + "open_builder": "Open builder", + "switch_to_builder_confirm_title": "Switch to Builder", + "switch_to_builder_confirm_content": "Switching will clear the current SQL and builder configuration.", "to_pinned_btn": "Fixe", "to_unpinned_btn": "Détacher", "filters": { diff --git a/src/plugins/elasticsearch/locale/id_ID.ts b/src/plugins/elasticsearch/locale/id_ID.ts index 2485369a2d..da1d1d7236 100644 --- a/src/plugins/elasticsearch/locale/id_ID.ts +++ b/src/plugins/elasticsearch/locale/id_ID.ts @@ -1,5 +1,7 @@ const id_ID = { "query": { + "range": "Query range", + "sql_required": "SQL is required", "mode": "Metode pencarian", "mode_indices": "Indices", "mode_index_patterns": "Index patterns", @@ -37,6 +39,12 @@ const id_ID = { "table": "Tabel", "timeseries": "Grafik deret waktu" }, + "dashboard": { + "mode": { + "timeSeries": "Data deret waktu", + "table": "Data non-deret waktu" + } + }, "add_to": { "btn": "Tambahkan ke", "recording_rule": "Recording rules", @@ -44,6 +52,11 @@ const id_ID = { } }, "builder": { + "title": "Builder", + "code": "Code", + "open_builder": "Open builder", + "switch_to_builder_confirm_title": "Switch to Builder", + "switch_to_builder_confirm_content": "Switching will clear the current SQL and builder configuration.", "to_pinned_btn": "Tetap", "to_unpinned_btn": "Lepas sematan", "filters": { diff --git a/src/plugins/elasticsearch/locale/ja_JP.ts b/src/plugins/elasticsearch/locale/ja_JP.ts index 85f203b244..92b0c4b8d4 100644 --- a/src/plugins/elasticsearch/locale/ja_JP.ts +++ b/src/plugins/elasticsearch/locale/ja_JP.ts @@ -1,5 +1,7 @@ const ja_JP = { query: { + range: 'Query range', + sql_required: 'SQL is required', mode: '検索モード', mode_indices: 'Indices', mode_index_patterns: 'Index patterns', @@ -37,6 +39,12 @@ const ja_JP = { table: '表', timeseries: '時系列', }, + dashboard: { + mode: { + timeSeries: '時系列データ', + table: '非時系列データ', + }, + }, add_to: { btn: '追加先...', recording_rule: '記録ルールに追加', @@ -44,6 +52,11 @@ const ja_JP = { }, }, builder: { + title: 'Builder', + code: 'Code', + open_builder: 'Open builder', + switch_to_builder_confirm_title: 'Switch to Builder', + switch_to_builder_confirm_content: 'Switching will clear the current SQL and builder configuration.', to_pinned_btn: '固定', to_unpinned_btn: '固定解除', filters: { diff --git a/src/plugins/elasticsearch/locale/ko_KR.ts b/src/plugins/elasticsearch/locale/ko_KR.ts index 664445dec5..cab4c2494b 100644 --- a/src/plugins/elasticsearch/locale/ko_KR.ts +++ b/src/plugins/elasticsearch/locale/ko_KR.ts @@ -1,5 +1,7 @@ const ko_KR = { "query": { + "range": "Query range", + "sql_required": "SQL is required", "mode": "검색 방식", "mode_indices": "Indices", "mode_index_patterns": "Index patterns", @@ -37,6 +39,12 @@ const ko_KR = { "table": "표", "timeseries": "시계열 그래프" }, + "dashboard": { + "mode": { + "timeSeries": "시계열 데이터", + "table": "비시계열 데이터" + } + }, "add_to": { "btn": "추가할 곳", "recording_rule": "레코딩 규칙", @@ -44,6 +52,11 @@ const ko_KR = { } }, "builder": { + "title": "Builder", + "code": "Code", + "open_builder": "Open builder", + "switch_to_builder_confirm_title": "Switch to Builder", + "switch_to_builder_confirm_content": "Switching will clear the current SQL and builder configuration.", "to_pinned_btn": "고정", "to_unpinned_btn": "고정 해제", "filters": { diff --git a/src/plugins/elasticsearch/locale/pt_BR.ts b/src/plugins/elasticsearch/locale/pt_BR.ts index 5b7fc04f51..b6b1015734 100644 --- a/src/plugins/elasticsearch/locale/pt_BR.ts +++ b/src/plugins/elasticsearch/locale/pt_BR.ts @@ -1,5 +1,7 @@ const pt_BR = { "query": { + "range": "Query range", + "sql_required": "SQL is required", "mode": "Forma de busca", "mode_indices": "Indices", "mode_index_patterns": "Index patterns", @@ -37,6 +39,12 @@ const pt_BR = { "table": "Tabela", "timeseries": "Gráfico de série temporal" }, + "dashboard": { + "mode": { + "timeSeries": "Dados de séries temporais", + "table": "Dados não temporais" + } + }, "add_to": { "btn": "Adicionar a", "recording_rule": "Regras de registro", @@ -44,6 +52,11 @@ const pt_BR = { } }, "builder": { + "title": "Builder", + "code": "Code", + "open_builder": "Open builder", + "switch_to_builder_confirm_title": "Switch to Builder", + "switch_to_builder_confirm_content": "Switching will clear the current SQL and builder configuration.", "to_pinned_btn": "Fixo", "to_unpinned_btn": "Desafixar", "filters": { diff --git a/src/plugins/elasticsearch/locale/ru_RU.ts b/src/plugins/elasticsearch/locale/ru_RU.ts index 290faa245f..a5fd160fc5 100644 --- a/src/plugins/elasticsearch/locale/ru_RU.ts +++ b/src/plugins/elasticsearch/locale/ru_RU.ts @@ -1,5 +1,7 @@ const ru_RU = { query: { + range: 'Query range', + sql_required: 'SQL is required', mode: 'Режим поиска', mode_indices: 'Indices', mode_index_patterns: 'Index patterns', @@ -37,6 +39,12 @@ const ru_RU = { table: 'Таблица', timeseries: 'Временные ряды', }, + dashboard: { + mode: { + timeSeries: 'Данные временных рядов', + table: 'Данные без временных рядов', + }, + }, add_to: { btn: 'Добавить в', recording_rule: 'Правило записи', @@ -44,6 +52,11 @@ const ru_RU = { }, }, builder: { + title: 'Builder', + code: 'Code', + open_builder: 'Open builder', + switch_to_builder_confirm_title: 'Switch to Builder', + switch_to_builder_confirm_content: 'Switching will clear the current SQL and builder configuration.', to_pinned_btn: 'Закрепить', to_unpinned_btn: 'Открепить', filters: { diff --git a/src/plugins/elasticsearch/locale/zh_CN.ts b/src/plugins/elasticsearch/locale/zh_CN.ts index e5a2ba1250..a5229ddb58 100644 --- a/src/plugins/elasticsearch/locale/zh_CN.ts +++ b/src/plugins/elasticsearch/locale/zh_CN.ts @@ -1,5 +1,7 @@ const zh_CN = { query: { + range: '查询区间', + sql_required: 'SQL 不能为空', mode: '检索方式', mode_indices: 'Indices', mode_index_patterns: 'Index patterns', @@ -37,6 +39,12 @@ const zh_CN = { table: '表格', timeseries: '时序图', }, + dashboard: { + mode: { + timeSeries: '时序数据', + table: '非时序数据', + }, + }, add_to: { btn: '添加到', recording_rule: '录制规则', @@ -44,6 +52,11 @@ const zh_CN = { }, }, builder: { + title: '构建器', + code: '代码', + open_builder: '打开构建器', + switch_to_builder_confirm_title: '切换至构建器', + switch_to_builder_confirm_content: '切换后将清空当前 SQL 及构建器配置。', to_pinned_btn: '固定', to_unpinned_btn: '取消固定', filters: { diff --git a/src/plugins/elasticsearch/locale/zh_HK.ts b/src/plugins/elasticsearch/locale/zh_HK.ts index 970239e185..accf507eaf 100644 --- a/src/plugins/elasticsearch/locale/zh_HK.ts +++ b/src/plugins/elasticsearch/locale/zh_HK.ts @@ -1,5 +1,7 @@ const zh_HK = { query: { + range: '查詢區間', + sql_required: 'SQL 不能為空', mode: '檢索方式', mode_indices: 'Indices', mode_index_patterns: 'Index patterns', @@ -37,6 +39,12 @@ const zh_HK = { table: '表格', timeseries: '時序圖', }, + dashboard: { + mode: { + timeSeries: '時序數據', + table: '非時序數據', + }, + }, add_to: { btn: '添加至...', recording_rule: '添加至記錄規則', @@ -44,6 +52,11 @@ const zh_HK = { }, }, builder: { + title: '構建器', + code: '代碼', + open_builder: '打開構建器', + switch_to_builder_confirm_title: '切換至構建器', + switch_to_builder_confirm_content: '切換後將清空目前 SQL 及構建器設定。', to_pinned_btn: '固定', to_unpinned_btn: '取消固定', filters: { diff --git a/src/plugins/elasticsearch/services.ts b/src/plugins/elasticsearch/services.ts index cd8b564326..2fd881c49e 100644 --- a/src/plugins/elasticsearch/services.ts +++ b/src/plugins/elasticsearch/services.ts @@ -369,6 +369,7 @@ export function esLogsQuery(data: { cate: DatasourceCateEnum; datasource_id: number; query: { + ref?: string; sql: string; index: string; start: number; // unix timestamp @@ -388,6 +389,7 @@ export function esSQLDsQuery(data: { cate: DatasourceCateEnum; datasource_id: number; query: { + ref?: string; sql: string; from: number; // unix timestamp to: number; // unix timestamp