From e077a4201e480cd2da063ac333b28f3d19798af9 Mon Sep 17 00:00:00 2001 From: scl Date: Tue, 10 Dec 2024 19:06:56 +0800 Subject: [PATCH 1/8] feat: new stat chart,editing table name,quick query --- src/store/modules/logquery/index.ts | 49 ++++++-- .../dashboard/logs/query/ChartContainer.vue | 50 ++++++++ src/views/dashboard/logs/query/CountChart.vue | 2 +- src/views/dashboard/logs/query/ExportLog.vue | 2 - .../dashboard/logs/query/FunnelChart.vue | 115 ++++++++++++++++++ .../dashboard/logs/query/InputEditor.vue | 12 -- src/views/dashboard/logs/query/SQLBuilder.vue | 36 ++---- src/views/dashboard/logs/query/TableData.vue | 87 +++++++++++-- src/views/dashboard/logs/query/Toolbar.vue | 39 +++--- src/views/dashboard/logs/query/index.vue | 4 +- src/views/dashboard/logs/query/until.ts | 18 +-- 11 files changed, 324 insertions(+), 90 deletions(-) create mode 100644 src/views/dashboard/logs/query/ChartContainer.vue create mode 100644 src/views/dashboard/logs/query/FunnelChart.vue diff --git a/src/store/modules/logquery/index.ts b/src/store/modules/logquery/index.ts index 97d99d68..e896006b 100644 --- a/src/store/modules/logquery/index.ts +++ b/src/store/modules/logquery/index.ts @@ -43,7 +43,8 @@ const useLogQueryStore = defineStore('logQuery', () => { const rangeTime = ref>([]) const time = ref(10) - const inputTableName = ref('') + const inputTableName = ref('') // table after query + const editingTableName = ref('') // table in editing const columns = computed(() => { if (!inputTableName.value) { @@ -82,8 +83,10 @@ const useLogQueryStore = defineStore('logQuery', () => { type Multiple = 1000 | 1000000 | 1000000000 const multipleRe = /timestamp\((\d)\)/ const dataLoadFlag = ref(0) - const tsColumn = computed(() => { - const fields = tableMap.value[inputTableName.value] || [] + const tsColumn = shallowRef() + + const getTsColumn = (tableName: string) => { + const fields = tableMap.value[tableName] || [] const field = fields.filter((column) => column.data_type.toLowerCase().indexOf('timestamp') > -1)[0] if (!field) { return null @@ -94,7 +97,7 @@ const useLogQueryStore = defineStore('logQuery', () => { multiple: (1000 ** (Number(timescale[1]) / 3)) as Multiple, ...field, } - }) + } const query = () => { queryLoading.value = true @@ -204,12 +207,12 @@ const useLogQueryStore = defineStore('logQuery', () => { let val = escapeSqlString(condition.value) if (condition.op === 'not contains') { val = `-"${val}"` - } else if (condition.op === 'match sequence') { + } else if (condition.op === 'contains') { val = `"${val}"` } - return `MATCHES(${condition.field.name},'"${val}"')` + return `MATCHES(${condition.field.name},'${val}')` } - return `${condition.field.name} ${condition.op} '"${escapeSqlString(condition.value)}"'` + return `${condition.field.name} ${condition.op} '${escapeSqlString(condition.value)}'` } function buildCondition() { @@ -242,15 +245,16 @@ const useLogQueryStore = defineStore('logQuery', () => { } watch( - [queryForm, unifiedRange, limit], + [queryForm, unifiedRange, limit, editingTableName], () => { - if (!inputTableName.value) { + if (!editingTableName.value) { return } if (editorType.value !== 'builder') { return } - let str = `SELECT * FROM ${inputTableName.value}` + tsColumn.value = getTsColumn(editingTableName.value) + let str = `SELECT * FROM ${editingTableName.value}` const where = buildCondition() if (where.length) { str += ` WHERE ${where.join('')}` @@ -273,13 +277,34 @@ const useLogQueryStore = defineStore('logQuery', () => { } }) + type TypeKey = keyof typeof typeMap + const opMap = { + String: ['=', 'contains', 'not contains', '!=', 'like'], + Number: ['=', '!=', '>', '>=', '<', '<='], + Time: ['>', '>=', '<', '<='], + } + type OpKey = keyof typeof opMap + + function getOpByField(field: string): string[] { + const fields = tableMap.value[inputTableName.value] + const index = fields.findIndex((f) => f.name === field) + if (index === -1) { + return [] + } + const type = fields[index].data_type as TypeKey + const opKey = typeMap[type] as OpKey + return opMap[opKey] || [] + } + function reset() { + editingTableName.value = '' inputTableName.value = '' sql.value = '' editingSql.value = '' queryForm.conditions = [] rows.value = [] } + return { sql, query, @@ -290,6 +315,7 @@ const useLogQueryStore = defineStore('logQuery', () => { selectedRowKey, rangeTime, inputTableName, + editingTableName, tsColumn, time, unifiedRange, @@ -311,7 +337,10 @@ const useLogQueryStore = defineStore('logQuery', () => { dataLoadFlag, showKeys, queryColumns, + getOpByField, reset, + getTsColumn, } }) + export default useLogQueryStore diff --git a/src/views/dashboard/logs/query/ChartContainer.vue b/src/views/dashboard/logs/query/ChartContainer.vue new file mode 100644 index 00000000..b97864f9 --- /dev/null +++ b/src/views/dashboard/logs/query/ChartContainer.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/views/dashboard/logs/query/CountChart.vue b/src/views/dashboard/logs/query/CountChart.vue index 4cb16427..0832b49b 100644 --- a/src/views/dashboard/logs/query/CountChart.vue +++ b/src/views/dashboard/logs/query/CountChart.vue @@ -114,7 +114,7 @@ VCharts( }) const countSql = computed(() => { - if (!tsColumn.value) { + if (!tsColumn.value || !inputTableName.value) { return '' } diff --git a/src/views/dashboard/logs/query/ExportLog.vue b/src/views/dashboard/logs/query/ExportLog.vue index 8237df7e..3c37af92 100644 --- a/src/views/dashboard/logs/query/ExportLog.vue +++ b/src/views/dashboard/logs/query/ExportLog.vue @@ -2,7 +2,6 @@ a-button(size="small" type="text" @click="exportSql") | Export as csv - - diff --git a/src/views/dashboard/logs/query/FunnelChart.vue b/src/views/dashboard/logs/query/FunnelChart.vue new file mode 100644 index 00000000..0f10c1fa --- /dev/null +++ b/src/views/dashboard/logs/query/FunnelChart.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/src/views/dashboard/logs/query/InputEditor.vue b/src/views/dashboard/logs/query/InputEditor.vue index bf1f0b76..b2ce03d7 100644 --- a/src/views/dashboard/logs/query/InputEditor.vue +++ b/src/views/dashboard/logs/query/InputEditor.vue @@ -61,18 +61,6 @@ }) }) - // parse table name - watch( - editingSql, - () => { - inputTableName.value = parseTable(editingSql.value) - limit.value = parseLimit(editingSql.value) - }, - { - immediate: true, - } - ) - const customSelectionTheme = EditorView.theme({ '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': { diff --git a/src/views/dashboard/logs/query/SQLBuilder.vue b/src/views/dashboard/logs/query/SQLBuilder.vue index 642dc9db..cf30696c 100644 --- a/src/views/dashboard/logs/query/SQLBuilder.vue +++ b/src/views/dashboard/logs/query/SQLBuilder.vue @@ -7,12 +7,11 @@ a-form( ) a-form-item(field="table" label="Table") a-select( - v-model="inputTableName" + v-model="editingTableName" style="width: auto" placeholder="Select Table" :options="tables" :allow-search="true" - :trigger-props="{ autoFitPopupMinWidth: true }" @change="handleTableChange" ) a-form-item(label="Where" field="conditions") @@ -28,7 +27,7 @@ a-form( @change="() => handleFieldChange(condition)" ) a-option( - v-for="column in tableMap[inputTableName]" + v-for="column in tableMap[editingTableName]" :key="column.label" :label="column.name" :value="column" @@ -61,7 +60,15 @@ a-form( import useLogQueryStore, { typeMap } from '@/store/modules/logquery' import type { Condition } from '@/views/dashboard/logs/query/types' - const { tableMap, inputTableName, tsColumn, queryForm: form, limit } = storeToRefs(useLogQueryStore()) + const { + tableMap, + inputTableName, + tsColumn, + queryForm: form, + limit, + editingTableName, + } = storeToRefs(useLogQueryStore()) + const { getOpByField } = useLogQueryStore() // inputTableName.value = 'syslog' const tables = computed>(() => { @@ -69,7 +76,7 @@ a-form( }) function addCondition() { - if (!inputTableName.value) { + if (!editingTableName.value) { return } form.value.conditions.push({ @@ -80,25 +87,6 @@ a-form( }) } - type TypeKey = keyof typeof typeMap - const opMap = { - String: ['contains', 'not contains', '=', '!=', 'like'], - Number: ['=', '!=', '>', '>=', '<', '<='], - Time: ['>', '>=', '<', '<='], - } - type OpKey = keyof typeof opMap - - function getOpByField(field: string): string[] { - const fields = tableMap.value[inputTableName.value] - const index = fields.findIndex((f) => f.name === field) - if (index === -1) { - return [] - } - const type = fields[index].data_type as TypeKey - const opKey = typeMap[type] as OpKey - return opMap[opKey] || [] - } - function removeCondition(index: number) { form.value.conditions.splice(index, 1) } diff --git a/src/views/dashboard/logs/query/TableData.vue b/src/views/dashboard/logs/query/TableData.vue index 86272c39..bab09765 100644 --- a/src/views/dashboard/logs/query/TableData.vue +++ b/src/views/dashboard/logs/query/TableData.vue @@ -9,8 +9,7 @@ :pagination="false" :row-selection="rowSelection" :bordered="false" - :class="{ wrap_table: wrapLine, single_column: mergeColumn, multiple_column: !mergeColumn }" - @row-click="handleRowClick" + :class="{ wrap_table: wrapLine, single_column: mergeColumn, multiple_column: !mergeColumn, builder_type: editorType === 'builder' }" ) template(#columns) template(v-for="column in tableColumns") @@ -21,7 +20,7 @@ :header-cell-style="column.headerCellStyle" ) template(#cell="{ record }") - | {{ renderTs(record, column.dataIndex) }} + span(style="cursor: pointer" @click="() => handleTsClick(record)") {{ renderTs(record, column.dataIndex) }} template(#title) a-tooltip( placement="top" @@ -38,30 +37,46 @@ :header-cell-style="column.headerCellStyle" ) template(#cell="{ record }") - span.entity-field(v-for="field in getEntryFields(record)") + span.entity-field.clickable( + v-for="field in getEntryFields(record)" + @click="(event) => handleContextMenu(record, field[0], event)" + ) template(v-if="showKeys") span(style="color: var(--color-text-3)") | {{ field[0] }}: | {{ field[1] }} template(v-else) | {{ field[1] }} - a-table-column( + a-table-column.clickable( v-else :data-index="column.dataIndex" :title="column.title" :header-cell-style="column.headerCellStyle" ) + template(#cell="{ record }") + span.clickable(@click="(event) => handleContextMenu(record, column.dataIndex, event)") {{ record[column.dataIndex] }} LogDetail(v-model:visible="detailVisible") + a-dropdown#td-context( + v-model:popup-visible="contextMenuVisible" + trigger="custom" + :style="{ top: `${contextMenuPosition.y}px`, left: `${contextMenuPosition.x}px` }" + @clickoutside="hideContextMenu" + @select="handleMenuClick" + ) + template(#content) + a-doption(value="copy") Copy Field Value + a-dsubmenu(trigger="hover") Filter + template(#content) + a-doption(v-for="op in filterOptions" :value="`filter_${op}`") {{ op }} value diff --git a/src/views/dashboard/logs/query/Toolbar.vue b/src/views/dashboard/logs/query/Toolbar.vue index 646b276e..c0367f7b 100644 --- a/src/views/dashboard/logs/query/Toolbar.vue +++ b/src/views/dashboard/logs/query/Toolbar.vue @@ -58,7 +58,7 @@ import { watchOnce, useStorage } from '@vueuse/core' import useLogQueryStore from '@/store/modules/logquery' import { relativeTimeMap, relativeTimeOptions } from '../../config' - import { parseTimeRange, processSQL } from './until' + import { parseTimeRange, processSQL, parseTable, parseLimit, addTsCondition } from './until' import SavedQuery from './SavedQuery.vue' import ExportLog from './ExportLog.vue' @@ -75,19 +75,9 @@ limit, queryLoading, refresh, + editingTableName, } = storeToRefs(useLogQueryStore()) - // parse time range when ts column confirmed - watchOnce(tsColumn, () => { - if (tsColumn.value && editorType.value === 'text') { - time.value = 0 - const parseResult = parseTimeRange(sqlData.value, tsColumn.value.name, tsColumn.value.multiple) - if (Array.isArray(parseResult) && parseResult.length === 2) { - rangeTime.value = parseResult - } else { - time.value = parseResult as number - } - } - }) + const { getRelativeRange, getTsColumn } = useLogQueryStore() let refreshTimeout = -1 function mayRefresh() { @@ -108,13 +98,24 @@ // const queryLoading = ref(false) function handleQuery() { - if (!inputTableName.value) { + if (!editingTableName.value) { return } - sqlData.value = editingSql.value + inputTableName.value = editingTableName.value + if (editorType.value === 'text') { - sqlData.value = processSQL(sqlData.value, tsColumn.value?.name, limit.value) + inputTableName.value = parseTable(editingSql.value) + + limit.value = parseLimit(editingSql.value) + tsColumn.value = getTsColumn(inputTableName.value) + if (tsColumn.value) { + const { multiple } = tsColumn.value + const [start, end] = getRelativeRange(multiple) + editingSql.value = addTsCondition(editingSql.value, tsColumn.value.name, start, end) + } + editingSql.value = processSQL(editingSql.value, tsColumn.value?.name, limit.value) } + sqlData.value = editingSql.value if (refresh.value) { mayRefresh() } else { @@ -167,10 +168,4 @@ :deep(.arco-btn-text[type='button']) { color: var(--color-text-2); } - :deep(.arco-radio-group-button) { - background-color: var(--color-fill-3); - } - :deep(.arco-radio-button.arco-radio-checked) { - color: var(--color-primary); - } diff --git a/src/views/dashboard/logs/query/index.vue b/src/views/dashboard/logs/query/index.vue index d48cc938..cf4e4c8f 100644 --- a/src/views/dashboard/logs/query/index.vue +++ b/src/views/dashboard/logs/query/index.vue @@ -7,7 +7,7 @@ style="padding: 10px 20px; border: 1px solid var(--color-neutral-3); border-top: none; background-color: var(--color-bg-2)" ) InputEditor(v-else) - CountChart.block( + ChartContainer.block( v-if="showChart" style="margin: 5px 0 0; padding: 10px 0; background-color: var(--color-bg-2); border: 1px solid var(--color-neutral-3); flex-shrink: 0" ) @@ -55,7 +55,7 @@ import useLogQueryStore from '@/store/modules/logquery' import InputEditor from './InputEditor.vue' import LogTableData from './TableData.vue' - import CountChart from './CountChart.vue' + import ChartContainer from './ChartContainer.vue' import SQLBuilder from './SQLBuilder.vue' import Toolbar from './Toolbar.vue' import Pagination from './Pagination.vue' diff --git a/src/views/dashboard/logs/query/until.ts b/src/views/dashboard/logs/query/until.ts index e1d07c28..95695865 100644 --- a/src/views/dashboard/logs/query/until.ts +++ b/src/views/dashboard/logs/query/until.ts @@ -194,17 +194,21 @@ export function toDateStr(time: number, multiple: number, format?: string) { const LIMIT_RE = /LIMIT\s+(\d+)/ export function processSQL(sql: string, tsColumn: string | undefined, limit: number) { + let orderByClause = '' const upperSql = sql.toUpperCase() if (upperSql.indexOf('ORDER BY') === -1 && tsColumn) { - sql += ` ORDER BY ${tsColumn} desc` + orderByClause = ` ORDER BY ${tsColumn} DESC ` } - const limitResult = LIMIT_RE.exec(upperSql) - if (!limitResult) { - sql += ` LIMIT ${limit}` - } else { - sql.replace(LIMIT_RE, `LIMIT ${limit}`) + // Check if LIMIT exists + const limitMatch = sql.match(/\bLIMIT\b/i) + + if (limitMatch) { + // Insert ORDER BY before LIMIT + const { index } = limitMatch + return `${sql.slice(0, index - 1)}${orderByClause} ${sql.slice(index)}` } - return sql + + return `${sql}${orderByClause}LIMIT ${limit}`.trim() } export function parseLimit(sql: string) { From 2917a08d0dda8fe871eb681f341b3f0a86bed162 Mon Sep 17 00:00:00 2001 From: scl Date: Tue, 10 Dec 2024 19:39:31 +0800 Subject: [PATCH 2/8] fix: reset chart when table change --- src/views/dashboard/logs/query/ChartContainer.vue | 6 +++++- src/views/dashboard/logs/query/Toolbar.vue | 12 +++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/views/dashboard/logs/query/ChartContainer.vue b/src/views/dashboard/logs/query/ChartContainer.vue index b97864f9..87835f5a 100644 --- a/src/views/dashboard/logs/query/ChartContainer.vue +++ b/src/views/dashboard/logs/query/ChartContainer.vue @@ -23,7 +23,7 @@ a-card import FunnelChart from './FunnelChart.vue' const currChart = ref('count') - const { columns } = storeToRefs(useLogQueryStore()) + const { columns, inputTableName } = storeToRefs(useLogQueryStore()) const frequencyField = ref('') const filterFields = computed(() => columns.value.filter((column) => column.data_type === 'string').map((column) => column.name) @@ -40,6 +40,10 @@ a-card } return 'Frequency Distribution' }) + + watch(inputTableName, () => { + currChart.value = 'count' + }) From 360cd7f0de54942ddb3833cd6f1e213bca11f896 Mon Sep 17 00:00:00 2001 From: scl Date: Wed, 11 Dec 2024 10:12:21 +0800 Subject: [PATCH 4/8] fix: sql code --- src/views/dashboard/logs/query/until.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/dashboard/logs/query/until.ts b/src/views/dashboard/logs/query/until.ts index 95695865..080b59c1 100644 --- a/src/views/dashboard/logs/query/until.ts +++ b/src/views/dashboard/logs/query/until.ts @@ -90,7 +90,7 @@ export function addTsCondition(sql: string, column: string, start: number | stri return replaceSql } whereIndex = findWhereClausePosition(sql) - return `${sql.slice(0, whereIndex)} where ${column} >= ${start} and ${column} < ${end} ${sql.slice(whereIndex)}` + return `${sql.slice(0, whereIndex - 1)} WHERE ${column} >= ${start} and ${column} < ${end} ${sql.slice(whereIndex)}` } export const TableNameReg = /(?<=from|FROM)\s+([^\s;]+)/ @@ -197,7 +197,7 @@ export function processSQL(sql: string, tsColumn: string | undefined, limit: num let orderByClause = '' const upperSql = sql.toUpperCase() if (upperSql.indexOf('ORDER BY') === -1 && tsColumn) { - orderByClause = ` ORDER BY ${tsColumn} DESC ` + orderByClause = ` ORDER BY ${tsColumn} DESC` } // Check if LIMIT exists const limitMatch = sql.match(/\bLIMIT\b/i) @@ -208,7 +208,7 @@ export function processSQL(sql: string, tsColumn: string | undefined, limit: num return `${sql.slice(0, index - 1)}${orderByClause} ${sql.slice(index)}` } - return `${sql}${orderByClause}LIMIT ${limit}`.trim() + return `${sql}${orderByClause} LIMIT ${limit}`.trim() } export function parseLimit(sql: string) { From 380f205764e481f9f9607b8afd6116f7d01ca7b5 Mon Sep 17 00:00:00 2001 From: scl Date: Wed, 11 Dec 2024 10:29:21 +0800 Subject: [PATCH 5/8] style: checkbox --- src/views/dashboard/logs/query/Toolbar.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/views/dashboard/logs/query/Toolbar.vue b/src/views/dashboard/logs/query/Toolbar.vue index 7356accc..66c95128 100644 --- a/src/views/dashboard/logs/query/Toolbar.vue +++ b/src/views/dashboard/logs/query/Toolbar.vue @@ -171,9 +171,12 @@ color: var(--color-text-2); } :deep(.arco-radio-group-button) { - background-color: var(--color-fill-3); + background-color: #fff; } :deep(.arco-radio-button.arco-radio-checked) { color: var(--color-primary); } + :deep(.arco-radio-button.arco-radio-checked) { + background-color: #a376ff33 !important; + } From 9777cee9fb45cb2892abab1fe42037fab3c585a7 Mon Sep 17 00:00:00 2001 From: scl Date: Wed, 11 Dec 2024 10:56:58 +0800 Subject: [PATCH 6/8] fix: page query --- src/views/dashboard/logs/query/Pagination.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/dashboard/logs/query/Pagination.vue b/src/views/dashboard/logs/query/Pagination.vue index 3b58182b..8060abb0 100644 --- a/src/views/dashboard/logs/query/Pagination.vue +++ b/src/views/dashboard/logs/query/Pagination.vue @@ -109,7 +109,7 @@ a-space(v-if="pages.length") function loadPage(start: number, end: number, pageIndex: number) { pages.value[pageIndex].loading = true const tsName = tsColumn.value?.name as string - const pageSql = addTsCondition(sql.value, tsName, start, end) + const pageSql = addTsCondition(sql.value, tsName, start, end + 1) queryPage(pageSql) .then(() => { const index = pages.value.findIndex((page) => page.start === start && page.end === end) From 715a45d317e946db9c5eeed8b8d8e635cca771e8 Mon Sep 17 00:00:00 2001 From: scl Date: Wed, 11 Dec 2024 11:29:14 +0800 Subject: [PATCH 7/8] fix: editing table name --- src/store/modules/logquery/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/modules/logquery/index.ts b/src/store/modules/logquery/index.ts index e896006b..b3653702 100644 --- a/src/store/modules/logquery/index.ts +++ b/src/store/modules/logquery/index.ts @@ -286,7 +286,7 @@ const useLogQueryStore = defineStore('logQuery', () => { type OpKey = keyof typeof opMap function getOpByField(field: string): string[] { - const fields = tableMap.value[inputTableName.value] + const fields = tableMap.value[editingTableName.value] const index = fields.findIndex((f) => f.name === field) if (index === -1) { return [] From 4804f51b1b93ffe8f3908be80bf573887cb3f1f3 Mon Sep 17 00:00:00 2001 From: scl Date: Wed, 11 Dec 2024 14:12:41 +0800 Subject: [PATCH 8/8] feat: fit popup --- src/views/dashboard/logs/query/SQLBuilder.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/views/dashboard/logs/query/SQLBuilder.vue b/src/views/dashboard/logs/query/SQLBuilder.vue index cf30696c..66d14265 100644 --- a/src/views/dashboard/logs/query/SQLBuilder.vue +++ b/src/views/dashboard/logs/query/SQLBuilder.vue @@ -12,6 +12,7 @@ a-form( placeholder="Select Table" :options="tables" :allow-search="true" + :trigger-props="{ autoFitPopupMinWidth: true }" @change="handleTableChange" ) a-form-item(label="Where" field="conditions") @@ -24,6 +25,7 @@ a-form( allow-search placeholder="field" value-key="name" + :trigger-props="{ autoFitPopupMinWidth: true }" @change="() => handleFieldChange(condition)" ) a-option( @@ -35,6 +37,7 @@ a-form( a-select.operator( v-model="condition.op" placeholder="operator" + :trigger-props="{ autoFitPopupMinWidth: true }" :options="getOpByField(condition.field && condition.field.name)" ) a-input.value(v-model="condition.value" placeholder="value")