From 7a8656dfc41e5f1da76029017f68bf39939b2af2 Mon Sep 17 00:00:00 2001 From: Omkar Kesarkhane <127078473+omkarK06@users.noreply.github.com> Date: Thu, 22 Aug 2024 21:12:55 +0530 Subject: [PATCH] fix: Updated QueryEditor component to support languages (#3988) fix #4292 ## Summary by CodeRabbit - **New Features** - Introduced a new custom `QueryEditor` component for a more customizable and consistent editing experience across various forms and editors. - Added support for a new programming language, VRL, including syntax highlighting and theming. - **Enhancements** - Updated multiple components (`AddTemplate.vue`, `AddFunction.vue`, `EditRole.vue`) to utilize the `QueryEditor` component, improving editing functionality and user interface. - Enhanced the `SearchBar.vue` component for better formatting and language handling. - **Bug Fixes** - Adjusted the language attribute in the `SearchBar.vue` component from "ruby" to "vrl" to ensure correct syntax highlighting. - **Tests** - Improved stability of the alerts test suite by adding a timeout after user input actions. - Added a timeout in the sanity test cases to enhance the reliability of template submission. --- .../playwright-tests/alerts.spec.js | 1 + .../playwright-tests/sanity.spec.js | 1 + web/src/components/QueryEditor.vue | 56 +- web/src/components/alerts/AddTemplate.vue | 94 +-- web/src/components/functions/AddFunction.vue | 81 +-- web/src/components/iam/roles/EditRole.vue | 11 +- .../components/iam/roles/PermissionsJSON.vue | 382 ---------- web/src/plugins/logs/SearchBar.vue | 95 ++- web/src/utils/query/vrlLanguageDefinition.ts | 660 ++++++++++++++++++ 9 files changed, 795 insertions(+), 586 deletions(-) delete mode 100644 web/src/components/iam/roles/PermissionsJSON.vue create mode 100644 web/src/utils/query/vrlLanguageDefinition.ts diff --git a/tests/ui-testing/playwright-tests/alerts.spec.js b/tests/ui-testing/playwright-tests/alerts.spec.js index 08b3fd7111..65918cd4d2 100644 --- a/tests/ui-testing/playwright-tests/alerts.spec.js +++ b/tests/ui-testing/playwright-tests/alerts.spec.js @@ -214,6 +214,7 @@ test.describe("Alerts testcases", () => { '[data-test="add-template-name-input"]').fill("test"); await page.click(".view-line") await page.keyboard.type('test'); + await page.waitForTimeout(500); await page.locator( '[data-test="add-template-submit-btn"]').click({ force: true }); await expect(page.locator( ".q-notification__message").getByText(/Please enter valid JSON/).first()).toBeVisible(); diff --git a/tests/ui-testing/playwright-tests/sanity.spec.js b/tests/ui-testing/playwright-tests/sanity.spec.js index 6f3c892f62..f8979cb867 100644 --- a/tests/ui-testing/playwright-tests/sanity.spec.js +++ b/tests/ui-testing/playwright-tests/sanity.spec.js @@ -427,6 +427,7 @@ test.describe("Sanity testcases", () => { const jsonString = '{"text": "{alert_name} is active"}'; await page.click(".view-line"); await page.keyboard.type(jsonString); + await page.waitForTimeout(500); await page .locator('[data-test="add-template-submit-btn"]') .click({ force: true }); diff --git a/web/src/components/QueryEditor.vue b/web/src/components/QueryEditor.vue index a92046358a..9abf25dde9 100644 --- a/web/src/components/QueryEditor.vue +++ b/web/src/components/QueryEditor.vue @@ -37,9 +37,8 @@ import { } from "vue"; import "monaco-editor/esm/vs/editor/editor.all.js"; -import "monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js"; -import "monaco-editor/esm/vs/basic-languages/sql/sql.js"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { vrlLanguageDefinition } from "@/utils/query/vrlLanguageDefinition"; import { useStore } from "vuex"; import { debounce } from "quasar"; @@ -125,7 +124,7 @@ export default defineComponent({ }); }; - onMounted(async () => { + const setupEditor = () => { monaco.editor.defineTheme("myCustomTheme", { base: "vs", // can also be vs-dark or hc-black inherit: true, // can also be false to completely replace the builtin rules @@ -210,6 +209,52 @@ export default defineComponent({ window.addEventListener("click", () => { editorObj.layout(); }); + + window.addEventListener("resize", async () => { + await nextTick(); + editorObj.layout(); + // queryEditorRef.value.resetEditorLayout(); + }); + }; + + onMounted(async () => { + provider.value?.dispose(); + + if (props.language === "vrl") { + monaco.languages.register({ id: "vrl" }); + + // Register a tokens provider for the language + monaco.languages.setMonarchTokensProvider( + "vrl", + vrlLanguageDefinition as any + ); + } + + if (props.language === "sql") { + await import( + "monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js" + ); + } + + if (props.language === "json") { + await import( + "monaco-editor/esm/vs/language/json/monaco.contribution.js" + ); + } + + if (props.language === "html") { + await import( + "monaco-editor/esm/vs/language/html/monaco.contribution.js" + ); + } + + if (props.language === "markdown") { + await import( + "monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js" + ); + } + + setupEditor(); }); onActivated(async () => { @@ -329,6 +374,10 @@ export default defineComponent({ editorRef.value.dispatchEvent(escEvent); }; + const formatDocument = () => { + editorObj.getAction("editor.action.formatDocument").run(); + }; + const getCursorIndex = () => { const currentPosition = editorObj.getPosition(); const cursorIndex = editorObj.getModel().getOffsetAt(currentPosition) - 1; @@ -344,6 +393,7 @@ export default defineComponent({ triggerAutoComplete, getCursorIndex, searchObj, + formatDocument, }; }, }); diff --git a/web/src/components/alerts/AddTemplate.vue b/web/src/components/alerts/AddTemplate.vue index 0f3a2982e0..2b72020a87 100644 --- a/web/src/components/alerts/AddTemplate.vue +++ b/web/src/components/alerts/AddTemplate.vue @@ -81,15 +81,14 @@ along with this program. If not, see . > {{ t("alert_templates.body") + " *" }} -
@@ -169,29 +168,17 @@ along with this program. If not, see . - - diff --git a/web/src/plugins/logs/SearchBar.vue b/web/src/plugins/logs/SearchBar.vue index 811f1548ec..86fdbcaa2e 100644 --- a/web/src/plugins/logs/SearchBar.vue +++ b/web/src/plugins/logs/SearchBar.vue @@ -180,7 +180,7 @@ along with this program. If not, see . @click.stop=" handleFavoriteSavedView( props.row, - favoriteViews.includes(props.row.view_id) + favoriteViews.includes(props.row.view_id), ) " > @@ -256,7 +256,7 @@ along with this program. If not, see . @click.stop=" handleFavoriteSavedView( props.row, - favoriteViews.includes(props.row.view_id) + favoriteViews.includes(props.row.view_id), ) " > @@ -555,8 +555,8 @@ along with this program. If not, see . ? 'empty-function' : '' " - language="ruby" @keydown.ctrl.enter="handleRunQueryFn" + language="vrl" @focus="searchObj.meta.functionEditorPlaceholderFlag = false" @blur="searchObj.meta.functionEditorPlaceholderFlag = true" /> @@ -936,7 +936,7 @@ export default defineComponent({ components: { DateTime, QueryEditor: defineAsyncComponent( - () => import("@/components/QueryEditor.vue") + () => import("@/components/QueryEditor.vue"), ), SyntaxGuide, AutoRefreshInterval, @@ -1008,7 +1008,7 @@ export default defineComponent({ query: this.searchObj.data.customDownloadQueryObj, page_type: this.searchObj.data.stream.streamType, }, - "UI" + "UI", ) .then((res) => { this.customDownloadDialog = false; @@ -1070,7 +1070,6 @@ export default defineComponent({ } = useLogs(); const queryEditorRef = ref(null); - const formData: any = ref(defaultValue()); const functionOptions = ref(searchObj.data.transforms); @@ -1134,7 +1133,7 @@ export default defineComponent({ (fields) => { if (fields != undefined && fields.length) updateFieldKeywords(fields); }, - { immediate: true, deep: true } + { immediate: true, deep: true }, ); watch( @@ -1142,7 +1141,7 @@ export default defineComponent({ (funs) => { if (funs.length) updateFunctionKeywords(funs); }, - { immediate: true, deep: true } + { immediate: true, deep: true }, ); onBeforeMount(async () => { @@ -1265,13 +1264,13 @@ export default defineComponent({ if (searchObj.data.parsedQuery?.from?.length > 0) { if ( !searchObj.data.stream.selectedStream.includes( - searchObj.data.parsedQuery.from[0].table + searchObj.data.parsedQuery.from[0].table, ) && searchObj.data.parsedQuery.from[0].table !== streamName ) { let streamFound = false; searchObj.data.stream.selectedStream = []; - + streamName = searchObj.data.parsedQuery.from[0].table; searchObj.data.streamResults.list.forEach((stream) => { if (stream.name == searchObj.data.parsedQuery.from[0].table) { @@ -1294,7 +1293,7 @@ export default defineComponent({ // true // ) // searchObj.data.stream.selectedStreamFields = data.schema - + // } } }); @@ -1313,7 +1312,7 @@ export default defineComponent({ } } } catch (e) { - console.log(e,"Logs: Error while updating query value"); + console.log(e, "Logs: Error while updating query value"); } }; @@ -1341,12 +1340,12 @@ export default defineComponent({ value.selectedDate.from = timestampToTimezoneDate( value.startTime / 1000, store.state.timezone, - "yyyy/MM/DD" + "yyyy/MM/DD", ); value.selectedTime.startTime = timestampToTimezoneDate( value.startTime / 1000, store.state.timezone, - "HH:mm" + "HH:mm", ); dateTimeRef.value.setAbsoluteTime(value.startTime, value.endTime); @@ -1532,7 +1531,7 @@ export default defineComponent({ if (isSavedFunctionAction.value == "create") { callTransform = jsTransformService.create( store.state.selectedOrganization.identifier, - formData.value + formData.value, ); callTransform @@ -1574,7 +1573,7 @@ export default defineComponent({ saveFunctionLoader.value = true; callTransform = jsTransformService.update( store.state.selectedOrganization.identifier, - formData.value + formData.value, ); callTransform @@ -1585,7 +1584,7 @@ export default defineComponent({ }); const transformIndex = searchObj.data.transforms.findIndex( - (obj) => obj.name === formData.value.name + (obj) => obj.name === formData.value.name, ); if (transformIndex !== -1) { searchObj.data.transforms[transformIndex].name = @@ -1694,7 +1693,7 @@ export default defineComponent({ } else { const needle = val.toLowerCase(); functionOptions.value = searchObj.data.transforms.filter( - (v) => v.name?.toLowerCase().indexOf(needle) > -1 + (v) => v.name?.toLowerCase().indexOf(needle) > -1, ); } }); @@ -1726,7 +1725,7 @@ export default defineComponent({ savedviewsService .getViewDetail( store.state.selectedOrganization.identifier, - item.view_id + item.view_id, ) .then(async (res) => { if (res.status == 200) { @@ -1757,32 +1756,32 @@ export default defineComponent({ // ----- Here we are explicitly handling stream change for multistream ----- let selectedStreams = []; const streamValues = searchObj.data.stream.streamLists.map( - (item) => item.value + (item) => item.value, ); if (typeof extractedObj.data.stream.selectedStream == "object") { if ( extractedObj.data.stream.selectedStream.hasOwnProperty( - "value" + "value", ) ) { selectedStreams.push( - extractedObj.data.stream.selectedStream.value + extractedObj.data.stream.selectedStream.value, ); } else { selectedStreams.push( - ...extractedObj.data.stream.selectedStream + ...extractedObj.data.stream.selectedStream, ); } } else { selectedStreams.push(extractedObj.data.stream.selectedStream); } const streamNotExist = selectedStreams.filter( - (stream_str) => !streamValues.includes(stream_str) + (stream_str) => !streamValues.includes(stream_str), ); if (streamNotExist.length > 0) { let errMsg = t("search.streamNotExist").replace( "[STREAM_NAME]", - streamNotExist + streamNotExist, ); throw new Error(errMsg); return; @@ -1821,7 +1820,7 @@ export default defineComponent({ name: "", function: searchObj.data.tempFunctionContent, }, - false + false, ); searchObj.data.tempFunctionContent = extractedObj.data.tempFunctionContent; @@ -1832,7 +1831,7 @@ export default defineComponent({ name: "", function: "", }, - false + false, ); searchObj.data.tempFunctionContent = ""; searchObj.meta.functionEditorPlaceholderFlag = true; @@ -1867,15 +1866,15 @@ export default defineComponent({ if (typeof extractedObj.data.stream.selectedStream == "object") { if ( extractedObj.data.stream.selectedStream.hasOwnProperty( - "value" + "value", ) ) { selectedStreams.push( - extractedObj.data.stream.selectedStream.value + extractedObj.data.stream.selectedStream.value, ); } else { selectedStreams.push( - ...extractedObj.data.stream.selectedStream + ...extractedObj.data.stream.selectedStream, ); } } else { @@ -1898,7 +1897,7 @@ export default defineComponent({ const streamData = await getStreams( searchObj.data.stream.streamType, - true + true, ); searchObj.data.streamResults = streamData; await loadStreamLists(); @@ -1906,15 +1905,15 @@ export default defineComponent({ // searchObj.value = mergeDeep(searchObj, extractedObj); const streamValues = searchObj.data.stream.streamLists.map( - (item) => item.value + (item) => item.value, ); const streamNotExist = selectedStreams.filter( - (stream_str) => !streamValues.includes(stream_str) + (stream_str) => !streamValues.includes(stream_str), ); if (streamNotExist.length > 0) { let errMsg = t("search.streamNotExist").replace( "[STREAM_NAME]", - streamNotExist + streamNotExist, ); throw new Error(errMsg); return; @@ -1926,7 +1925,7 @@ export default defineComponent({ name: "", function: searchObj.data.tempFunctionContent, }, - false + false, ); searchObj.data.tempFunctionContent = extractedObj.data.tempFunctionContent; @@ -1937,7 +1936,7 @@ export default defineComponent({ name: "", function: "", }, - false + false, ); searchObj.data.tempFunctionContent = ""; searchObj.meta.functionEditorPlaceholderFlag = true; @@ -2029,7 +2028,7 @@ export default defineComponent({ saveViewLoader.value = true; updateSavedViews( savedViewSelectedName.value.view_id, - savedViewSelectedName.value.view_name + savedViewSelectedName.value.view_name, ); }); } else { @@ -2048,7 +2047,7 @@ export default defineComponent({ savedviewsService .delete( store.state.selectedOrganization.identifier, - deleteViewID.value + deleteViewID.value, ) .then((res) => { if (res.status == 200) { @@ -2203,7 +2202,7 @@ export default defineComponent({ searchObj.data.savedViews[index].payload = viewObj.data; searchObj.data.savedViews[index].view_name = viewName; } - } + }, ); $q.notify({ @@ -2255,7 +2254,7 @@ export default defineComponent({ const queryString = Object.entries(queryObj) .map( ([key, value]) => - `${encodeURIComponent(key)}=${encodeURIComponent(value)}` + `${encodeURIComponent(key)}=${encodeURIComponent(value)}`, ) .join("&"); @@ -2285,7 +2284,7 @@ export default defineComponent({ const resetFilters = () => { if (searchObj.meta.sqlMode == true) { searchObj.data.query = `SELECT [FIELD_LIST] FROM "${searchObj.data.stream.selectedStream.join( - "," + ",", )}" ORDER BY ${store.state.zoConfig.timestamp_column} DESC`; if ( searchObj.data.stream.interestingFieldList.length > 0 && @@ -2293,12 +2292,12 @@ export default defineComponent({ ) { searchObj.data.query = searchObj.data.query.replace( "[FIELD_LIST]", - searchObj.data.stream.interestingFieldList.join(",") + searchObj.data.stream.interestingFieldList.join(","), ); } else { searchObj.data.query = searchObj.data.query.replace( "[FIELD_LIST]", - "*" + "*", ); } } else { @@ -2343,7 +2342,7 @@ export default defineComponent({ let favoriteViewsList = localSavedViews.value; if (favoriteViewsList.length > 0) { favoriteViewsList = favoriteViewsList.filter( - (item) => item.view_id != row.view_id + (item) => item.view_id != row.view_id, ); // for (const [key, item] of favoriteViewsList.entries()) { // console.log(item, key); @@ -2620,7 +2619,7 @@ export default defineComponent({ if (query.toLowerCase().includes("order by")) { const [beforeOrderBy, afterOrderBy] = queryIndexSplit( query, - "order by" + "order by", ); query = beforeOrderBy.trim() + @@ -2631,7 +2630,7 @@ export default defineComponent({ } else if (query.toLowerCase().includes("limit")) { const [beforeLimit, afterLimit] = queryIndexSplit( query, - "limit" + "limit", ); query = beforeLimit.trim() + " AND " + filter + " limit" + afterLimit; @@ -2642,7 +2641,7 @@ export default defineComponent({ if (query.toLowerCase().includes("order by")) { const [beforeOrderBy, afterOrderBy] = queryIndexSplit( query, - "order by" + "order by", ); query = beforeOrderBy.trim() + @@ -2653,7 +2652,7 @@ export default defineComponent({ } else if (query.toLowerCase().includes("limit")) { const [beforeLimit, afterLimit] = queryIndexSplit( query, - "limit" + "limit", ); query = beforeLimit.trim() + diff --git a/web/src/utils/query/vrlLanguageDefinition.ts b/web/src/utils/query/vrlLanguageDefinition.ts new file mode 100644 index 0000000000..d21962dd20 --- /dev/null +++ b/web/src/utils/query/vrlLanguageDefinition.ts @@ -0,0 +1,660 @@ +// VRL Language Definition +export const vrlLanguageDefinition = { + defaultToken: "invalid", + ignoreCase: true, + tokenPostfix: ".vrl", + + brackets: [ + { open: "{", close: "}", token: "delimiter.curly" }, + { open: "[", close: "]", token: "delimiter.square" }, + { open: "(", close: ")", token: "delimiter.parenthesis" }, + ], + + regEx: /\/(?!\/\/)(?:[^\/\\]|\\.)*\/[igm]*/, + + keywords: [ + "abort", + "as", + "break", + "continue", + "else", + "false", + "for", + "if", + "impl", + "in", + "let", + "loop", + "null", + "return", + "self", + "std", + "then", + "this", + "true", + "type", + "until", + "use", + "while", + ], + + // we include these common regular expressions + symbols: /[=>