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
1 change: 1 addition & 0 deletions packages/business/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"axios": "catalog:",
"cron-parser": "catalog:",
"dayjs": "catalog:",
"json5": "^2.2.3",
"juice": "catalog:",
"lodash": "catalog:",
"monaco-editor": "catalog:",
Expand Down
11 changes: 10 additions & 1 deletion packages/business/src/views/data-server/Drawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ const save = async (type?: boolean) => {
}
}

const normalizedCustomWhere =
apiType === 'customerQuery' && fullCustomQuery
? mqlEditor.value?.normalize(customWhere) ?? customWhere
: customWhere

const params = form.value?.params
?.filter((t: any) => t.name)
.map((t: any) => {
Expand Down Expand Up @@ -537,12 +542,16 @@ const save = async (type?: boolean) => {
fields,
path,
fullCustomQuery,
customWhere,
customWhere: normalizedCustomWhere,
},
],
pathSetting: pathSettingList,
}

if (apiType === 'customerQuery' && fullCustomQuery) {
form.value.customWhere = normalizedCustomWhere
}

if (!type && connectionId && tableName) {
formData.fields = allFields.value
// const fieldList = await getAllFields()
Expand Down
167 changes: 129 additions & 38 deletions packages/business/src/views/data-server/MqlEditor.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script setup lang="ts">
import { useI18n } from '@tap/i18n'
import JSON5 from 'json5'
import * as monaco from 'monaco-editor'
import { onBeforeUnmount, ref } from 'vue'
import { ref } from 'vue'
import MonacoEditor from './MonacoEditor.vue'

const { t } = useI18n()
const JSON5_LANGUAGE_ID = 'json5'

const props = defineProps({
height: {
Expand Down Expand Up @@ -35,7 +37,73 @@ const editorValue = defineModel('modelValue', {
type: String,
default: '',
})
const monacoEditorRef = ref(null)
const monacoEditorRef = ref<any>(null)

let json5LanguageRegistered = false

const registerJson5Language = () => {
if (json5LanguageRegistered) return

monaco.languages.register({
id: JSON5_LANGUAGE_ID,
extensions: ['.json5'],
aliases: ['JSON5', 'json5'],
})

monaco.languages.setLanguageConfiguration(JSON5_LANGUAGE_ID, {
comments: {
lineComment: '//',
blockComment: ['/*', '*/'],
},
brackets: [
['{', '}'],
['[', ']'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"', notIn: ['string', 'comment'] },
{ open: "'", close: "'", notIn: ['string', 'comment'] },
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
})

monaco.languages.setMonarchTokensProvider(JSON5_LANGUAGE_ID, {
defaultToken: '',
tokenPostfix: '.json5',
tokenizer: {
root: [
[/[ \t\r\n]+/, 'white'],
[/\/\/.*$/, 'comment'],
[/\/\*/, { token: 'comment', next: '@comment' }],
[/\b(?:true|false|null|Infinity|NaN)\b/, 'keyword'],
[/[+\-]?(?:0x[0-9a-f]+|(?:\d+\.\d*|\.\d+|\d+)(?:e[+\-]?\d+)?)/i, 'number'],
[/'(?:[^'\\]|\\.)*'/, 'string'],
[/"(?:[^"\\]|\\.)*"/, 'string'],
[/[A-Z_$][\w$]*/i, 'identifier'],
[/[{}[\]]/, '@brackets'],
[/[:,]/, 'delimiter'],
[/./, 'delimiter.invalid'],
],
comment: [
[/[^/*]+/, 'comment'],
[/\*\//, { token: 'comment', next: '@pop' }],
[/[/*]/, 'comment'],
],
},
})

json5LanguageRegistered = true
}

registerJson5Language()

const mongoOperators = [
// Comparison operators
Expand Down Expand Up @@ -141,16 +209,17 @@ const mongoOperators = [
},
]

const registerMongoCompletion = () => {
return monaco.languages.registerCompletionItemProvider('json', {
const registerMongoCompletion = (languageId: string) => {
return monaco.languages.registerCompletionItemProvider(languageId, {
triggerCharacters: ['$', '"', "'", '{'],
provideCompletionItems: (model, position) => {
provideCompletionItems: (model: any, position: any) => {
const word = model.getWordUntilPosition(position)
const lineContent = model.getLineContent(position.lineNumber)
const textBeforeCursor = lineContent.slice(
0,
Math.max(0, position.column - 1),
)
const isJson5 = model.getLanguageId() === JSON5_LANGUAGE_ID

const range = {
startLineNumber: position.lineNumber,
Expand All @@ -159,7 +228,7 @@ const registerMongoCompletion = () => {
endColumn: word.endColumn,
}

const suggestions = []
const suggestions: any[] = []

if (lineContent?.trim() === '{}') return { suggestions }

Expand Down Expand Up @@ -200,14 +269,14 @@ const registerMongoCompletion = () => {
label: op.label,
kind: op.kind,
detail: op.detail,
insertText: isInQuotes ? op.label : `"${op.label}"`,
insertText: isInQuotes || isJson5 ? op.label : `"${op.label}"`,
range: replaceRange,
sortText: `2${op.label}`,
})),
)

if (props.fields && props.fields.length > 0) {
const matchingFields = props.fields.filter((field) =>
const matchingFields = (props.fields as any[]).filter((field: any) =>
field.field_name.toLowerCase().startsWith(word.word.toLowerCase()),
)
if (matchingFields.length > 0) {
Expand All @@ -216,9 +285,10 @@ const registerMongoCompletion = () => {
label: field.field_name,
kind: monaco.languages.CompletionItemKind.Field,
detail: field.data_type,
insertText: isInQuotes
? field.field_name
: `"${field.field_name}"`,
insertText:
isInQuotes || isJson5
? field.field_name
: `"${field.field_name}"`,
range,
sortText: `1${field.field_name}`,
})),
Expand All @@ -227,7 +297,7 @@ const registerMongoCompletion = () => {
}

if (props.variables && props.variables.length > 0 && word.word) {
const matchingVariables = props.variables.filter((variable) =>
const matchingVariables = (props.variables as any[]).filter((variable: any) =>
variable.name.toLowerCase().startsWith(word.word.toLowerCase()),
)

Expand Down Expand Up @@ -271,9 +341,10 @@ const registerMongoCompletion = () => {
}
} else {
// 不在 {{}} 内部,需要完整的 {{variable}}
insertText = isInQuotes
? `{{${variable.name}}}`
: `"{{${variable.name}}}"`
insertText =
isInQuotes || isJson5
? `{{${variable.name}}}`
: `"{{${variable.name}}}"`
}

return {
Expand All @@ -299,19 +370,21 @@ const registerMongoCompletion = () => {
}

// 注册自动补全
let completionDisposable = null
let completionRegistered = false

if (typeof monaco !== 'undefined') {
completionDisposable = registerMongoCompletion()
if (typeof monaco !== 'undefined' && !completionRegistered) {
completionRegistered = true
registerMongoCompletion('json')
registerMongoCompletion(JSON5_LANGUAGE_ID)
}

const validateJSON = (jsonString) => {
const validateJSON = (jsonString: string) => {
if (!jsonString.trim()) {
return { isValid: true, error: null }
}

try {
const parsed = JSON.parse(jsonString)
const parsed = JSON5.parse(jsonString)
if (typeof parsed !== 'object' || parsed === null) {
return {
isValid: false,
Expand All @@ -323,32 +396,37 @@ const validateJSON = (jsonString) => {
}
}
return { isValid: true, error: null }
} catch (syntaxError) {
} catch (syntaxError: any) {
const errorMessage = String(syntaxError?.message ?? '')
return {
isValid: false,
error: {
message: syntaxError.message,
line: getErrorLine(syntaxError.message),
column: getErrorColumn(syntaxError.message),
message: errorMessage,
line: syntaxError.lineNumber || getErrorLine(errorMessage) || 1,
column: syntaxError.columnNumber || getErrorColumn(errorMessage) || 1,
},
}
}
}

// Extract line number from JSON parse error message
const getErrorLine = (errorMessage) => {
function getErrorLine(errorMessage: string) {
const lineMatch = errorMessage.match(/line (\d+)/i)
return lineMatch ? Number.parseInt(lineMatch[1]) : 1
return Number.parseInt(lineMatch?.[1] ?? '1')
}

const getErrorColumn = (errorMessage) => {
function getErrorColumn(errorMessage: string) {
const columnMatch = errorMessage.match(/column (\d+)/i)
return columnMatch ? Number.parseInt(columnMatch[1]) : 1
return Number.parseInt(columnMatch?.[1] ?? '1')
}

const validationError = ref(null)
const validationError = ref<{
message: string
line: number
column: number
} | null>(null)

const handleChange = (val) => {
const handleChange = (val: string) => {
const validation = validateJSON(val)
validationError.value = validation.error

Expand All @@ -360,20 +438,32 @@ const handleChange = (val) => {
})
}

const formatCode = () => {
if (monacoEditorRef.value) {
monacoEditorRef.value.format()
const normalizeJSON = (jsonString: string) => {
if (!jsonString.trim()) {
return ''
}

try {
const parsed = JSON5.parse(jsonString)
if (typeof parsed !== 'object' || parsed === null) {
return null
}
return JSON.stringify(parsed, null, 2)
} catch {
return null
}
}

onBeforeUnmount(() => {
if (completionDisposable) {
completionDisposable.dispose()
const formatCode = () => {
const normalized = normalizeJSON(editorValue.value)
if (normalized !== null) {
editorValue.value = normalized
}
})
}

defineExpose({
format: formatCode,
normalize: normalizeJSON,
getEditor: () => monacoEditorRef.value?.getEditor(),
validateJSON,
})
Expand All @@ -389,7 +479,7 @@ defineExpose({
ref="monacoEditorRef"
v-model="editorValue"
:options="{
language: 'json',
language: 'json5',
automaticLayout: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
Expand All @@ -407,6 +497,7 @@ defineExpose({
formatOnType: false,
...options,
}"
:height="height"
@change="handleChange"
/>
</div>
Expand Down
25 changes: 23 additions & 2 deletions packages/ldp/src/DataTracePage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const bloodlineLoading = ref(false)
const filterMode = ref<'builder' | 'mql'>('builder')
const filterRows = ref<FilterRow[]>([{ field: '', operator: '=', value: '' }])
const mqlJson = ref('')
const mqlEditorRef = ref<any>(null)
const trackedFields = ref<string[]>([])
const trackedFieldInput = ref('')
const selectedNodeId = ref<string | null>(null)
Expand Down Expand Up @@ -471,7 +472,22 @@ function handleTrace() {
.filter(Boolean)
if (custom.length) filters = { custom }
} else {
filters = { sql: mqlJson.value }
const validation = mqlEditorRef.value?.validateJSON(mqlJson.value)
if (!validation?.isValid) {
ElMessage.error(
`${t('public_json_format_error')}: ${validation?.error?.message || ''}`,
)
tracing.value = false
nodeStatus.value = {}
return
}

const normalizedMqlJson =
mqlEditorRef.value?.normalize(mqlJson.value) ?? mqlJson.value
mqlJson.value = normalizedMqlJson
if (normalizedMqlJson.trim()) {
filters = { sql: normalizedMqlJson }
}
}

traceAbortController = getTraceData(
Expand Down Expand Up @@ -977,7 +993,12 @@ const OplogTreeNode = defineComponent({

<!-- MQL Mode -->
<div v-else class="trace-mql">
<MqlEditor v-model="mqlJson" height="180" :fields="fieldOptions" />
<MqlEditor
ref="mqlEditorRef"
v-model="mqlJson"
:height="180"
:fields="fieldOptions"
/>
</div>

<!-- Tracked Fields -->
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading