2021-07-28 18:03:19 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div>
|
2023-02-07 16:54:44 +08:00
|
|
|
|
<el-row>
|
|
|
|
|
|
<el-col :span="4">
|
2023-02-18 23:02:14 +08:00
|
|
|
|
<el-row type="flex" justify="space-between">
|
|
|
|
|
|
<el-col :span="24" class="el-scrollbar flex-auto">
|
|
|
|
|
|
<tag-tree @node-click="nodeClick" :load="loadNode">
|
|
|
|
|
|
<template #prefix="{ data }">
|
|
|
|
|
|
<span v-if="data.type == NodeType.Redis">
|
|
|
|
|
|
<el-popover placement="right-start" title="redis实例信息" trigger="hover" :width="210">
|
|
|
|
|
|
<template #reference>
|
2023-03-17 09:46:41 +08:00
|
|
|
|
<SvgIcon name="iconfont icon-op-redis" :size="18" />
|
2023-02-18 23:02:14 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template #default>
|
2023-07-03 21:42:04 +08:00
|
|
|
|
<el-form class="instances-pop-form" label-width="auto" :size="'small'">
|
2023-02-18 23:02:14 +08:00
|
|
|
|
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
|
2023-03-06 16:59:57 +08:00
|
|
|
|
<el-form-item label="模式:">{{ data.params.mode }}</el-form-item>
|
2023-02-18 23:02:14 +08:00
|
|
|
|
<el-form-item label="链接:">{{ data.params.host }}</el-form-item>
|
2023-07-06 20:59:22 +08:00
|
|
|
|
<el-form-item label="备注:">{{ data.params.remark }}</el-form-item>
|
2023-02-18 23:02:14 +08:00
|
|
|
|
</el-form>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-popover>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
2023-03-17 09:46:41 +08:00
|
|
|
|
<SvgIcon v-if="data.type == NodeType.Db" name="Coin" color="#67c23a" />
|
2023-02-18 23:02:14 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</tag-tree>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
2023-02-07 08:39:14 +08:00
|
|
|
|
</el-col>
|
2023-02-18 23:02:14 +08:00
|
|
|
|
|
2023-08-28 17:25:59 +08:00
|
|
|
|
<el-col v-loading="state.loadingKeyTree" :span="7" class="el-scrollbar flex-auto" style="overflow: auto">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<el-row>
|
|
|
|
|
|
<el-col :span="2">
|
|
|
|
|
|
<el-input v-model="state.keySeparator" placeholder="分割符" size="small" class="ml5" />
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="18">
|
|
|
|
|
|
<el-input @clear="clear" v-model="scanParam.match" placeholder="match 支持*模糊key" clearable size="small" class="ml10" />
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
class="ml15"
|
|
|
|
|
|
:disabled="!scanParam.id || !scanParam.db"
|
|
|
|
|
|
@click="searchKey()"
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
icon="search"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
plain
|
|
|
|
|
|
></el-button>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row class="mb5 mt5">
|
|
|
|
|
|
<el-col :span="20">
|
|
|
|
|
|
<el-button class="ml5" :disabled="!scanParam.id || !scanParam.db" @click="scan(true)" type="success" icon="more" size="small" plain
|
|
|
|
|
|
>加载更多</el-button
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-auth="'redis:data:save'"
|
|
|
|
|
|
:disabled="!scanParam.id || !scanParam.db"
|
|
|
|
|
|
@click="showNewKeyDialog"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon="plus"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
plain
|
|
|
|
|
|
>新增key</el-button
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
:disabled="!scanParam.id || !scanParam.db"
|
|
|
|
|
|
@click="flushDb"
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
plain
|
|
|
|
|
|
v-auth="'redis:data:del'"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon="delete"
|
|
|
|
|
|
>flush</el-button
|
|
|
|
|
|
>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
|
<span style="display: inline-block" class="mt5">keys: {{ state.dbsize }}</span>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<el-tree
|
|
|
|
|
|
:style="{ maxHeight: state.keyTreeHeight, height: state.keyTreeHeight, overflow: 'auto', border: '1px solid #e1f3d8' }"
|
|
|
|
|
|
ref="keyTreeRef"
|
|
|
|
|
|
:highlight-current="true"
|
|
|
|
|
|
:data="keyTreeData"
|
|
|
|
|
|
:props="treeProps"
|
|
|
|
|
|
:indent="8"
|
|
|
|
|
|
node-key="key"
|
|
|
|
|
|
:auto-expand-parent="false"
|
|
|
|
|
|
:default-expanded-keys="Array.from(state.keyTreeExpanded)"
|
|
|
|
|
|
@node-click="handleKeyTreeNodeClick"
|
|
|
|
|
|
@node-expand="keyTreeNodeExpand"
|
|
|
|
|
|
@node-collapse="keyTreeNodeCollapse"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default="{ node, data }">
|
|
|
|
|
|
<span class="custom-tree-node key-list-custom-node">
|
|
|
|
|
|
<el-dropdown size="small" trigger="contextmenu">
|
|
|
|
|
|
<span class="el-dropdown-link">
|
|
|
|
|
|
<span v-if="data.type == 1 && !node.expanded">
|
|
|
|
|
|
<SvgIcon :size="15" name="folder" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-if="data.type == 1 && node.expanded">
|
|
|
|
|
|
<SvgIcon :size="15" name="folder-opened" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-if="data.type == 1" class="ml5" style="font-weight: bold">
|
|
|
|
|
|
{{ node.label }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-if="data.type == 2" class="ml5" style="color: #67c23a">
|
|
|
|
|
|
{{ node.label }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
<span v-if="!node.isLeaf" class="ml5" style="font-weight: bold"> ({{ data.keyCount }}) </span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
<template #dropdown v-if="data.type == 2">
|
|
|
|
|
|
<el-dropdown-menu>
|
|
|
|
|
|
<el-dropdown-item @click="showKeyDetail(data.key, true)">
|
|
|
|
|
|
<el-link type="primary" icon="plus" :underline="false" style="margin-left: 2px">新tab打开</el-link>
|
|
|
|
|
|
</el-dropdown-item>
|
|
|
|
|
|
<span v-auth="'redis:data:del'">
|
|
|
|
|
|
<el-dropdown-item @click="delKey(data.key)">
|
|
|
|
|
|
<el-link type="danger" icon="delete" :underline="false" style="margin-left: 2px">删除</el-link>
|
|
|
|
|
|
</el-dropdown-item>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</el-dropdown-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dropdown>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-tree>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
|
|
|
|
|
|
<el-col :span="13" style="border-left: 1px solid var(--el-card-border-color)">
|
|
|
|
|
|
<div class="ml5">
|
|
|
|
|
|
<el-tabs @tab-remove="removeDataTab" style="width: 100%" v-model="state.activeName">
|
|
|
|
|
|
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
|
|
|
|
|
|
<key-detail :redisId="scanParam.id" :db="scanParam.db" :key-info="dt.keyInfo" @change-key="searchKey()" @del-key="delKey" />
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
2023-02-13 21:11:16 +08:00
|
|
|
|
</div>
|
2023-02-07 08:39:14 +08:00
|
|
|
|
</el-col>
|
2023-02-07 16:54:44 +08:00
|
|
|
|
</el-row>
|
2021-12-21 03:06:29 +00:00
|
|
|
|
|
2021-07-28 18:03:19 +08:00
|
|
|
|
<div style="text-align: center; margin-top: 10px"></div>
|
|
|
|
|
|
|
2023-07-06 20:59:22 +08:00
|
|
|
|
<el-dialog title="新增Key" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
|
2023-07-05 22:06:32 +08:00
|
|
|
|
<el-form ref="keyForm" label-width="auto">
|
2023-04-16 00:50:36 +08:00
|
|
|
|
<el-form-item prop="key" label="键名">
|
2023-08-28 17:25:59 +08:00
|
|
|
|
<el-input v-model.trim="newKeyDialog.keyInfo.key" placeholder="请输入键名"></el-input>
|
2023-04-16 00:50:36 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item prop="type" label="类型">
|
2023-08-28 17:25:59 +08:00
|
|
|
|
<el-select v-model="newKeyDialog.keyInfo.type" default-first-option style="width: 100%" placeholder="请选择类型">
|
2023-04-16 00:50:36 +08:00
|
|
|
|
<el-option key="string" label="string" value="string"></el-option>
|
|
|
|
|
|
<el-option key="hash" label="hash" value="hash"></el-option>
|
|
|
|
|
|
<el-option key="set" label="set" value="set"></el-option>
|
|
|
|
|
|
<el-option key="zset" label="zset" value="zset"></el-option>
|
|
|
|
|
|
<el-option key="list" label="list" value="list"></el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<div class="dialog-footer">
|
|
|
|
|
|
<el-button @click="cancelNewKey()">取 消</el-button>
|
2023-08-28 17:25:59 +08:00
|
|
|
|
<el-button v-auth="'redis:data:save'" type="primary" @click="newKey">确 定</el-button>
|
2023-04-16 00:50:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2021-07-28 18:03:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2022-10-29 20:08:15 +08:00
|
|
|
|
<script lang="ts" setup>
|
2021-07-28 18:03:19 +08:00
|
|
|
|
import { redisApi } from './api';
|
2023-08-28 17:25:59 +08:00
|
|
|
|
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick } from 'vue';
|
2021-07-28 18:03:19 +08:00
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
2022-07-10 12:14:06 +08:00
|
|
|
|
import { isTrue, notBlank, notNull } from '@/common/assert';
|
2023-02-18 23:02:14 +08:00
|
|
|
|
import { TagTreeNode } from '../component/tag';
|
|
|
|
|
|
import TagTree from '../component/TagTree.vue';
|
2023-08-28 17:25:59 +08:00
|
|
|
|
import { keysToTree, sortByTreeNodes, keysToList } from './utils';
|
2023-02-18 23:02:14 +08:00
|
|
|
|
|
2023-04-16 00:50:36 +08:00
|
|
|
|
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
|
2023-03-15 11:41:03 +08:00
|
|
|
|
|
2023-02-18 23:02:14 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 树节点类型
|
|
|
|
|
|
*/
|
2023-02-20 18:41:45 +08:00
|
|
|
|
class NodeType {
|
2023-07-06 20:59:22 +08:00
|
|
|
|
static Redis = 1;
|
|
|
|
|
|
static Db = 2;
|
2023-02-18 23:02:14 +08:00
|
|
|
|
}
|
2022-10-27 10:30:11 +08:00
|
|
|
|
|
2023-08-28 17:25:59 +08:00
|
|
|
|
const treeProps = {
|
|
|
|
|
|
label: 'name',
|
|
|
|
|
|
children: 'children',
|
|
|
|
|
|
isLeaf: 'leaf',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const defaultCount = 250;
|
|
|
|
|
|
|
|
|
|
|
|
const keyTreeRef: any = ref(null);
|
|
|
|
|
|
|
2022-10-29 20:08:15 +08:00
|
|
|
|
const state = reactive({
|
|
|
|
|
|
tags: [],
|
|
|
|
|
|
redisList: [] as any,
|
|
|
|
|
|
dbList: [],
|
2023-08-28 17:25:59 +08:00
|
|
|
|
keyTreeHeight: window.innerHeight - 147 - 30 + 'px',
|
|
|
|
|
|
loadingKeyTree: false,
|
|
|
|
|
|
keys: [] as any,
|
|
|
|
|
|
keySeparator: ':',
|
|
|
|
|
|
keyTreeData: [] as any,
|
|
|
|
|
|
keyTreeExpanded: new Set(),
|
|
|
|
|
|
activeName: '',
|
|
|
|
|
|
dataTabs: {} as any,
|
2022-10-29 20:08:15 +08:00
|
|
|
|
scanParam: {
|
|
|
|
|
|
id: null as any,
|
2022-12-05 21:45:35 +08:00
|
|
|
|
mode: '',
|
2023-07-02 17:06:00 +08:00
|
|
|
|
db: null as any,
|
2022-10-29 20:08:15 +08:00
|
|
|
|
match: null,
|
2023-08-28 17:25:59 +08:00
|
|
|
|
count: defaultCount,
|
2022-10-29 20:08:15 +08:00
|
|
|
|
cursor: {},
|
|
|
|
|
|
},
|
2023-08-28 17:25:59 +08:00
|
|
|
|
newKeyDialog: {
|
2022-10-29 20:08:15 +08:00
|
|
|
|
visible: false,
|
|
|
|
|
|
keyInfo: {
|
|
|
|
|
|
type: 'string',
|
|
|
|
|
|
timed: -1,
|
|
|
|
|
|
key: '',
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
dbsize: 0,
|
|
|
|
|
|
});
|
2021-07-28 18:03:19 +08:00
|
|
|
|
|
2023-08-28 17:25:59 +08:00
|
|
|
|
const { scanParam, keyTreeData, newKeyDialog } = toRefs(state);
|
2023-02-18 23:02:14 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
setHeight();
|
2023-08-28 17:25:59 +08:00
|
|
|
|
// 监听浏览器窗口大小变化,更新对应组件高度
|
|
|
|
|
|
window.onresize = () => setHeight();
|
2023-07-06 20:59:22 +08:00
|
|
|
|
});
|
2023-02-18 23:02:14 +08:00
|
|
|
|
|
|
|
|
|
|
const setHeight = () => {
|
2023-08-28 17:25:59 +08:00
|
|
|
|
state.keyTreeHeight = window.innerHeight - 177 + 'px';
|
2023-07-06 20:59:22 +08:00
|
|
|
|
};
|
2023-02-18 23:02:14 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* instmap; tagPaht -> redis info[]
|
|
|
|
|
|
*/
|
|
|
|
|
|
const instMap: Map<string, any[]> = new Map();
|
|
|
|
|
|
|
|
|
|
|
|
const getInsts = async () => {
|
2023-07-01 14:34:42 +08:00
|
|
|
|
const res = await redisApi.redisList.request({ pageNum: 1, pageSize: 1000 });
|
2023-07-06 20:59:22 +08:00
|
|
|
|
if (!res.total) return;
|
2023-02-18 23:02:14 +08:00
|
|
|
|
for (const redisInfo of res.list) {
|
|
|
|
|
|
const tagPath = redisInfo.tagPath;
|
|
|
|
|
|
let redisInsts = instMap.get(tagPath) || [];
|
|
|
|
|
|
redisInsts.push(redisInfo);
|
|
|
|
|
|
instMap.set(tagPath, redisInsts);
|
|
|
|
|
|
}
|
2023-07-06 20:59:22 +08:00
|
|
|
|
};
|
2023-02-18 23:02:14 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加载文件树节点
|
|
|
|
|
|
* @param {Object} node
|
|
|
|
|
|
* @param {Object} resolve
|
|
|
|
|
|
*/
|
|
|
|
|
|
const loadNode = async (node: any) => {
|
|
|
|
|
|
// 一级为tagPath
|
|
|
|
|
|
if (node.level === 0) {
|
|
|
|
|
|
await getInsts();
|
|
|
|
|
|
const tagPaths = instMap.keys();
|
|
|
|
|
|
const tagNodes = [];
|
|
|
|
|
|
for (let tagPath of tagPaths) {
|
|
|
|
|
|
tagNodes.push(new TagTreeNode(tagPath, tagPath));
|
|
|
|
|
|
}
|
|
|
|
|
|
return tagNodes;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = node.data;
|
|
|
|
|
|
// 点击tagPath -> 加载数据库信息列表
|
|
|
|
|
|
if (data.type === TagTreeNode.TagPath) {
|
2023-07-06 20:59:22 +08:00
|
|
|
|
const redisInfos = instMap.get(data.key);
|
2023-02-18 23:02:14 +08:00
|
|
|
|
return redisInfos?.map((x: any) => {
|
|
|
|
|
|
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Redis).withParams(x);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 点击redis实例 -> 加载库列表
|
|
|
|
|
|
if (data.type === NodeType.Redis) {
|
|
|
|
|
|
return await getDbs(data.params);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const nodeClick = (data: any) => {
|
|
|
|
|
|
// 点击库事件
|
|
|
|
|
|
if (data.type === NodeType.Db) {
|
|
|
|
|
|
resetScanParam();
|
|
|
|
|
|
state.scanParam.id = data.params.id;
|
|
|
|
|
|
state.scanParam.db = data.params.db;
|
|
|
|
|
|
scan();
|
|
|
|
|
|
}
|
2023-07-06 20:59:22 +08:00
|
|
|
|
};
|
2023-02-18 23:02:14 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取所有库信息
|
|
|
|
|
|
* @param redisInfo redis信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
const getDbs = async (redisInfo: any) => {
|
|
|
|
|
|
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
2023-03-17 09:46:41 +08:00
|
|
|
|
return new TagTreeNode(x, `db${x}`, NodeType.Db).withIsLeaf(true).withParams({
|
2023-02-18 23:02:14 +08:00
|
|
|
|
id: redisInfo.id,
|
|
|
|
|
|
db: x,
|
|
|
|
|
|
name: `db${x}`,
|
|
|
|
|
|
keys: 0,
|
2023-07-06 20:59:22 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2023-03-17 09:46:41 +08:00
|
|
|
|
|
|
|
|
|
|
if (redisInfo.mode == 'cluster') {
|
|
|
|
|
|
return dbs;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-07-06 20:59:22 +08:00
|
|
|
|
const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: 'Keyspace' });
|
2023-02-18 23:02:14 +08:00
|
|
|
|
for (let db in res.Keyspace) {
|
|
|
|
|
|
for (let d of dbs) {
|
|
|
|
|
|
if (db == d.params.name) {
|
2023-07-06 20:59:22 +08:00
|
|
|
|
d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0;
|
2023-02-18 23:02:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 替换label
|
|
|
|
|
|
dbs.forEach((e: any) => {
|
2023-07-06 20:59:22 +08:00
|
|
|
|
e.label = `${e.params.name} [${e.params.keys}]`;
|
2023-02-18 23:02:14 +08:00
|
|
|
|
});
|
|
|
|
|
|
return dbs;
|
2023-07-06 20:59:22 +08:00
|
|
|
|
};
|
2023-02-18 23:02:14 +08:00
|
|
|
|
|
2023-08-28 17:25:59 +08:00
|
|
|
|
const scan = async (appendKey = false) => {
|
2022-10-29 20:08:15 +08:00
|
|
|
|
isTrue(state.scanParam.id != null, '请先选择redis');
|
2023-08-28 17:25:59 +08:00
|
|
|
|
notBlank(state.scanParam.db, '请先选择库');
|
2022-10-29 20:08:15 +08:00
|
|
|
|
|
2022-12-05 21:45:35 +08:00
|
|
|
|
const match: string = state.scanParam.match || '';
|
|
|
|
|
|
if (!match) {
|
2023-08-28 17:25:59 +08:00
|
|
|
|
state.scanParam.count = defaultCount;
|
2022-12-05 21:45:35 +08:00
|
|
|
|
} else if (match.indexOf('*') != -1) {
|
|
|
|
|
|
const dbsize = state.dbsize;
|
|
|
|
|
|
// 如果为模糊搜索,并且搜索的key模式大于指定字符数,则将count设大点scan
|
|
|
|
|
|
if (match.length > 10) {
|
|
|
|
|
|
state.scanParam.count = dbsize > 100000 ? Math.floor(dbsize / 10) : 1000;
|
|
|
|
|
|
} else {
|
2023-08-28 17:25:59 +08:00
|
|
|
|
state.scanParam.count = defaultCount;
|
2022-12-05 21:45:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-07-06 20:59:22 +08:00
|
|
|
|
const scanParam = { ...state.scanParam };
|
2022-12-05 21:45:35 +08:00
|
|
|
|
// 集群模式count设小点,因为后端会从所有master节点scan一遍然后合并结果,默认假设redis集群有3个master
|
|
|
|
|
|
if (scanParam.mode == 'cluster') {
|
2023-07-06 20:59:22 +08:00
|
|
|
|
scanParam.count = Math.floor(state.scanParam.count / 3);
|
2022-10-29 20:08:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2023-08-28 17:25:59 +08:00
|
|
|
|
state.loadingKeyTree = true;
|
2022-12-05 21:45:35 +08:00
|
|
|
|
const res = await redisApi.scan.request(scanParam);
|
2023-08-28 17:25:59 +08:00
|
|
|
|
// 追加key,则将新key合并至原keys(加载更多)
|
|
|
|
|
|
if (appendKey) {
|
|
|
|
|
|
state.keys = [...state.keys, ...res.keys];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
state.keys = res.keys;
|
|
|
|
|
|
}
|
|
|
|
|
|
setKeyList(state.keys);
|
2022-10-29 20:08:15 +08:00
|
|
|
|
state.dbsize = res.dbSize;
|
|
|
|
|
|
state.scanParam.cursor = res.cursor;
|
|
|
|
|
|
} finally {
|
2023-08-28 17:25:59 +08:00
|
|
|
|
state.loadingKeyTree = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const setKeyList = (keys: any) => {
|
|
|
|
|
|
state.keyTreeData = state.keySeparator ? keysToTree(keys, state.keySeparator, state.keyTreeExpanded) : keysToList(keys);
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
// key长度小于指定数量,则展开所有节点
|
|
|
|
|
|
if (keys.length <= 20) {
|
|
|
|
|
|
expandAllKeyNode(state.keyTreeData);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sortByTreeNodes(keyTreeRef.value.root.childNodes);
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 展开所有节点
|
|
|
|
|
|
const expandAllKeyNode = (nodes: any) => {
|
|
|
|
|
|
for (let node of nodes) {
|
|
|
|
|
|
if (!node.children) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
state.keyTreeExpanded.add(node.key);
|
|
|
|
|
|
for (let i = 0; i < node.children.length; i++) {
|
|
|
|
|
|
expandAllKeyNode(node.children);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleKeyTreeNodeClick = async (data: any) => {
|
|
|
|
|
|
// 目录则不做处理
|
|
|
|
|
|
if (data.type == 1) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showKeyDetail(data.key);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const showKeyDetail = async (key: any, newTab = false) => {
|
|
|
|
|
|
let keyInfo;
|
|
|
|
|
|
if (typeof key == 'object') {
|
|
|
|
|
|
keyInfo = key;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (state.dataTabs[key]) {
|
|
|
|
|
|
state.activeName = key;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await redisApi.keyInfo.request({ id: state.scanParam.id, db: state.scanParam.db, key: key });
|
|
|
|
|
|
keyInfo = {
|
|
|
|
|
|
key: key,
|
|
|
|
|
|
type: res.type,
|
|
|
|
|
|
timed: res.ttl,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let label = keyInfo.key;
|
|
|
|
|
|
if (label.length > 40) {
|
|
|
|
|
|
label = label.slice(0, 40) + '...';
|
|
|
|
|
|
}
|
|
|
|
|
|
const dataTab = {
|
|
|
|
|
|
key: keyInfo.key,
|
|
|
|
|
|
label,
|
|
|
|
|
|
keyInfo,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!newTab) {
|
|
|
|
|
|
delete state.dataTabs[state.activeName];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
state.dataTabs[keyInfo.key] = dataTab;
|
|
|
|
|
|
state.activeName = keyInfo.key;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeDataTab = (targetName: string) => {
|
|
|
|
|
|
const tabNames = Object.keys(state.dataTabs);
|
|
|
|
|
|
let activeName = state.activeName;
|
|
|
|
|
|
tabNames.forEach((name, index) => {
|
|
|
|
|
|
if (name === targetName) {
|
|
|
|
|
|
const nextTab = tabNames[index + 1] || tabNames[index - 1];
|
|
|
|
|
|
if (nextTab) {
|
|
|
|
|
|
activeName = nextTab;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
state.activeName = activeName;
|
|
|
|
|
|
delete state.dataTabs[targetName];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const keyTreeNodeExpand = (data: any, node: any, component: any) => {
|
|
|
|
|
|
state.keyTreeExpanded.add(data.key);
|
|
|
|
|
|
// async sort nodes
|
|
|
|
|
|
if (!node.customSorted) {
|
|
|
|
|
|
node.customSorted = true;
|
|
|
|
|
|
sortByTreeNodes(node.childNodes);
|
2022-10-29 20:08:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2023-08-28 17:25:59 +08:00
|
|
|
|
const keyTreeNodeCollapse = (data: any, node: any, component: any) => {
|
|
|
|
|
|
state.keyTreeExpanded.delete(data.key);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2022-10-29 20:08:15 +08:00
|
|
|
|
const searchKey = async () => {
|
|
|
|
|
|
state.scanParam.cursor = {};
|
2023-08-28 17:25:59 +08:00
|
|
|
|
await scan(false);
|
2022-10-29 20:08:15 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const clear = () => {
|
|
|
|
|
|
resetScanParam();
|
|
|
|
|
|
if (state.scanParam.id) {
|
|
|
|
|
|
scan();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2022-12-05 21:45:35 +08:00
|
|
|
|
const resetScanParam = () => {
|
2022-10-29 20:08:15 +08:00
|
|
|
|
state.scanParam.match = null;
|
|
|
|
|
|
state.scanParam.cursor = {};
|
2023-08-28 17:25:59 +08:00
|
|
|
|
state.keyTreeExpanded.clear();
|
|
|
|
|
|
state.dataTabs = {};
|
|
|
|
|
|
state.activeName = '';
|
2022-10-29 20:08:15 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2023-04-16 00:50:36 +08:00
|
|
|
|
const showNewKeyDialog = () => {
|
2022-10-29 20:08:15 +08:00
|
|
|
|
notNull(state.scanParam.id, '请先选择redis');
|
2023-07-06 20:59:22 +08:00
|
|
|
|
notNull(state.scanParam.db, '请选择要操作的库');
|
2023-08-28 17:25:59 +08:00
|
|
|
|
resetNewKeyInfo();
|
2023-04-16 00:50:36 +08:00
|
|
|
|
state.newKeyDialog.visible = true;
|
2023-07-06 20:59:22 +08:00
|
|
|
|
};
|
2023-04-16 00:50:36 +08:00
|
|
|
|
|
2023-06-15 19:18:29 +08:00
|
|
|
|
const flushDb = () => {
|
|
|
|
|
|
ElMessageBox.confirm(`确定清空[${state.scanParam.db}]库的所有key?`, '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
2023-07-06 20:59:22 +08:00
|
|
|
|
})
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
redisApi.flushDb
|
|
|
|
|
|
.request({
|
|
|
|
|
|
id: state.scanParam.id,
|
|
|
|
|
|
db: state.scanParam.db,
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
ElMessage.success('清除成功!');
|
|
|
|
|
|
searchKey();
|
|
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
};
|
2023-06-15 19:18:29 +08:00
|
|
|
|
|
2023-04-16 00:50:36 +08:00
|
|
|
|
const cancelNewKey = () => {
|
2023-08-28 17:25:59 +08:00
|
|
|
|
resetNewKeyInfo();
|
2023-04-16 00:50:36 +08:00
|
|
|
|
state.newKeyDialog.visible = false;
|
2023-07-06 20:59:22 +08:00
|
|
|
|
};
|
2023-04-16 00:50:36 +08:00
|
|
|
|
|
|
|
|
|
|
const newKey = async () => {
|
2023-08-28 17:25:59 +08:00
|
|
|
|
const keyInfo = state.newKeyDialog.keyInfo;
|
2023-07-06 20:59:22 +08:00
|
|
|
|
const keyType = keyInfo.type;
|
2023-04-16 00:50:36 +08:00
|
|
|
|
const key = keyInfo.key;
|
2023-07-06 20:59:22 +08:00
|
|
|
|
notBlank(key, '键名不能为空');
|
2023-04-16 00:50:36 +08:00
|
|
|
|
|
|
|
|
|
|
if (keyType == 'string') {
|
|
|
|
|
|
await redisApi.setString.request({
|
|
|
|
|
|
id: state.scanParam.id,
|
|
|
|
|
|
db: state.scanParam.db,
|
|
|
|
|
|
key: key,
|
|
|
|
|
|
value: '',
|
2023-07-06 20:59:22 +08:00
|
|
|
|
});
|
2022-10-29 20:08:15 +08:00
|
|
|
|
}
|
2023-08-28 17:25:59 +08:00
|
|
|
|
|
|
|
|
|
|
showKeyDetail(
|
|
|
|
|
|
{
|
|
|
|
|
|
...keyInfo,
|
|
|
|
|
|
},
|
|
|
|
|
|
true
|
|
|
|
|
|
);
|
2023-04-16 00:50:36 +08:00
|
|
|
|
state.newKeyDialog.visible = false;
|
2023-08-28 17:25:59 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加新增的key至key tree
|
|
|
|
|
|
state.keys.push(key);
|
|
|
|
|
|
setKeyList(state.keys);
|
2023-07-06 20:59:22 +08:00
|
|
|
|
};
|
2022-10-29 20:08:15 +08:00
|
|
|
|
|
2023-08-28 17:25:59 +08:00
|
|
|
|
const resetNewKeyInfo = () => {
|
|
|
|
|
|
state.newKeyDialog.keyInfo.key = '';
|
|
|
|
|
|
state.newKeyDialog.keyInfo.type = 'string';
|
|
|
|
|
|
state.newKeyDialog.keyInfo.timed = -1;
|
2023-07-06 20:59:22 +08:00
|
|
|
|
};
|
2022-10-29 20:08:15 +08:00
|
|
|
|
|
2023-08-28 17:25:59 +08:00
|
|
|
|
const delKey = (key: string) => {
|
2022-10-29 20:08:15 +08:00
|
|
|
|
ElMessageBox.confirm(`确定删除[ ${key} ] 该key?`, '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
2023-07-06 20:59:22 +08:00
|
|
|
|
})
|
2023-08-28 17:25:59 +08:00
|
|
|
|
.then(async () => {
|
|
|
|
|
|
await redisApi.delKey.request({
|
|
|
|
|
|
key,
|
|
|
|
|
|
id: state.scanParam.id,
|
|
|
|
|
|
db: state.scanParam.db,
|
|
|
|
|
|
});
|
|
|
|
|
|
ElMessage.success('删除成功!');
|
|
|
|
|
|
searchKey();
|
|
|
|
|
|
|
|
|
|
|
|
removeDataTab(key);
|
2023-07-06 20:59:22 +08:00
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
2022-10-29 20:08:15 +08:00
|
|
|
|
};
|
2023-02-18 23:02:14 +08:00
|
|
|
|
</script>
|
2022-10-29 20:08:15 +08:00
|
|
|
|
|
2023-02-18 23:02:14 +08:00
|
|
|
|
<style lang="scss">
|
|
|
|
|
|
.instances-pop-form {
|
|
|
|
|
|
.el-form-item {
|
|
|
|
|
|
margin-bottom: unset;
|
2023-02-07 08:39:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2023-08-28 17:25:59 +08:00
|
|
|
|
|
|
|
|
|
|
.key-list-vtree {
|
|
|
|
|
|
height: calc(100vh - 250px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.key-list-vtree .key-list-custom-node {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
/*note the following 2 items should be same value, may not consist with itemSize*/
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
line-height: 22px;
|
|
|
|
|
|
}
|
2021-07-28 18:03:19 +08:00
|
|
|
|
</style>
|