From 1e7767a5b139ce129cb504d583e1bf8a7f60e16b Mon Sep 17 00:00:00 2001 From: sun Date: Thu, 5 Dec 2024 12:06:42 +0800 Subject: [PATCH] feat: log query and log pipelines (#455) * feat: logs * fix: interceptor * build: update pnpm lockfile * fix: table selection * fix: columns visible * fix: langjson * build: update pnpm lockfile * feat: reorder menu * fix: typo * fix: pipeline db * fix: add db init * feat: uppercase * fix: export name * fix: typo * feat: tsconfig * fix: ts * feat: select auto widh * refactor: remove scripts module --------- Co-authored-by: ZonaHe --- .eslintrc.js | 1 + package.json | 6 +- pnpm-lock.yaml | 74 +++-- src/api/axios.d.ts | 2 +- src/api/editor.ts | 9 +- src/api/interceptor.ts | 31 +- src/api/pipeline.ts | 94 ++++++ src/assets/style/select.less | 2 +- src/components/index.ts | 2 + src/components/time-select/index.vue | 65 ++-- src/components/yml-editor.vue | 43 +++ src/locale/en-US.ts | 2 + src/locale/en-US/logquery.ts | 15 + src/locale/en-US/menu.ts | 2 + src/locale/zh-CN.ts | 1 + src/locale/zh-CN/logquery.ts | 15 + src/locale/zh-CN/menu.ts | 2 + src/main.ts | 5 + src/router/routes/modules/dashboard.ts | 22 ++ src/store/modules/logquery/index.ts | 293 ++++++++++++++++++ src/views/dashboard/config.ts | 26 ++ .../dashboard/logs/pipelines/PipeFileView.vue | 227 ++++++++++++++ src/views/dashboard/logs/pipelines/index.vue | 118 +++++++ src/views/dashboard/logs/query/CountChart.vue | 218 +++++++++++++ src/views/dashboard/logs/query/ExportLog.vue | 38 +++ src/views/dashboard/logs/query/FormView.vue | 30 ++ .../dashboard/logs/query/InputEditor.vue | 127 ++++++++ src/views/dashboard/logs/query/JSONView.vue | 36 +++ src/views/dashboard/logs/query/LogDetail.vue | 53 ++++ src/views/dashboard/logs/query/Pagination.vue | 196 ++++++++++++ src/views/dashboard/logs/query/SQLBuilder.vue | 160 ++++++++++ src/views/dashboard/logs/query/SavedQuery.vue | 36 +++ src/views/dashboard/logs/query/TableData.vue | 276 +++++++++++++++++ src/views/dashboard/logs/query/Toolbar.vue | 176 +++++++++++ src/views/dashboard/logs/query/index.vue | 112 +++++++ src/views/dashboard/logs/query/types.ts | 16 + src/views/dashboard/logs/query/until.ts | 233 ++++++++++++++ tsconfig.json | 2 + 38 files changed, 2699 insertions(+), 67 deletions(-) create mode 100644 src/api/pipeline.ts create mode 100644 src/components/yml-editor.vue create mode 100644 src/locale/en-US/logquery.ts create mode 100644 src/locale/zh-CN/logquery.ts create mode 100644 src/store/modules/logquery/index.ts create mode 100644 src/views/dashboard/logs/pipelines/PipeFileView.vue create mode 100644 src/views/dashboard/logs/pipelines/index.vue create mode 100644 src/views/dashboard/logs/query/CountChart.vue create mode 100644 src/views/dashboard/logs/query/ExportLog.vue create mode 100644 src/views/dashboard/logs/query/FormView.vue create mode 100644 src/views/dashboard/logs/query/InputEditor.vue create mode 100644 src/views/dashboard/logs/query/JSONView.vue create mode 100644 src/views/dashboard/logs/query/LogDetail.vue create mode 100644 src/views/dashboard/logs/query/Pagination.vue create mode 100644 src/views/dashboard/logs/query/SQLBuilder.vue create mode 100644 src/views/dashboard/logs/query/SavedQuery.vue create mode 100644 src/views/dashboard/logs/query/TableData.vue create mode 100644 src/views/dashboard/logs/query/Toolbar.vue create mode 100644 src/views/dashboard/logs/query/index.vue create mode 100644 src/views/dashboard/logs/query/types.ts create mode 100644 src/views/dashboard/logs/query/until.ts diff --git a/.eslintrc.js b/.eslintrc.js index 75b8845c..708d824f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -69,5 +69,6 @@ module.exports = { 'import/no-extraneous-dependencies': 0, 'noUnusedLocals': 0, 'prefer-destructuring': ['error', { object: true, array: false }], + 'no-continue': 1, }, } diff --git a/package.json b/package.json index 3dff8975..95b00f18 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ ] }, "dependencies": { - "@arco-design/web-vue": "^2.41.0", + "@arco-design/web-vue": "^2.56.2", "@babel/core": "^7.20.5", "@codemirror/autocomplete": "^6.4.2", "@codemirror/lang-java": "^6.0.1", @@ -45,6 +45,7 @@ "@codemirror/state": "^6.2.0", "@codemirror/theme-one-dark": "^6.1.0", "@codemirror/view": "^6.9.3", + "@codemirror/lang-json": "^6.0.1", "@lezer/common": "^1.0.2", "@lezer/highlight": "^1.1.2", "@lezer/lr": "^1.2.3", @@ -84,7 +85,8 @@ "vue-codemirror": "^6.1.1", "vue-echarts": "^6.5.0", "vue-i18n": "^9.2.2", - "vue-router": "^4.0.14" + "vue-router": "^4.0.14", + "js-file-download": "^0.4.12" }, "devDependencies": { "@babel/types": "^7.21.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a51651e4..ccacda4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,8 +10,8 @@ overrides: dependencies: '@arco-design/web-vue': - specifier: ^2.41.0 - version: 2.41.0(vue@3.2.47) + specifier: ^2.56.2 + version: 2.56.3(vue@3.2.47) '@babel/core': specifier: ^7.20.5 version: 7.20.5 @@ -21,6 +21,9 @@ dependencies: '@codemirror/lang-java': specifier: ^6.0.1 version: 6.0.1 + '@codemirror/lang-json': + specifier: ^6.0.1 + version: 6.0.1 '@codemirror/lang-python': specifier: ^6.1.0 version: 6.1.0(@codemirror/state@6.2.0)(@codemirror/view@6.9.3)(@lezer/common@1.0.2) @@ -93,6 +96,9 @@ dependencies: eslint-plugin: specifier: ^1.0.1 version: 1.0.1 + js-file-download: + specifier: ^0.4.12 + version: 0.4.12 json-bigint: specifier: ^1.0.0 version: 1.0.0 @@ -346,8 +352,8 @@ packages: color: 3.2.1 dev: false - /@arco-design/web-vue@2.41.0(vue@3.2.47): - resolution: {integrity: sha512-g3hurEQsdK1LIxYoHc3wvCMdajet1W2y8khSbtSYdjeFaa3B00aaYkworysANLUMLeAg/TEOvDbF+YupxNfuHA==} + /@arco-design/web-vue@2.56.3(vue@3.2.47): + resolution: {integrity: sha512-D2CPIXRBUPcg37TFsfWROZddCWFZnIwqGpsOhOn2BhmH89UFqtBGpTxyuMdYJEwKNXunp3dVL6V69ZMmJBRPOg==} peerDependencies: vue: ^3.1.0 dependencies: @@ -753,7 +759,7 @@ packages: '@codemirror/lang-javascript': 6.2.1 '@codemirror/language': 6.6.0 '@lezer/common': 1.0.2 - '@lezer/highlight': 1.1.2 + '@lezer/highlight': 1.1.6 '@lezer/lr': 1.3.10 dev: false @@ -833,8 +839,8 @@ packages: dependencies: '@codemirror/lang-css': 6.2.1(@codemirror/view@6.9.3) '@codemirror/language': 6.6.0 - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 transitivePeerDependencies: - '@codemirror/view' dev: false @@ -920,8 +926,8 @@ packages: '@codemirror/autocomplete': 6.4.2(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.9.3)(@lezer/common@1.0.2) '@codemirror/language': 6.6.0 '@codemirror/state': 6.2.0 - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 transitivePeerDependencies: - '@codemirror/view' - '@lezer/common' @@ -934,7 +940,7 @@ packages: '@codemirror/lang-javascript': 6.2.1 '@codemirror/language': 6.6.0 '@lezer/common': 1.0.2 - '@lezer/highlight': 1.1.2 + '@lezer/highlight': 1.1.6 '@lezer/lr': 1.3.10 dev: false @@ -942,8 +948,8 @@ packages: resolution: {integrity: sha512-sQLsqhRjl2MWG3rxZysX+2XAyed48KhLBHLgq9xcKxIJu3npH/G+BIXW5NM5mHeDUjG0jcGh9BcjP0NfMStuzA==} dependencies: '@codemirror/language': 6.6.0 - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 dev: false /@codemirror/lang-xml@6.0.2(@codemirror/view@6.9.3): @@ -1383,15 +1389,15 @@ packages: /@lezer/cpp@1.1.1: resolution: {integrity: sha512-eS1M3L3U2mDowoFVPG7tEp01SWu9/68Nx3HEBgLJVn3N9ku7g5S7WdFv0jzmcTipAyONYfZJ+7x4WRkfdB2Ung==} dependencies: - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 dev: false /@lezer/css@1.1.3: resolution: {integrity: sha512-SjSM4pkQnQdJDVc80LYzEaMiNy9txsFbI7HsMgeVF28NdLaAdHNtQ+kB/QqDUzRBV/75NTXjJ/R5IdC8QQGxMg==} dependencies: - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 dev: false /@lezer/highlight@1.1.2: @@ -1410,8 +1416,8 @@ packages: resolution: {integrity: sha512-Kk9HJARZTc0bAnMQUqbtuhFVsB4AnteR2BFUWfZV7L/x1H0aAKz6YabrfJ2gk/BEgjh9L3hg5O4y2IDZRBdzuQ==} dependencies: '@lezer/common': 1.0.2 - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 dev: false /@lezer/java@1.0.4: @@ -1431,15 +1437,15 @@ packages: /@lezer/json@1.0.1: resolution: {integrity: sha512-nkVC27qiEZEjySbi6gQRuMwa2sDu2PtfjSgz0A4QF81QyRGm3kb2YRzLcOPcTEtmcwvrX/cej7mlhbwViA4WJw==} dependencies: - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 dev: false /@lezer/lezer@1.1.2: resolution: {integrity: sha512-O8yw3CxPhzYHB1hvwbdozjnAslhhR8A5BH7vfEMof0xk3p+/DFDfZkA9Tde6J+88WgtwaHy4Sy6ThZSkaI0Evw==} dependencies: - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 dev: false /@lezer/lr@1.2.3: @@ -1458,14 +1464,14 @@ packages: resolution: {integrity: sha512-JYOI6Lkqbl83semCANkO3CKbKc0pONwinyagBufWBm+k4yhIcqfCF8B8fpEpvJLmIy7CAfwiq7dQ/PzUZA340g==} dependencies: '@lezer/common': 1.0.2 - '@lezer/highlight': 1.1.2 + '@lezer/highlight': 1.1.6 dev: false /@lezer/php@1.0.1: resolution: {integrity: sha512-aqdCQJOXJ66De22vzdwnuC502hIaG9EnPK2rSi+ebXyUd+j7GAX1mRjWZOVOmf3GST1YUfUCu6WXDiEgDGOVwA==} dependencies: - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 dev: false /@lezer/python@1.1.8: @@ -1485,15 +1491,15 @@ packages: /@lezer/sass@1.0.3: resolution: {integrity: sha512-n4l2nVOB7gWiGU/Cg2IVxpt2Ic9Hgfgy/7gk+p/XJibAsPXs0lSbsfGwQgwsAw9B/euYo3oS6lEFr9WytoqcZg==} dependencies: - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 dev: false /@lezer/xml@1.0.2: resolution: {integrity: sha512-dlngsWceOtQBMuBPw5wtHpaxdPJ71aVntqjbpGkFtWsp4WtQmCnuTjQGocviymydN6M18fhj6UQX3oiEtSuY7w==} dependencies: - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 dev: false /@nextjournal/lang-clojure@1.0.0: @@ -1506,7 +1512,7 @@ packages: /@nextjournal/lezer-clojure@1.0.0: resolution: {integrity: sha512-VZyuGu4zw5mkTOwQBTaGVNWmsOZAPw5ZRxu1/Knk/Xfs7EDBIogwIs5UXTYkuECX5ZQB8eOB+wKA2pc7VyqaZQ==} dependencies: - '@lezer/lr': 1.2.3 + '@lezer/lr': 1.3.10 dev: false /@nodelib/fs.scandir@2.1.5: @@ -2758,8 +2764,8 @@ packages: resolution: {integrity: sha512-AqSzkQgfWsjBbifio3dy/zDj6WXEw4g52Mq6bltIWLMWryWWRMpFwjQSlHtCGOol1FENYObUF5KI4ofiv8bjXA==} dependencies: '@codemirror/language': 6.6.0 - '@lezer/highlight': 1.1.2 - '@lezer/lr': 1.2.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.10 dev: false /codemirror@6.0.0(@lezer/common@1.0.2): @@ -4591,6 +4597,10 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /js-file-download@0.4.12: + resolution: {integrity: sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==} + dev: false + /js-sdsl@4.4.2: resolution: {integrity: sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==} dev: true diff --git a/src/api/axios.d.ts b/src/api/axios.d.ts index e51eadb4..601cacd7 100644 --- a/src/api/axios.d.ts +++ b/src/api/axios.d.ts @@ -3,6 +3,6 @@ import { AxiosRequestConfig } from 'axios' declare module 'axios' { export interface AxiosRequestConfig { - traceTimeStart: number + traceTimeStart?: number } } diff --git a/src/api/editor.ts b/src/api/editor.ts index 6b7cdea3..fb353970 100644 --- a/src/api/editor.ts +++ b/src/api/editor.ts @@ -2,6 +2,7 @@ import axios, { AxiosRequestConfig, AxiosRequestHeaders } from 'axios' import dayjs from 'dayjs' import qs from 'qs' import { PromForm } from '@/store/modules/code-run/types' +import { HttpResponse } from './interceptor' const sqlUrl = `/v1/sql` const scriptUrl = `/v1/scripts` @@ -97,7 +98,7 @@ const getTableByName = (tableName: string) => { ) } -const runSQL = (code: string) => { +const runSQL = (code: string): Promise => { return axios.post(sqlUrl, makeSqlData(code), addDatabaseParams()) } @@ -136,6 +137,11 @@ const writeInfluxDB = (data: string, precision: string) => { } as AxiosRequestConfig return axios.post(`${influxURL}/write`, data, config) } +const runSQLWithCSV = (code: string): Promise => { + const params = addDatabaseParams() + params.params.format = 'csv' + return axios.post(sqlUrl, makeSqlData(code), params) +} export default { getTables, @@ -149,4 +155,5 @@ export default { writeInfluxDB, checkScriptsTable, fetchTablesCount, + runSQLWithCSV, } diff --git a/src/api/interceptor.ts b/src/api/interceptor.ts index 8ee43f99..cf7fa07d 100644 --- a/src/api/interceptor.ts +++ b/src/api/interceptor.ts @@ -23,6 +23,15 @@ export interface Auth { } // todo: can we use env and proxy at the same time? +export const TableNameReg = /(?<=from|FROM)\s+([^\s;]+)/ +export function parseTable(sql: string) { + const result = sql.match(TableNameReg) + if (result && result.length) { + const arr = result[1].trim().split('.') + return arr[arr.length - 1] + } + return '' +} axios.interceptors.request.use( (config: AxiosRequestConfig) => { @@ -51,6 +60,7 @@ axios.interceptors.request.use( return Promise.reject(error) } ) +const ignoreList = ['pipelines'] axios.interceptors.response.use( (response: AxiosResponse) => { @@ -70,16 +80,22 @@ axios.interceptors.response.use( return Promise.reject(errorResponse) } if (isV1) { + if (response.config.params && response.config.params.format === 'csv') { + return response.data + } response.data = JSONbigint({ storeAsString: true }).parse(response.data) const { data } = response if (data.code && data.code !== 0) { // v1 and error - Message.error({ - content: data.error || 'Error', - duration: 5 * 1000, - closable: true, - resetOnHover: true, - }) + const tableName = parseTable(decodeURIComponent(response.config.data)) + if (ignoreList.indexOf(tableName) === -1) { + Message.error({ + content: data.error || 'Error', + duration: 5 * 1000, + closable: true, + resetOnHover: true, + }) + } const error = { error: data.error || 'Error', startTime: new Date(response.config.traceTimeStart).toLocaleTimeString(), @@ -104,7 +120,8 @@ axios.interceptors.response.use( } const data = JSON.parse(error.response.data) const isInflux = !!error.config.url?.startsWith(`/v1/influxdb`) - if (!isInflux) { + const tableName = parseTable(decodeURIComponent(error.response.config.data)) + if (!isInflux && ignoreList.indexOf(tableName) === -1) { Message.error({ content: data.error || 'Request Error', duration: 5 * 1000, diff --git a/src/api/pipeline.ts b/src/api/pipeline.ts new file mode 100644 index 00000000..b126e24d --- /dev/null +++ b/src/api/pipeline.ts @@ -0,0 +1,94 @@ +import axios from 'axios' +import qs from 'qs' +import dayjs from 'dayjs' +import editorAPI from './editor' + +const { runSQL } = editorAPI +const url = '/v1/events/pipelines' +const sqlUrl = `/v1/sql` + +const makeSqlData = (sql: string) => { + return qs.stringify({ + sql, + }) +} + +export type PipeFile = { + name: string + content?: string + version: string +} + +/* eslint-disable */ +function nanoTimestampToUTCString(nanoTimestamp: bigint) { + nanoTimestamp = BigInt(nanoTimestamp) + const divide1 = BigInt(1000000000) + const seconds = nanoTimestamp / divide1 // na to s + const divide2 = BigInt(1000000) + const milliseconds = Number((nanoTimestamp % divide1) / divide2) // ms + const nanoseconds = Number(nanoTimestamp % divide2) + + const date = new Date(Number(seconds) * 1000) + const isoString = date.toISOString().replace('Z', '').replace('.000', '') + return `${isoString}.${String(milliseconds).padStart(3, '0')}${String(nanoseconds).padStart(6, '0')}Z` +} + +/* eslint-enable */ +export function create(pipeFile: PipeFile) { + const appStore = useAppStore() + const { content, name } = pipeFile + const file = new File([content], `${name}.yaml`, { + type: 'application/yaml', + }) + const formData = new FormData() + formData.append('file', file) + return axios.postForm(`${url}/${name}?db=${appStore.database}`, formData) +} + +export function list() { + return runSQL( + `SELECT name,max(created_at) as created_at FROM greptime_private.pipelines group by name order by created_at desc` + ).then((result) => { + return result.output[0].records.rows.map((row: any) => { + return { + name: row[0], + version: nanoTimestampToUTCString(row[1]), + } + }) + }) +} + +export function getByName(name: string): Promise { + const sql = `select name, created_at, pipeline + from greptime_private.pipelines + where name = '${name}' + order by created_at desc + limit 1` + return runSQL(sql).then((result) => { + const row = result.output[0].records.rows[0] as any + return { + name: row[0], + version: nanoTimestampToUTCString(row[1]), + content: row[2], + } + }) +} + +export function del(name: string, version: string) { + const appStore = useAppStore() + return axios.delete(`${url}/${name}`, { + params: { + db: appStore.database, + version, + }, + }) +} + +export function debug(name: string, content: any) { + const appStore = useAppStore() + return axios.post(`${url}/dryrun?pipeline_name=${name}&db=${appStore.database}`, JSON.parse(content), { + headers: { + 'Content-Type': 'application/json', // Set Content-Type + }, + }) +} diff --git a/src/assets/style/select.less b/src/assets/style/select.less index c4917837..75944dc4 100644 --- a/src/assets/style/select.less +++ b/src/assets/style/select.less @@ -2,7 +2,7 @@ padding-right: 8px; padding-left: 8px; background: var(--grey-bg-color); - border-radius: 4px; + // border-radius: 4px; } .arco-select-view-single:hover { diff --git a/src/components/index.ts b/src/components/index.ts index 9ff841d1..164d8184 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -13,6 +13,7 @@ import Chart from './chart/index.vue' import Breadcrumb from './breadcrumb/index.vue' import 'echarts/lib/component/dataset' import 'echarts/lib/component/transform' +import YMLEditor from './yml-editor.vue' // Manually introduce ECharts modules to reduce packing size @@ -32,5 +33,6 @@ export default { install(Vue: App) { Vue.component('Chart', Chart) Vue.component('Breadcrumb', Breadcrumb) + Vue.component('YMLEditorSimple', YMLEditor) }, } diff --git a/src/components/time-select/index.vue b/src/components/time-select/index.vue index 4044c8e5..99a6da87 100644 --- a/src/components/time-select/index.vue +++ b/src/components/time-select/index.vue @@ -1,18 +1,20 @@ diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 25589d34..c1be021b 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -2,6 +2,7 @@ import dashboard from './en-US/dashboard' import settings from './en-US/settings' import menu from './en-US/menu' import playground from './en-US/playground' +import logquery from './en-US/logquery' export default { 'navbar.action.locale': 'Switch to English', @@ -13,4 +14,5 @@ export default { ...settings, ...menu, ...playground, + ...logquery, } diff --git a/src/locale/en-US/logquery.ts b/src/locale/en-US/logquery.ts new file mode 100644 index 00000000..e8c0f7db --- /dev/null +++ b/src/locale/en-US/logquery.ts @@ -0,0 +1,15 @@ +export default { + 'logquery.run': 'Run', + 'logquery.saveSql': 'Save SQL', + 'logquery.savedSql': 'Saved SQL', + 'logquery.showTables': 'Show tables', + 'logquery.results': 'Results', + 'logquery.hideStatChart': 'Hide chart', + 'logquery.showStatChart': 'Show chart', + 'logquery.live': 'Live', + 'logquery.columns': 'Columns', + 'logquery.wrapLines': 'Wrap lines', + 'logquery.nodata': 'No Data', + 'logquery.newer': 'Newer', + 'logquery.older': 'Older', +} diff --git a/src/locale/en-US/menu.ts b/src/locale/en-US/menu.ts index 134ec9d9..0ab4d896 100644 --- a/src/locale/en-US/menu.ts +++ b/src/locale/en-US/menu.ts @@ -27,4 +27,6 @@ export default { 'menu.tour.tables': 'Full list of tables and their metadata of your instance.', 'menu.tour.ingest': 'Ingest time-series data from the Ingest UI.', 'menu.tour.workbench': 'Build advanced dashboard using UI builder and YAML based configuration, all managed by git.', + 'menu.dashboard.logquery': 'Log Query', + 'menu.dashboard.logpipeline': 'Log Pipelines', } diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 2d22a0d0..a5c1000b 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -2,6 +2,7 @@ import dashboard from './zh-CN/dashboard' import menu from './zh-CN/menu' import playground from './zh-CN/playground' import settings from './zh-CN/settings' +import logquery from './zh-CN/logquery' export default { 'navbar.action.locale': '切换到中文', diff --git a/src/locale/zh-CN/logquery.ts b/src/locale/zh-CN/logquery.ts new file mode 100644 index 00000000..0dedad7f --- /dev/null +++ b/src/locale/zh-CN/logquery.ts @@ -0,0 +1,15 @@ +export default { + 'logquery.run': '查询', + 'logquery.saveSql': '保存为常用 SQL', + 'logquery.savedSql': '常用 SQL', + 'logquery.showTables': '所有表格', + 'logquery.results': '结果', + 'logquery.hideStatChart': '隐藏图表', + 'logquery.showStatChart': '显示图表', + 'logquery.live': '实时', + 'logquery.columns': '设置列', + 'logquery.wrapLines': '自动换行', + 'logquery.nodata': '暂无数据', + 'logquery.newer': '较新', + 'logquery.older': '较旧', +} diff --git a/src/locale/zh-CN/menu.ts b/src/locale/zh-CN/menu.ts index 896944bf..3ea4aadb 100644 --- a/src/locale/zh-CN/menu.ts +++ b/src/locale/zh-CN/menu.ts @@ -25,4 +25,6 @@ export default { 'menu.tour.query': '查看实例的完整表列表及其元数据。将 SQL 和 PromQL 查询放入我们的查询编辑器。', 'menu.tour.ingest': '从 Ingest UI 中摄取时间序列数据。', 'menu.tour.workbench': '使用 UI 构建器和基于 YAML 的配置构建高级仪表板,所有这些都由 git 管理。', + 'menu.dashboard.logquery': '日志查询', + 'menu.dashboard.logpipeline': '日志 Pipelines', } diff --git a/src/main.ts b/src/main.ts index 59d95e70..e339cc39 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,11 @@ import '@/api/interceptor' const app: App = createApp(Apps) +app.config.errorHandler = (err, vm, info) => { + console.error(err, info) + // Optionally show an error message to users +} + app.use(ArcoVue, {}) app.use(ArcoVueIcon) diff --git a/src/router/routes/modules/dashboard.ts b/src/router/routes/modules/dashboard.ts index 7ed5c368..35bc2b5b 100644 --- a/src/router/routes/modules/dashboard.ts +++ b/src/router/routes/modules/dashboard.ts @@ -89,6 +89,28 @@ const DASHBOARD: AppRouteRecordRaw = { }, ], }, + { + path: 'log-query', + component: () => import('@/views/dashboard/logs/query/index.vue'), + name: 'log-query', + meta: { + locale: 'menu.dashboard.logquery', + requiresAuth: false, + icon: 'log', + roles: ['admin', 'cloud'], + }, + }, + { + path: 'log-pipeline', + name: 'log-pipeline', + component: () => import('@/views/dashboard/logs/pipelines/index.vue'), + meta: { + locale: 'menu.dashboard.logpipeline', + requiresAuth: false, + roles: ['admin', 'cloud'], + icon: 'configuration', + }, + }, { path: 'status', name: 'status', diff --git a/src/store/modules/logquery/index.ts b/src/store/modules/logquery/index.ts new file mode 100644 index 00000000..20e5b335 --- /dev/null +++ b/src/store/modules/logquery/index.ts @@ -0,0 +1,293 @@ +import { defineStore } from 'pinia' +import dayjs from 'dayjs' +import { useLocalStorage, useStorage } from '@vueuse/core' +import editorAPI from '@/api/editor' +import { ColumnType, Condition, TSColumn } from '@/views/dashboard/logs/query/types' +import { toObj } from '@/views/dashboard/logs/query/until' +import { SchemaType } from '../code-run/types' + +type TableMap = { [key: string]: Array } + +export const typeMap = { + 'string': 'String', + 'int unsigned': 'Number', + 'bigint': 'Number', + 'int32': 'Number', + 'int64': 'Number', + 'double': 'Number', + 'float64': 'Number', + 'timestamp': 'Time', + 'timestamp(3)': 'Time', + 'timestamp(6)': 'Time', + 'timestamp(9)': 'Time', +} +type ColumnsMap = { + [key: string]: Array +} + +const useLogQueryStore = defineStore('logQuery', () => { + const sql = ref(``) + const editingSql = ref('') + // const columns = shallowRef>([]) + const displayedColumns = useStorage('logquery-table-column-visible', {}) + const rows = shallowRef>([]) + const tableMap = ref({}) + + const selectedRowKey = ref(-1) + const currRow = computed(() => { + if (selectedRowKey.value > -1) { + return rows.value[selectedRowKey.value] + } + return null + }) + + const rangeTime = ref>([]) + const time = ref(10) + const inputTableName = ref('') + + const columns = computed(() => { + if (!inputTableName.value) { + return [] + } + return tableMap.value[inputTableName.value] + }) + const queryNum = ref(0) + const tableIndex = ref(0) + const editorType = ref('builder') + const queryColumns = shallowRef>([]) + + const queryForm = reactive({ + conditions: [] as Array, + orderBy: 'DESC', + }) + + const limit = ref(1000) + const queryLoading = ref(false) + const refresh = ref(false) + // unix seconds + const unifiedRange = computed(() => { + if (time.value && time.value > 0) { + return [dayjs().subtract(time.value, 'minute').unix(), dayjs().unix()] + } + return rangeTime.value.map((v) => Number(v)) + }) + + const getRelativeRange = (multiple: number) => { + if (time.value && time.value > 0) { + return [`now() - Interval '${time.value}m'`, 'now()'] + } + return rangeTime.value.map((v) => Number(v) * multiple) + } + // multiple relative to s, one of 1000 1000 * 1000 1000 * 1000 * 1000 + type Multiple = 1000 | 1000000 | 1000000000 + const multipleRe = /timestamp\((\d)\)/ + const dataLoadFlag = ref(0) + const tsColumn = computed(() => { + const fields = tableMap.value[inputTableName.value] || [] + const field = fields.filter((column) => column.data_type.toLowerCase().indexOf('timestamp') > -1)[0] + if (!field) { + return null + } + const timescale = multipleRe.exec(field.data_type) + if (!timescale) return null + return { + multiple: (1000 ** (Number(timescale[1]) / 3)) as Multiple, + ...field, + } + }) + + const query = () => { + queryLoading.value = true + return editorAPI + .runSQL(sql.value) + .then((result) => { + // columns.value = result.output[0].records.schema.column_schemas + queryColumns.value = result.output[0].records.schema.column_schemas + rows.value = result.output[0].records.rows.map((row, index) => { + return toObj(row, queryColumns.value, index, tsColumn.value) + }) + }) + .finally(() => { + queryLoading.value = false + dataLoadFlag.value = Math.random() + }) + } + + const getColumnByName = (name: string) => { + const index = columns.value.findIndex((column) => column.name === name) + return columns.value[index] + } + + const getColumn = (name: string) => { + const allColumns = tableMap.value[inputTableName.value] + const index = allColumns.findIndex((column) => column.name === name) + return allColumns[index] + } + + const mergeColumn = useLocalStorage('logquery-merge-column', true) + const showKeys = useLocalStorage('logquery-show-keys', true) + + function getSchemas() { + return editorAPI + .runSQL( + `SELECT + table_name, + table_schema, + column_name, + data_type + FROM + information_schema.columns + Where table_schema != 'information_schema' + ORDER BY + table_name + ` + ) + .then((result) => { + const { rows: schemaRows } = result.output[0].records + const tmp: TableMap = {} + for (let i = 0; i < schemaRows.length; i += 1) { + const row = schemaRows[i] as string[] + const tableName = row[0] + if (!tmp[tableName]) { + tmp[tableName] = [] + } + tmp[tableName].push({ + name: row[2], + data_type: row[3], + label: row[2], + }) + } + tableMap.value = tmp + }) + } + function escapeSqlString(value: string) { + if (typeof value !== 'string') { + return value // Only escape if it's a string + } + + // Replace common SQL special characters with their escaped versions + return value + .replace(/\\/g, '\\\\') // Escape backslashes + .replace(/'/g, "''") // Escape single quotes by doubling + .replace(/"/g, '\\"') // Escape double quotes (if needed) + .replace(/\n/g, '\\n') // Escape newline + .replace(/\r/g, '\\r') // Escape carriage return + } + + function singleCondition(condition: Condition) { + const column = condition.field + const columnType = typeMap[column.data_type as keyof typeof typeMap] + if (columnType === 'Number' || columnType === 'Time') { + return `${condition.field.name} ${condition.op} ${condition.value}` + } + if (condition.op === 'like') { + // return `MATCHES(${condition.field.name},'"${escapeSqlString(condition.value)}"')` + return `${condition.field.name} like '%${condition.value}%'` + } + if (['contains', 'not contains', 'match sequence'].indexOf(condition.op) > -1) { + let val = escapeSqlString(condition.value) + if (condition.op === 'not contains') { + val = `-"${val}"` + } else if (condition.op === 'match sequence') { + val = `"${val}"` + } + return `MATCHES(${condition.field.name},'"${val}"')` + } + return `${condition.field.name} ${condition.op} '"${escapeSqlString(condition.value)}"'` + } + + function buildCondition() { + const where = [] + const conditions = queryForm.conditions.filter((v) => v.field && v.op && v.value) + for (let i = 0; i < conditions.length; i += 1) { + const condition = conditions[i] + if (i === 0) { + where.push(singleCondition(condition)) + continue + } + if (condition.rel === 'and') { + where.push(` AND ${singleCondition(condition)}`) + } else { + where.push(` OR ${singleCondition(condition)}`) + } + } + if (unifiedRange.value.length === 2) { + if (tsColumn.value) { + const { multiple } = tsColumn.value + const [start, end] = getRelativeRange(multiple) + let prefix = ' AND' + if (!where.length) { + prefix = '' + } + where.push(`${prefix} ${tsColumn.value.name} >= ${start} AND ${tsColumn.value.name} < ${end}`) + } + } + return where + } + + watch( + [queryForm, unifiedRange, limit], + () => { + if (!inputTableName.value) { + return + } + if (editorType.value !== 'builder') { + return + } + let str = `SELECT * FROM ${inputTableName.value}` + const where = buildCondition() + if (where.length) { + str += ` WHERE ${where.join('')}` + } + if (tsColumn.value) { + str += ` ORDER BY ${tsColumn.value?.name} ${queryForm.orderBy}` + } + str += ` LIMIT ${limit.value}` + editingSql.value = str + }, + { + immediate: true, + deep: true, + } + ) + + watch(columns, () => { + if (!displayedColumns.value[inputTableName.value]) { + displayedColumns.value[inputTableName.value] = columns.value.map((c) => c.name) + } + }) + + return { + sql, + query, + rows, + columns, + getColumnByName, + currRow, + selectedRowKey, + rangeTime, + inputTableName, + tsColumn, + time, + unifiedRange, + queryNum, + getSchemas, + tableMap, + getRelativeRange, + editorType, + queryForm, + buildCondition, + getColumn, + editingSql, + displayedColumns, + limit, + queryLoading, + refresh, + tableIndex, + mergeColumn, + dataLoadFlag, + showKeys, + queryColumns, + } +}) +export default useLogQueryStore diff --git a/src/views/dashboard/config.ts b/src/views/dashboard/config.ts index 19ef8543..6dabfd9f 100644 --- a/src/views/dashboard/config.ts +++ b/src/views/dashboard/config.ts @@ -167,3 +167,29 @@ export const tableSteps: DriveStep[] = [ const navbarStepElements = navbarSteps.map((step) => step.element) const tableStepElements = tableSteps.map((step) => step.element) + +export const relativeTimeOptions = [ + { value: 1, label: 'Last 1 minute' }, + { value: 10, label: 'Last 10 minutes' }, + { value: 30, label: 'Last 30 minutes' }, + { value: 60, label: 'Last 1 hour' }, + { value: 180, label: 'Last 3 hours' }, + { value: 360, label: 'Last 6 hours' }, + { value: 720, label: 'Last 12 hours' }, + { value: 1440, label: 'Last 24 hours' }, + { value: 2880, label: 'Last 2 days' }, + { value: 10080, label: 'Last 7 days' }, +] + +export const relativeTimeMap: { [key: number]: string } = { + 1: 'Last 1 minute', + 10: 'Last 10 minutes', + 30: 'Last 30 minutes', + 60: 'Last 1 hour', + 180: 'Last 3 hours', + 360: 'Last 6 hours', + 720: 'Last 12 hours', + 1440: 'Last 24 hours', + 2880: 'Last 2 days', + 10080: 'Last 7 days', +} diff --git a/src/views/dashboard/logs/pipelines/PipeFileView.vue b/src/views/dashboard/logs/pipelines/PipeFileView.vue new file mode 100644 index 00000000..7840a937 --- /dev/null +++ b/src/views/dashboard/logs/pipelines/PipeFileView.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/src/views/dashboard/logs/pipelines/index.vue b/src/views/dashboard/logs/pipelines/index.vue new file mode 100644 index 00000000..cd983a81 --- /dev/null +++ b/src/views/dashboard/logs/pipelines/index.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/views/dashboard/logs/query/CountChart.vue b/src/views/dashboard/logs/query/CountChart.vue new file mode 100644 index 00000000..4cb16427 --- /dev/null +++ b/src/views/dashboard/logs/query/CountChart.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/src/views/dashboard/logs/query/ExportLog.vue b/src/views/dashboard/logs/query/ExportLog.vue new file mode 100644 index 00000000..8237df7e --- /dev/null +++ b/src/views/dashboard/logs/query/ExportLog.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/views/dashboard/logs/query/FormView.vue b/src/views/dashboard/logs/query/FormView.vue new file mode 100644 index 00000000..b4a81f03 --- /dev/null +++ b/src/views/dashboard/logs/query/FormView.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/views/dashboard/logs/query/InputEditor.vue b/src/views/dashboard/logs/query/InputEditor.vue new file mode 100644 index 00000000..bf1f0b76 --- /dev/null +++ b/src/views/dashboard/logs/query/InputEditor.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/src/views/dashboard/logs/query/JSONView.vue b/src/views/dashboard/logs/query/JSONView.vue new file mode 100644 index 00000000..dc104b91 --- /dev/null +++ b/src/views/dashboard/logs/query/JSONView.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/views/dashboard/logs/query/LogDetail.vue b/src/views/dashboard/logs/query/LogDetail.vue new file mode 100644 index 00000000..14f943cd --- /dev/null +++ b/src/views/dashboard/logs/query/LogDetail.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/views/dashboard/logs/query/Pagination.vue b/src/views/dashboard/logs/query/Pagination.vue new file mode 100644 index 00000000..3b58182b --- /dev/null +++ b/src/views/dashboard/logs/query/Pagination.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/src/views/dashboard/logs/query/SQLBuilder.vue b/src/views/dashboard/logs/query/SQLBuilder.vue new file mode 100644 index 00000000..642dc9db --- /dev/null +++ b/src/views/dashboard/logs/query/SQLBuilder.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/src/views/dashboard/logs/query/SavedQuery.vue b/src/views/dashboard/logs/query/SavedQuery.vue new file mode 100644 index 00000000..6981e976 --- /dev/null +++ b/src/views/dashboard/logs/query/SavedQuery.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/views/dashboard/logs/query/TableData.vue b/src/views/dashboard/logs/query/TableData.vue new file mode 100644 index 00000000..86272c39 --- /dev/null +++ b/src/views/dashboard/logs/query/TableData.vue @@ -0,0 +1,276 @@ + + + + + diff --git a/src/views/dashboard/logs/query/Toolbar.vue b/src/views/dashboard/logs/query/Toolbar.vue new file mode 100644 index 00000000..646b276e --- /dev/null +++ b/src/views/dashboard/logs/query/Toolbar.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/src/views/dashboard/logs/query/index.vue b/src/views/dashboard/logs/query/index.vue new file mode 100644 index 00000000..4531d6c3 --- /dev/null +++ b/src/views/dashboard/logs/query/index.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/src/views/dashboard/logs/query/types.ts b/src/views/dashboard/logs/query/types.ts new file mode 100644 index 00000000..2d9c3380 --- /dev/null +++ b/src/views/dashboard/logs/query/types.ts @@ -0,0 +1,16 @@ +export type ColumnType = { + name: string + data_type: string + label: string +} + +export type Condition = { + field: ColumnType + op: string + value: string + rel: 'and' | 'or' +} + +export type TSColumn = ColumnType & { + multiple: number +} diff --git a/src/views/dashboard/logs/query/until.ts b/src/views/dashboard/logs/query/until.ts new file mode 100644 index 00000000..e1d07c28 --- /dev/null +++ b/src/views/dashboard/logs/query/until.ts @@ -0,0 +1,233 @@ +import dayjs from 'dayjs' +import type { SchemaType } from '@/store/modules/code-run/types' +import { TSColumn } from './types' + +function findWhereClausePosition(sql: string) { + // Normalize case for easier comparison + const upperSql = sql.toUpperCase() + + // Find the first keyword after FROM where WHERE should go before + const groupByIndex = upperSql.indexOf('GROUP BY') + const orderByIndex = upperSql.indexOf('ORDER BY') + const limitIndex = upperSql.indexOf('LIMIT') + + // Find the position to insert WHERE clause: + // Insert before GROUP BY, ORDER BY, or LIMIT, whichever comes first + let insertPosition = upperSql.length // Default to end of the query if no keywords + + if (groupByIndex !== -1 && groupByIndex < insertPosition) { + insertPosition = groupByIndex + } + + if (orderByIndex !== -1 && orderByIndex < insertPosition) { + insertPosition = orderByIndex + } + + if (limitIndex !== -1 && limitIndex < insertPosition) { + insertPosition = limitIndex + } + + return insertPosition +} + +export function parseWhereCondition(sql: string) { + // 正则表达式:匹配字段、操作符和值 + const conditionRegex = /(\w+)\s*(=|>|<|>=|<=|!=|like)\s*('(?:[^']|\\')*'|\d+)/g + // 解析所有条件 + const conditions = [] + let match = conditionRegex.exec(sql) + while (match !== null) { + const field = match[1] + const operator = match[2] + let value = match[3] + + // 去除引号(如果值是字符串) + if (value.startsWith("'") && value.endsWith("'")) { + value = value.slice(1, -1) + } + conditions.push({ field, operator, value }) + match = conditionRegex.exec(sql) + } + return conditions +} + +export function getWhereClause(sql: string) { + // This regex will match "WHERE" in any case and capture everything until GROUP BY, ORDER BY, LIMIT, or end of the string. + const whereMatch = sql.match(/\bWHERE\b([\s\S]*?)(\bGROUP BY\b|\bORDER BY\b|\bLIMIT\b|$)/i) + return whereMatch ? whereMatch[1].trim() : '' +} + +export function addTsCondition(sql: string, column: string, start: number | string, end: number | string) { + const upperSql = sql.toUpperCase() + let whereIndex = upperSql.indexOf('WHERE') + let replaceSql = sql.trim() + if (whereIndex > -1) { + if (end) { + const lessRegex = new RegExp(`(${column}\\s*[<=]*\\s*)(\\d+|\\s*now\\(\\))`, 'g') + if (lessRegex.test(replaceSql)) { + replaceSql = replaceSql.replace(lessRegex, (match, prefix) => { + return `${prefix}${end}` + }) + } else { + replaceSql = `${replaceSql.slice(0, whereIndex + 5)} ${column} < ${end} and ${replaceSql.slice(whereIndex + 5)}` + } + } + + if (start) { + const moreRegex = new RegExp(`(${column}\\s*[>=]*\\s*)(\\d+|[\\S\\s]*Interval '\\d+\\s*m')`, 'g') + // Use the regex to replace the matched number with `newValue` + if (moreRegex.test(replaceSql)) { + replaceSql = replaceSql.replace(moreRegex, (match, prefix) => { + return `${prefix}${start}` + }) + } else { + replaceSql = `${replaceSql.slice(0, whereIndex + 5)} ${column} >= ${start} and ${replaceSql.slice( + whereIndex + 5 + )}` + } + } + + return replaceSql + } + whereIndex = findWhereClausePosition(sql) + return `${sql.slice(0, whereIndex)} where ${column} >= ${start} and ${column} < ${end} ${sql.slice(whereIndex)}` +} + +export const TableNameReg = /(?<=from|FROM)\s+([^\s;]+)/ +export function parseTable(sql: string) { + const result = sql.match(TableNameReg) + if (result && result.length) { + const arr = result[1].trim().split('.') + return arr[arr.length - 1] + } + return '' +} + +export function parseTimeRange(sql: string, tsColumn: string, multiple: number): string[] | number { + const lessRegex = new RegExp(`${tsColumn}\\s*[<=]*\\s*(\\d+)`, 'g') + const moreRegex = new RegExp(`${tsColumn}\\s*[>=]*\\s*(\\d+|[\\S\\s]*Interval '\\d+\\s*m')`, 'g') + const intervalRe = /Interval '(\d+)m'/ + const parseResult = [] + const moreResult = moreRegex.exec(sql) + if (moreResult) { + const interval = intervalRe.exec(moreResult[1]) + if (interval) { + return Number(interval[1]) + } + parseResult.push(Number(moreResult[1]) / multiple) + } + const lessResult = lessRegex.exec(sql) + if (lessResult) { + parseResult.push(Number(lessResult[1]) / multiple) + } + + return parseResult.map((v) => String(v)) +} + +export function calculateInterval(start: number, end: number) { + const startTime = new Date(start).getTime() + const endTime = new Date(end).getTime() + const totalSeconds = Math.floor((endTime - startTime) / 120) * 120 + // Calculate interval in seconds + return Math.max(1, Math.floor(totalSeconds / 120)) +} + +// use basetime to sync with real time +// start,end unix +// return unix +export function generateTimeRange( + start: number, + end: number, + intervalSeconds: number, + basetime: number, + multiple: number +) { + // return [] + if (start === 0) { + return [] + } + start *= multiple + end *= multiple + if (start === end) { + return [start, end] + } + let current = basetime > start ? basetime : start + const result = [] + while (current >= start) { + result.push(current) + current -= intervalSeconds * multiple + } + current = basetime + intervalSeconds * multiple + while (current <= end) { + result.push(current) + current += intervalSeconds * multiple + } + return result +} + +export function toMs(time: number, multiple: number) { + const timescale = (String(multiple).length - 1) / 3 + if (timescale === 0) { + return time * 1000 + } + if (timescale === 1) { + return time + } + return time / 1000 ** (timescale - 1) +} + +export const TimeTypes = { + TimestampSecond: 1, + TimestampMillisecond: 1000, + TimestampMicroSecond: 1000 * 1000, + TimestampNanosecond: 1000 * 1000 * 1000, +} + +export type TimeType = keyof typeof TimeTypes + +export function toDateStr(time: number, multiple: number, format?: string) { + // const multiple = TimeTypes[type] + const ms = toMs(time, multiple) + return dayjs(ms).format(format || 'MM-DD HH:mm:ss.SSS') +} + +const LIMIT_RE = /LIMIT\s+(\d+)/ + +export function processSQL(sql: string, tsColumn: string | undefined, limit: number) { + const upperSql = sql.toUpperCase() + if (upperSql.indexOf('ORDER BY') === -1 && tsColumn) { + sql += ` ORDER BY ${tsColumn} desc` + } + const limitResult = LIMIT_RE.exec(upperSql) + if (!limitResult) { + sql += ` LIMIT ${limit}` + } else { + sql.replace(LIMIT_RE, `LIMIT ${limit}`) + } + return sql +} + +export function parseLimit(sql: string) { + const upperSql = sql.toUpperCase() + const limitResult = LIMIT_RE.exec(upperSql) + if (limitResult) { + return Number(limitResult[1]) + } + return 1000 +} + +export function parseOrderBy(sql: string) { + const match = sql.match(/ORDER BY\s+\w+\s+(desc|asc)+/i) + return match ? match[1] : null +} + +export function toObj(row: any, columns: Array, index: number, tsColumn: TSColumn) { + const obj = {} as any + obj.index = index + for (let i = 0; i < columns.length; i += 1) { + const column = columns[i] + obj[column.name] = row[i] + } + obj.key = index + return obj +} diff --git a/tsconfig.json b/tsconfig.json index 13548f1e..1b176502 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,8 @@ "resolveJsonModule": true, "esModuleInterop": true, "baseUrl": ".", + "noImplicitAny": false, + "strictNullChecks": false, "paths": { "@/*": ["src/*"] },