Skip to content

Commit ca36f05

Browse files
authored
Merge pull request #614 from tapdata/fix/TAP-12259-ui
feat: add JSON5 support and enhance MQL editor functionality
2 parents 6666458 + e35aa44 commit ca36f05

5 files changed

Lines changed: 166 additions & 41 deletions

File tree

packages/business/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"axios": "catalog:",
2121
"cron-parser": "catalog:",
2222
"dayjs": "catalog:",
23+
"json5": "^2.2.3",
2324
"juice": "catalog:",
2425
"lodash": "catalog:",
2526
"monaco-editor": "catalog:",

packages/business/src/views/data-server/Drawer.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,11 @@ const save = async (type?: boolean) => {
453453
}
454454
}
455455
456+
const normalizedCustomWhere =
457+
apiType === 'customerQuery' && fullCustomQuery
458+
? mqlEditor.value?.normalize(customWhere) ?? customWhere
459+
: customWhere
460+
456461
const params = form.value?.params
457462
?.filter((t: any) => t.name)
458463
.map((t: any) => {
@@ -537,12 +542,16 @@ const save = async (type?: boolean) => {
537542
fields,
538543
path,
539544
fullCustomQuery,
540-
customWhere,
545+
customWhere: normalizedCustomWhere,
541546
},
542547
],
543548
pathSetting: pathSettingList,
544549
}
545550
551+
if (apiType === 'customerQuery' && fullCustomQuery) {
552+
form.value.customWhere = normalizedCustomWhere
553+
}
554+
546555
if (!type && connectionId && tableName) {
547556
formData.fields = allFields.value
548557
// const fieldList = await getAllFields()

packages/business/src/views/data-server/MqlEditor.vue

Lines changed: 129 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<script setup lang="ts">
22
import { useI18n } from '@tap/i18n'
3+
import JSON5 from 'json5'
34
import * as monaco from 'monaco-editor'
4-
import { onBeforeUnmount, ref } from 'vue'
5+
import { ref } from 'vue'
56
import MonacoEditor from './MonacoEditor.vue'
67
78
const { t } = useI18n()
9+
const JSON5_LANGUAGE_ID = 'json5'
810
911
const props = defineProps({
1012
height: {
@@ -35,7 +37,73 @@ const editorValue = defineModel('modelValue', {
3537
type: String,
3638
default: '',
3739
})
38-
const monacoEditorRef = ref(null)
40+
const monacoEditorRef = ref<any>(null)
41+
42+
let json5LanguageRegistered = false
43+
44+
const registerJson5Language = () => {
45+
if (json5LanguageRegistered) return
46+
47+
monaco.languages.register({
48+
id: JSON5_LANGUAGE_ID,
49+
extensions: ['.json5'],
50+
aliases: ['JSON5', 'json5'],
51+
})
52+
53+
monaco.languages.setLanguageConfiguration(JSON5_LANGUAGE_ID, {
54+
comments: {
55+
lineComment: '//',
56+
blockComment: ['/*', '*/'],
57+
},
58+
brackets: [
59+
['{', '}'],
60+
['[', ']'],
61+
],
62+
autoClosingPairs: [
63+
{ open: '{', close: '}' },
64+
{ open: '[', close: ']' },
65+
{ open: '(', close: ')' },
66+
{ open: '"', close: '"', notIn: ['string', 'comment'] },
67+
{ open: "'", close: "'", notIn: ['string', 'comment'] },
68+
],
69+
surroundingPairs: [
70+
{ open: '{', close: '}' },
71+
{ open: '[', close: ']' },
72+
{ open: '(', close: ')' },
73+
{ open: '"', close: '"' },
74+
{ open: "'", close: "'" },
75+
],
76+
})
77+
78+
monaco.languages.setMonarchTokensProvider(JSON5_LANGUAGE_ID, {
79+
defaultToken: '',
80+
tokenPostfix: '.json5',
81+
tokenizer: {
82+
root: [
83+
[/[ \t\r\n]+/, 'white'],
84+
[/\/\/.*$/, 'comment'],
85+
[/\/\*/, { token: 'comment', next: '@comment' }],
86+
[/\b(?:true|false|null|Infinity|NaN)\b/, 'keyword'],
87+
[/[+\-]?(?:0x[0-9a-f]+|(?:\d+\.\d*|\.\d+|\d+)(?:e[+\-]?\d+)?)/i, 'number'],
88+
[/'(?:[^'\\]|\\.)*'/, 'string'],
89+
[/"(?:[^"\\]|\\.)*"/, 'string'],
90+
[/[A-Z_$][\w$]*/i, 'identifier'],
91+
[/[{}[\]]/, '@brackets'],
92+
[/[:,]/, 'delimiter'],
93+
[/./, 'delimiter.invalid'],
94+
],
95+
comment: [
96+
[/[^/*]+/, 'comment'],
97+
[/\*\//, { token: 'comment', next: '@pop' }],
98+
[/[/*]/, 'comment'],
99+
],
100+
},
101+
})
102+
103+
json5LanguageRegistered = true
104+
}
105+
106+
registerJson5Language()
39107
40108
const mongoOperators = [
41109
// Comparison operators
@@ -141,16 +209,17 @@ const mongoOperators = [
141209
},
142210
]
143211
144-
const registerMongoCompletion = () => {
145-
return monaco.languages.registerCompletionItemProvider('json', {
212+
const registerMongoCompletion = (languageId: string) => {
213+
return monaco.languages.registerCompletionItemProvider(languageId, {
146214
triggerCharacters: ['$', '"', "'", '{'],
147-
provideCompletionItems: (model, position) => {
215+
provideCompletionItems: (model: any, position: any) => {
148216
const word = model.getWordUntilPosition(position)
149217
const lineContent = model.getLineContent(position.lineNumber)
150218
const textBeforeCursor = lineContent.slice(
151219
0,
152220
Math.max(0, position.column - 1),
153221
)
222+
const isJson5 = model.getLanguageId() === JSON5_LANGUAGE_ID
154223
155224
const range = {
156225
startLineNumber: position.lineNumber,
@@ -159,7 +228,7 @@ const registerMongoCompletion = () => {
159228
endColumn: word.endColumn,
160229
}
161230
162-
const suggestions = []
231+
const suggestions: any[] = []
163232
164233
if (lineContent?.trim() === '{}') return { suggestions }
165234
@@ -200,14 +269,14 @@ const registerMongoCompletion = () => {
200269
label: op.label,
201270
kind: op.kind,
202271
detail: op.detail,
203-
insertText: isInQuotes ? op.label : `"${op.label}"`,
272+
insertText: isInQuotes || isJson5 ? op.label : `"${op.label}"`,
204273
range: replaceRange,
205274
sortText: `2${op.label}`,
206275
})),
207276
)
208277
209278
if (props.fields && props.fields.length > 0) {
210-
const matchingFields = props.fields.filter((field) =>
279+
const matchingFields = (props.fields as any[]).filter((field: any) =>
211280
field.field_name.toLowerCase().startsWith(word.word.toLowerCase()),
212281
)
213282
if (matchingFields.length > 0) {
@@ -216,9 +285,10 @@ const registerMongoCompletion = () => {
216285
label: field.field_name,
217286
kind: monaco.languages.CompletionItemKind.Field,
218287
detail: field.data_type,
219-
insertText: isInQuotes
220-
? field.field_name
221-
: `"${field.field_name}"`,
288+
insertText:
289+
isInQuotes || isJson5
290+
? field.field_name
291+
: `"${field.field_name}"`,
222292
range,
223293
sortText: `1${field.field_name}`,
224294
})),
@@ -227,7 +297,7 @@ const registerMongoCompletion = () => {
227297
}
228298
229299
if (props.variables && props.variables.length > 0 && word.word) {
230-
const matchingVariables = props.variables.filter((variable) =>
300+
const matchingVariables = (props.variables as any[]).filter((variable: any) =>
231301
variable.name.toLowerCase().startsWith(word.word.toLowerCase()),
232302
)
233303
@@ -271,9 +341,10 @@ const registerMongoCompletion = () => {
271341
}
272342
} else {
273343
// 不在 {{}} 内部,需要完整的 {{variable}}
274-
insertText = isInQuotes
275-
? `{{${variable.name}}}`
276-
: `"{{${variable.name}}}"`
344+
insertText =
345+
isInQuotes || isJson5
346+
? `{{${variable.name}}}`
347+
: `"{{${variable.name}}}"`
277348
}
278349
279350
return {
@@ -299,19 +370,21 @@ const registerMongoCompletion = () => {
299370
}
300371
301372
// 注册自动补全
302-
let completionDisposable = null
373+
let completionRegistered = false
303374
304-
if (typeof monaco !== 'undefined') {
305-
completionDisposable = registerMongoCompletion()
375+
if (typeof monaco !== 'undefined' && !completionRegistered) {
376+
completionRegistered = true
377+
registerMongoCompletion('json')
378+
registerMongoCompletion(JSON5_LANGUAGE_ID)
306379
}
307380
308-
const validateJSON = (jsonString) => {
381+
const validateJSON = (jsonString: string) => {
309382
if (!jsonString.trim()) {
310383
return { isValid: true, error: null }
311384
}
312385
313386
try {
314-
const parsed = JSON.parse(jsonString)
387+
const parsed = JSON5.parse(jsonString)
315388
if (typeof parsed !== 'object' || parsed === null) {
316389
return {
317390
isValid: false,
@@ -323,32 +396,37 @@ const validateJSON = (jsonString) => {
323396
}
324397
}
325398
return { isValid: true, error: null }
326-
} catch (syntaxError) {
399+
} catch (syntaxError: any) {
400+
const errorMessage = String(syntaxError?.message ?? '')
327401
return {
328402
isValid: false,
329403
error: {
330-
message: syntaxError.message,
331-
line: getErrorLine(syntaxError.message),
332-
column: getErrorColumn(syntaxError.message),
404+
message: errorMessage,
405+
line: syntaxError.lineNumber || getErrorLine(errorMessage) || 1,
406+
column: syntaxError.columnNumber || getErrorColumn(errorMessage) || 1,
333407
},
334408
}
335409
}
336410
}
337411
338412
// Extract line number from JSON parse error message
339-
const getErrorLine = (errorMessage) => {
413+
function getErrorLine(errorMessage: string) {
340414
const lineMatch = errorMessage.match(/line (\d+)/i)
341-
return lineMatch ? Number.parseInt(lineMatch[1]) : 1
415+
return Number.parseInt(lineMatch?.[1] ?? '1')
342416
}
343417
344-
const getErrorColumn = (errorMessage) => {
418+
function getErrorColumn(errorMessage: string) {
345419
const columnMatch = errorMessage.match(/column (\d+)/i)
346-
return columnMatch ? Number.parseInt(columnMatch[1]) : 1
420+
return Number.parseInt(columnMatch?.[1] ?? '1')
347421
}
348422
349-
const validationError = ref(null)
423+
const validationError = ref<{
424+
message: string
425+
line: number
426+
column: number
427+
} | null>(null)
350428
351-
const handleChange = (val) => {
429+
const handleChange = (val: string) => {
352430
const validation = validateJSON(val)
353431
validationError.value = validation.error
354432
@@ -360,20 +438,32 @@ const handleChange = (val) => {
360438
})
361439
}
362440
363-
const formatCode = () => {
364-
if (monacoEditorRef.value) {
365-
monacoEditorRef.value.format()
441+
const normalizeJSON = (jsonString: string) => {
442+
if (!jsonString.trim()) {
443+
return ''
444+
}
445+
446+
try {
447+
const parsed = JSON5.parse(jsonString)
448+
if (typeof parsed !== 'object' || parsed === null) {
449+
return null
450+
}
451+
return JSON.stringify(parsed, null, 2)
452+
} catch {
453+
return null
366454
}
367455
}
368456
369-
onBeforeUnmount(() => {
370-
if (completionDisposable) {
371-
completionDisposable.dispose()
457+
const formatCode = () => {
458+
const normalized = normalizeJSON(editorValue.value)
459+
if (normalized !== null) {
460+
editorValue.value = normalized
372461
}
373-
})
462+
}
374463
375464
defineExpose({
376465
format: formatCode,
466+
normalize: normalizeJSON,
377467
getEditor: () => monacoEditorRef.value?.getEditor(),
378468
validateJSON,
379469
})
@@ -389,7 +479,7 @@ defineExpose({
389479
ref="monacoEditorRef"
390480
v-model="editorValue"
391481
:options="{
392-
language: 'json',
482+
language: 'json5',
393483
automaticLayout: true,
394484
minimap: { enabled: false },
395485
scrollBeyondLastLine: false,
@@ -407,6 +497,7 @@ defineExpose({
407497
formatOnType: false,
408498
...options,
409499
}"
500+
:height="height"
410501
@change="handleChange"
411502
/>
412503
</div>

packages/ldp/src/DataTracePage.vue

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const bloodlineLoading = ref(false)
103103
const filterMode = ref<'builder' | 'mql'>('builder')
104104
const filterRows = ref<FilterRow[]>([{ field: '', operator: '=', value: '' }])
105105
const mqlJson = ref('')
106+
const mqlEditorRef = ref<any>(null)
106107
const trackedFields = ref<string[]>([])
107108
const trackedFieldInput = ref('')
108109
const selectedNodeId = ref<string | null>(null)
@@ -471,7 +472,22 @@ function handleTrace() {
471472
.filter(Boolean)
472473
if (custom.length) filters = { custom }
473474
} else {
474-
filters = { sql: mqlJson.value }
475+
const validation = mqlEditorRef.value?.validateJSON(mqlJson.value)
476+
if (!validation?.isValid) {
477+
ElMessage.error(
478+
`${t('public_json_format_error')}: ${validation?.error?.message || ''}`,
479+
)
480+
tracing.value = false
481+
nodeStatus.value = {}
482+
return
483+
}
484+
485+
const normalizedMqlJson =
486+
mqlEditorRef.value?.normalize(mqlJson.value) ?? mqlJson.value
487+
mqlJson.value = normalizedMqlJson
488+
if (normalizedMqlJson.trim()) {
489+
filters = { sql: normalizedMqlJson }
490+
}
475491
}
476492
477493
traceAbortController = getTraceData(
@@ -977,7 +993,12 @@ const OplogTreeNode = defineComponent({
977993

978994
<!-- MQL Mode -->
979995
<div v-else class="trace-mql">
980-
<MqlEditor v-model="mqlJson" height="180" :fields="fieldOptions" />
996+
<MqlEditor
997+
ref="mqlEditorRef"
998+
v-model="mqlJson"
999+
:height="180"
1000+
:fields="fieldOptions"
1001+
/>
9811002
</div>
9821003

9831004
<!-- Tracked Fields -->

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)