feat: create a history based autocomplete input
This commit is contained in:
parent
11ab39f11f
commit
66b2f1eade
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue