Files
mayfly-go/mayfly_go_web/src/views/ops/redis/DataOperation.vue
2023-03-15 11:41:03 +08:00

458 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<el-row>
<el-col :span="4">
<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>
<img src="@/assets/icon/redis.png" class="img-icon" />
</template>
<template #default>
<el-form class="instances-pop-form" label-width="50px" :size="'small'">
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
<el-form-item label="模式:">{{ data.params.mode }}</el-form-item>
<el-form-item label="链接:">{{ data.params.host }}</el-form-item>
<el-form-item label="备注:">{{
data.params.remark
}}</el-form-item>
</el-form>
</template>
</el-popover>
</span>
<el-icon v-if="data.type == NodeType.Db">
<Coin color="#67c23a" />
</el-icon>
</template>
</tag-tree>
</el-col>
</el-row>
</el-col>
<el-col :span="20" style="border-left: 1px solid var(--el-card-border-color);">
<div class="mt10 ml5">
<el-col>
<el-form class="search-form" label-position="right" :inline="true" label-width="60px">
<el-form-item label="key" label-width="40px">
<el-input placeholder="match 支持*模糊key" style="width: 250px" v-model="scanParam.match"
@clear="clear()" clearable></el-input>
</el-form-item>
<el-form-item label="count" label-width="40px">
<el-input placeholder="count" style="width: 70px" v-model.number="scanParam.count">
</el-input>
</el-form-item>
<el-form-item>
<el-button @click="searchKey()" type="success" icon="search" plain></el-button>
<el-button @click="scan()" icon="bottom" plain>scan</el-button>
<el-popover placement="right" :width="200" trigger="click">
<template #reference>
<el-button type="primary" icon="plus" plain v-auth="'redis:data:save'"></el-button>
</template>
<el-tag @click="onAddData('string')" :color="getTypeColor('string')"
style="cursor: pointer">string</el-tag>
<el-tag @click="onAddData('hash')" :color="getTypeColor('hash')" class="ml5"
style="cursor: pointer">hash</el-tag>
<el-tag @click="onAddData('set')" :color="getTypeColor('set')" class="ml5"
style="cursor: pointer">set</el-tag>
<!-- <el-tag @click="onAddData('list')" :color="getTypeColor('list')" class="ml5" style="cursor: pointer">list</el-tag> -->
</el-popover>
</el-form-item>
<div style="float: right">
<span>keys: {{ state.dbsize }}</span>
</div>
</el-form>
</el-col>
<el-table v-loading="state.loading" :data="state.keys" stripe :highlight-current-row="true"
style="cursor: pointer">
<el-table-column show-overflow-tooltip prop="key" label="key"></el-table-column>
<el-table-column prop="type" label="type" width="80">
<template #default="scope">
<el-tag :color="getTypeColor(scope.row.type)" size="small">{{ scope.row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="ttl" label="ttl(过期时间)" width="140">
<template #default="scope">
{{ ttlConveter(scope.row.ttl) }}
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button @click="getValue(scope.row)" type="success" icon="search" plain
size="small">查看
</el-button>
<el-button v-auth="'redis:data:del'" @click="del(scope.row.key)" type="danger" icon="delete" plain size="small">删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-col>
</el-row>
<div style="text-align: center; margin-top: 10px"></div>
<hash-value v-model:visible="hashValueDialog.visible" :operationType="dataEdit.operationType"
:title="dataEdit.title" :keyInfo="dataEdit.keyInfo" :redisId="scanParam.id" :db="scanParam.db"
@cancel="onCancelDataEdit" @valChange="searchKey" />
<string-value v-model:visible="stringValueDialog.visible" :operationType="dataEdit.operationType"
:title="dataEdit.title" :keyInfo="dataEdit.keyInfo" :redisId="scanParam.id" :db="scanParam.db"
@cancel="onCancelDataEdit" @valChange="searchKey" />
<set-value v-model:visible="setValueDialog.visible" :title="dataEdit.title" :keyInfo="dataEdit.keyInfo"
:redisId="scanParam.id" :db="scanParam.db" :operationType="dataEdit.operationType" @valChange="searchKey"
@cancel="onCancelDataEdit" />
<list-value v-model:visible="listValueDialog.visible" :title="dataEdit.title" :keyInfo="dataEdit.keyInfo"
:redisId="scanParam.id" :db="scanParam.db" :operationType="dataEdit.operationType" @valChange="searchKey"
@cancel="onCancelDataEdit" />
</div>
</template>
<script lang="ts" setup>
import { redisApi } from './api';
import { defineAsyncComponent, toRefs, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { isTrue, notBlank, notNull } from '@/common/assert';
import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
const HashValue = defineAsyncComponent(() => import('./HashValue.vue'));
const StringValue = defineAsyncComponent(() => import('./StringValue.vue'));
const SetValue = defineAsyncComponent(() => import('./SetValue.vue'));
const ListValue = defineAsyncComponent(() => import('./ListValue.vue'));
/**
* 树节点类型
*/
class NodeType {
static Redis = 1
static Db = 2
}
const state = reactive({
instanceMenuMaxHeight: '600',
loading: false,
tags: [],
redisList: [] as any,
dbList: [],
query: {
tagPath: null,
},
scanParam: {
id: null as any,
mode: '',
db: '',
match: null,
count: 10,
cursor: {},
},
dataEdit: {
visible: false,
title: '新增数据',
operationType: 1,
keyInfo: {
type: 'string',
timed: -1,
key: '',
},
},
hashValueDialog: {
visible: false,
},
stringValueDialog: {
visible: false,
},
setValueDialog: {
visible: false,
},
listValueDialog: {
visible: false,
},
keys: [],
dbsize: 0,
});
const {
scanParam,
dataEdit,
hashValueDialog,
stringValueDialog,
setValueDialog,
listValueDialog,
} = toRefs(state)
onMounted(async () => {
setHeight();
})
const setHeight = () => {
state.instanceMenuMaxHeight = window.innerHeight - 115 + 'px';
}
/**
* instmap; tagPaht -> redis info[]
*/
const instMap: Map<string, any[]> = new Map();
const getInsts = async () => {
const res = await redisApi.redisList.request({});
if (!res.total) return
for (const redisInfo of res.list) {
const tagPath = redisInfo.tagPath;
let redisInsts = instMap.get(tagPath) || [];
redisInsts.push(redisInfo);
instMap.set(tagPath, redisInsts);
}
}
/**
* 加载文件树节点
* @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) {
const redisInfos = instMap.get(data.key)
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();
}
}
/**
* 获取所有库信息
* @param redisInfo redis信息
*/
const getDbs = async (redisInfo: any) => {
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
return new TagTreeNode(x, x, NodeType.Db).withIsLeaf(true).withParams({
id: redisInfo.id,
db: x,
name: `db${x}`,
keys: 0,
})
})
const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: "Keyspace" });
for (let db in res.Keyspace) {
for (let d of dbs) {
if (db == d.params.name) {
d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0
}
}
}
// 替换label
dbs.forEach((e: any) => {
e.label = `${e.params.name} [${e.params.keys}]`
});
return dbs;
}
const scan = async () => {
isTrue(state.scanParam.id != null, '请先选择redis');
notBlank(state.scanParam.count, 'count不能为空');
const match: string = state.scanParam.match || '';
if (!match) {
isTrue(state.scanParam.count <= 100, "key搜索条件为空时, count不能大于100")
} 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 {
state.scanParam.count = 100;
}
}
const scanParam = { ...state.scanParam }
// 集群模式count设小点因为后端会从所有master节点scan一遍然后合并结果,默认假设redis集群有3个master
if (scanParam.mode == 'cluster') {
scanParam.count = Math.floor(state.scanParam.count / 3)
}
state.loading = true;
try {
const res = await redisApi.scan.request(scanParam);
state.keys = res.keys;
state.dbsize = res.dbSize;
state.scanParam.cursor = res.cursor;
} finally {
state.loading = false;
}
};
const searchKey = async () => {
state.scanParam.cursor = {};
await scan();
};
const clear = () => {
resetScanParam();
if (state.scanParam.id) {
scan();
}
};
const resetScanParam = () => {
state.scanParam.count = 10;
state.scanParam.match = null;
state.scanParam.cursor = {};
};
const getValue = async (row: any) => {
const type = row.type;
state.dataEdit.keyInfo.type = type;
state.dataEdit.keyInfo.timed = row.ttl;
state.dataEdit.keyInfo.key = row.key;
state.dataEdit.operationType = 2;
state.dataEdit.title = '查看数据';
if (type == 'hash') {
state.hashValueDialog.visible = true;
} else if (type == 'string') {
state.stringValueDialog.visible = true;
} else if (type == 'set') {
state.setValueDialog.visible = true;
} else if (type == 'list') {
state.listValueDialog.visible = true;
} else {
ElMessage.warning('暂不支持该类型');
}
};
const onAddData = (type: string) => {
notNull(state.scanParam.id, '请先选择redis');
state.dataEdit.operationType = 1;
state.dataEdit.title = '新增数据';
state.dataEdit.keyInfo.type = type;
state.dataEdit.keyInfo.timed = -1;
if (type == 'hash') {
state.hashValueDialog.visible = true;
} else if (type == 'string') {
state.stringValueDialog.visible = true;
} else if (type == 'set') {
state.setValueDialog.visible = true;
} else if (type == 'list') {
state.listValueDialog.visible = true;
} else {
ElMessage.warning('暂不支持该类型');
}
};
const onCancelDataEdit = () => {
state.dataEdit.keyInfo = {} as any;
};
const del = (key: string) => {
ElMessageBox.confirm(`确定删除[ ${key} ] 该key?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
redisApi.delKey
.request({
key,
id: state.scanParam.id,
db: state.scanParam.db,
})
.then(() => {
ElMessage.success('删除成功!');
searchKey();
});
}).catch(() => { });
};
const ttlConveter = (ttl: any) => {
if (ttl == -1 || ttl == 0) {
return '永久';
}
if (!ttl) {
ttl = 0;
}
let second = parseInt(ttl); // 秒
let min = 0; // 分
let hour = 0; // 小时
let day = 0;
if (second > 60) {
min = parseInt(second / 60 + '');
second = second % 60;
if (min > 60) {
hour = parseInt(min / 60 + '');
min = min % 60;
if (hour > 24) {
day = parseInt(hour / 24 + '');
hour = hour % 24;
}
}
}
let result = '' + second + 's';
if (min > 0) {
result = '' + min + 'm:' + result;
}
if (hour > 0) {
result = '' + hour + 'h:' + result;
}
if (day > 0) {
result = '' + day + 'd:' + result;
}
return result;
};
const getTypeColor = (type: string) => {
if (type == 'string') {
return '#E4F5EB';
}
if (type == 'hash') {
return '#F9E2AE';
}
if (type == 'set') {
return '#A8DEE0';
}
};
</script>
<style lang="scss">
.instances-pop-form {
.el-form-item {
margin-bottom: unset;
}
}
</style>