feat: create a history based autocomplete input

This commit is contained in:
rick 2025-03-08 21:11:34 +08:00
parent 11ab39f11f
commit 66b2f1eade
No known key found for this signature in database
GPG Key ID: 260A80C757EC6783
4 changed files with 178 additions and 35 deletions

View File

@ -0,0 +1,119 @@
<template>
<el-autocomplete
v-model="input"
clearable
:fetch-suggestions="querySearch"
@select="handleSelect"
@keyup.enter="handleEnter"
:placeholder="props.placeholder"
>
<template #default="{ item }">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>{{ item.value }}</span>
<el-icon @click.stop="deleteHistoryItem(item)">
<delete />
</el-icon>
</div>
</template>
</el-autocomplete>
</template>
<script setup lang="ts">
import { ref, defineProps } from 'vue'
import { ElAutocomplete, ElIcon } from 'element-plus'
import { Delete } from '@element-plus/icons-vue'
const props = defineProps({
maxItems: {
type: Number,
default: 10
},
key: {
type: String,
default: 'history'
},
storage: {
type: String,
default: 'localStorage'
},
callback: {
type: Function,
default: () => true
},
placeholder: {
type: String,
default: 'Type something'
}
})
const input = ref('')
const suggestions = ref([])
interface HistoryItem {
value: string
count: number
timestamp: number
}
const querySearch = (queryString: string, cb: any) => {
const results = suggestions.value.filter((item: HistoryItem) => item.value.includes(queryString))
cb(results)
}
const handleSelect = (item: HistoryItem) => {
input.value = item.value
}
const handleEnter = async () => {
if (props.callback) {
const result = await props.callback()
if (!result) {
return
}
}
if (input.value === '') {
return;
}
const history = JSON.parse(getStorage().getItem(props.key) || '[]')
const existingItem = history.find((item: HistoryItem) => item.value === input.value)
if (existingItem) {
existingItem.count++
existingItem.timestamp = Date.now()
} else {
history.push({ value: input.value, count: 1, timestamp: Date.now() })
}
if (history.length > props.maxItems) {
history.sort((a: HistoryItem, b: HistoryItem) => a.count - b.count || a.timestamp - b.timestamp)
history.shift()
}
getStorage().setItem(props.key, JSON.stringify(history))
suggestions.value = history
}
const loadHistory = () => {
suggestions.value = JSON.parse(getStorage().getItem('history') || '[]')
}
const deleteHistoryItem = (item: HistoryItem) => {
const history = JSON.parse(getStorage().getItem(props.key) || '[]')
const updatedHistory = history.filter((historyItem: HistoryItem) => historyItem.value !== item.value)
getStorage().setItem(props.key, JSON.stringify(updatedHistory))
suggestions.value = updatedHistory
}
const getStorage = () => {
switch (props.storage) {
case 'localStorage':
return localStorage
case 'sessionStorage':
return sessionStorage
default:
return localStorage
}
}
loadHistory()
</script>

View File

@ -2,18 +2,23 @@
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { API } from './net' import { API } from './net'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Codemirror } from 'vue-codemirror'
import HistoryInput from '../components/HistoryInput.vue'
const stores = ref([]) const stores = ref([])
const kind = ref('') const kind = ref('')
const store = ref('') const store = ref('')
const sqlQuery = ref('') const sqlQuery = ref('')
const queryResult = ref([]) const queryResult = ref([])
const queryResultAsJSON= ref('')
const columns = ref([]) const columns = ref([])
const queryTip = ref('') const queryTip = ref('')
const databases = ref([]) const databases = ref([])
const tables = ref([]) const tables = ref([])
const currentDatabase = ref('') const currentDatabase = ref('')
const loadingStores = ref(true) const loadingStores = ref(true)
const dataFormat = ref('table')
const dataFormatOptions = ['table', 'json']
const tablesTree = ref([]) const tablesTree = ref([])
watch(store, (s) => { watch(store, (s) => {
@ -78,6 +83,7 @@ const ormDataHandler = (data) => {
tables.value = data.meta.tables tables.value = data.meta.tables
currentDatabase.value = data.meta.currentDatabase currentDatabase.value = data.meta.currentDatabase
queryResult.value = result queryResult.value = result
queryResultAsJSON.value = JSON.stringify(result, null, 2)
columns.value = Array.from(cols).sort((a, b) => { columns.value = Array.from(cols).sort((a, b) => {
if (a === 'id') return -1; if (a === 'id') return -1;
if (b === 'id') return 1; if (b === 'id') return 1;
@ -111,10 +117,13 @@ const executeQuery = async () => {
break; break;
} }
API.DataQuery(store.value, kind.value, currentDatabase.value, sqlQuery.value, (data) => { let success = false
try {
const data = await API.DataQueryAsync(store.value, kind.value, currentDatabase.value, sqlQuery.value);
switch (kind.value) { switch (kind.value) {
case 'atest-store-orm': case 'atest-store-orm':
ormDataHandler(data) ormDataHandler(data)
success = true
break; break;
case 'atest-store-etcd': case 'atest-store-etcd':
keyValueDataHandler(data) keyValueDataHandler(data)
@ -129,19 +138,20 @@ const executeQuery = async () => {
type: 'error' type: 'error'
}); });
} }
}, (e) => { } catch (e: any) {
ElMessage({ ElMessage({
showClose: true, showClose: true,
message: e.message, message: e.message,
type: 'error' type: 'error'
}); });
}) }
return success
} }
</script> </script>
<template> <template>
<div> <div>
<el-container style="height: calc(100vh - 45px);"> <el-container style="height: calc(100vh - 50px);">
<el-aside v-if="kind === 'atest-store-orm'"> <el-aside v-if="kind === 'atest-store-orm'">
<el-scrollbar> <el-scrollbar>
<el-select v-model="currentDatabase" placeholder="Select database" @change="queryTables" filterable> <el-select v-model="currentDatabase" placeholder="Select database" @change="queryTables" filterable>
@ -163,9 +173,9 @@ const executeQuery = async () => {
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="17"> <el-col :span="16">
<el-form-item> <el-form-item>
<el-input v-model="sqlQuery" :placeholder="queryTip" @keyup.enter="executeQuery"></el-input> <HistoryInput :placeholder="queryTip" :callback="executeQuery" v-model="sqlQuery"/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="2"> <el-col :span="2">
@ -173,13 +183,20 @@ const executeQuery = async () => {
<el-button type="primary" @click="executeQuery">Execute</el-button> <el-button type="primary" @click="executeQuery">Execute</el-button>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="2">
<el-select v-model="dataFormat" placeholder="Select data format">
<el-option v-for="item in dataFormatOptions" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-col>
</el-row> </el-row>
</el-form> </el-form>
</el-header> </el-header>
<el-main> <el-main>
<el-table :data="queryResult"> <el-table :data="queryResult" stripe v-if="dataFormat === 'table'">
<el-table-column v-for="col in columns" :key="col" :prop="col" :label="col"></el-table-column> <el-table-column v-for="col in columns" :key="col" :prop="col" :label="col" sortable/>
</el-table> </el-table>
<Codemirror v-else-if="dataFormat === 'json'"
v-model="queryResultAsJSON"/>
</el-main> </el-main>
</el-container> </el-container>
</el-container> </el-container>

View File

@ -973,7 +973,7 @@ const renameTestCase = (name: string) => {
</div> </div>
</el-header> </el-header>
<el-main style="padding-left: 5px;"> <el-main style="padding-left: 5px; min-height: 280px;">
<el-tabs v-model="requestActiveTab"> <el-tabs v-model="requestActiveTab">
<el-tab-pane name="query" v-if="props.kindName !== 'tRPC' && props.kindName !== 'gRPC'"> <el-tab-pane name="query" v-if="props.kindName !== 'tRPC' && props.kindName !== 'gRPC'">
<template #label> <template #label>

View File

@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ca } from 'element-plus/es/locales.mjs'
import { Cache } from './cache' import { Cache } from './cache'
async function DefaultResponseProcess(response: any) { async function DefaultResponseProcess(response: any) {
@ -779,32 +780,38 @@ var SBOM = (callback: (d: any) => void) => {
.then(callback) .then(callback)
} }
interface QueryObject {
sql: string
key: string
}
var DataQueryAsync = (store: string, kind: string, currentDatabase: string, query: string) => {
const queryObj = {} as QueryObject;
switch (kind) {
case 'atest-store-orm':
queryObj['sql'] = query;
queryObj['key'] = currentDatabase;
break;
case 'atest-store-etcd':
queryObj['key'] = query;
break;
case 'atest-store-redis':
queryObj['key'] = query;
break;
}
const requestOptions = {
method: 'POST',
headers: {
'X-Store-Name': store,
'X-Database': currentDatabase
},
body: JSON.stringify(queryObj)
}
return fetch(`/api/v1/data/query`, requestOptions)
.then(DefaultResponseProcess)
}
var DataQuery = (store: string, kind: string, currentDatabase: string, query: string, callback: (d: any) => void, errHandler: (d: any) => void) => { var DataQuery = (store: string, kind: string, currentDatabase: string, query: string, callback: (d: any) => void, errHandler: (d: any) => void) => {
const queryObj = {} DataQueryAsync(store, kind, currentDatabase, query).then(callback).catch(errHandler)
switch (kind) {
case 'atest-store-orm':
queryObj['sql'] = query;
queryObj['key'] = currentDatabase;
break;
case 'atest-store-etcd':
queryObj['key'] = query;
break;
case 'atest-store-redis':
queryObj['key'] = query;
break;
}
const requestOptions = {
method: 'POST',
headers: {
'X-Store-Name': store,
'X-Database': currentDatabase
},
body: JSON.stringify(queryObj)
}
fetch(`/api/v1/data/query`, requestOptions)
.then(DefaultResponseProcess)
.then(callback)
.catch(errHandler)
} }
export const API = { export const API = {
@ -819,6 +826,6 @@ export const API = {
FunctionsQuery, FunctionsQuery,
GetSecrets, DeleteSecret, CreateOrUpdateSecret, GetSecrets, DeleteSecret, CreateOrUpdateSecret,
GetSuggestedAPIs, GetSwaggers, GetSuggestedAPIs, GetSwaggers,
ReloadMockServer, GetMockConfig, SBOM, DataQuery, ReloadMockServer, GetMockConfig, SBOM, DataQuery, DataQueryAsync,
getToken getToken
} }