feat: mongo优化

This commit is contained in:
meilin.huang
2023-08-25 10:20:32 +08:00
parent a5bcbe151d
commit 2e969d46fb
12 changed files with 728 additions and 343 deletions

View File

@@ -10,7 +10,7 @@ RUN yarn
RUN yarn build
# 构建后端资源
FROM golang:1.20-alpine3.16 as be-builder
FROM golang:1.21.0 as be-builder
ENV GOPROXY https://goproxy.cn
WORKDIR /mayfly
@@ -31,6 +31,9 @@ FROM alpine:3.16
RUN apk add --no-cache ca-certificates bash expat
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/\$TZ /etc/localtime && echo \$TZ > /etc/timezone
WORKDIR /mayfly
COPY --from=be-builder /mayfly/mayfly-go /usr/local/bin/mayfly-go

View File

@@ -1,7 +0,0 @@
export enum ResultEnum {
SUCCESS = 200,
ERROR = 400,
PARAM_ERROR = 405,
SERVER_ERROR = 500,
NO_PERMISSION = 501,
}

View File

@@ -28,7 +28,7 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
let link = document.createElement('a');
let exportContent = '\uFEFF';
let blob = new Blob([exportContent + csvString], {
type: 'text/plain;charset=utrf-8',
type: 'text/plain;charset=utf-8',
});
link.id = 'download-csv';
link.setAttribute('href', URL.createObjectURL(blob));

View File

@@ -35,55 +35,81 @@
</el-col>
<el-col :span="20">
<el-container id="mongo-tab" style="border: 1px solid #eee; margin-top: 1px">
<el-tabs @tab-remove="removeDataTab" style="width: 100%; margin-left: 5px" v-model="state.activeName">
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
<el-row>
<el-col :span="2">
<div>
<el-link @click="findCommand(state.activeName)" icon="refresh" :underline="false" class=""> </el-link>
<el-link @click="onEditDoc(null)" class="ml5" type="primary" icon="plus" :underline="false"> </el-link>
</div>
</el-col>
<el-col :span="22">
<el-input
ref="findParamInputRef"
v-model="dt.findParamStr"
placeholder="点击输入相应查询条件"
@focus="showFindDialog(dt.key)"
>
<template #prepend>查询参数</template>
</el-input>
</el-col>
</el-row>
<el-row :style="`height: ${dataHeight}; overflow: auto;`">
<el-col :span="6" v-for="item in dt.datas" :key="item">
<el-card :body-style="{ padding: '0px', position: 'relative' }">
<el-input type="textarea" v-model="item.value" :rows="10" />
<div style="padding: 3px; float: right" class="mr5 mongo-doc-btns">
<div>
<el-link @click="onEditDoc(item)" :underline="false" type="success" icon="MagicStick"></el-link>
<div id="mongo-tab" style="border: 1px solid #eee; margin-top: 1px">
<el-row v-if="nowColl">
<el-descriptions :column="10" size="small" border>
<!-- <el-descriptions-item label-align="right" label="tag">xxx</el-descriptions-item> -->
<!-- <el-divider direction="vertical" border-style="dashed" /> -->
<el-descriptions-item label="ns" label-align="right">
{{ nowColl.stats.ns }}
</el-descriptions-item>
<el-descriptions-item label="count" label-align="right">
{{ nowColl.stats.count }}
</el-descriptions-item>
<el-descriptions-item label="avgObjSize" label-align="right">
{{ formatByteSize(nowColl.stats.avgObjSize) }}
</el-descriptions-item>
<el-descriptions-item label="size" label-align="right">
{{ formatByteSize(nowColl.stats.size) }}
</el-descriptions-item>
<el-descriptions-item label="totalSize" label-align="right">
{{ formatByteSize(nowColl.stats.totalSize) }}
</el-descriptions-item>
<el-descriptions-item label="storageSize" label-align="right">
{{ formatByteSize(nowColl.stats.storageSize) }}
</el-descriptions-item>
<el-descriptions-item label="freeStorageSize" label-align="right">
{{ formatByteSize(nowColl.stats.freeStorageSize) }}
</el-descriptions-item>
</el-descriptions>
</el-row>
<!-- <el-link @click="onSaveDoc(item.value)" :underline="false"
type="warning" icon="DocumentChecked"></el-link> -->
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?">
<template #reference>
<el-link :underline="false" type="danger" icon="DocumentDelete"> </el-link>
</template>
</el-popconfirm>
</div>
<el-row type="flex">
<el-tabs @tab-remove="removeDataTab" style="width: 100%; margin-left: 5px" v-model="state.activeName">
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
<el-row>
<el-col :span="2">
<div class="mt5">
<el-link @click="findCommand(state.activeName)" icon="refresh" :underline="false" class=""> </el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link v-auth="perms.saveData" @click="onEditDoc(null)" type="primary" icon="plus" :underline="false"> </el-link>
</div>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-container>
</el-col>
<el-col :span="22">
<el-input
ref="findParamInputRef"
v-model="dt.findParamStr"
placeholder="点击输入相应查询条件"
@focus="showFindDialog(dt.key)"
>
<template #prepend>查询参数</template>
</el-input>
</el-col>
</el-row>
<el-row :style="`height: ${dataHeight}; overflow: auto;`">
<el-col :span="6" v-for="item in dt.datas" :key="item">
<el-card :body-style="{ padding: '0px', position: 'relative' }">
<el-input type="textarea" v-model="item.value" :rows="10" />
<div style="padding: 3px; float: right" class="mr5 mongo-doc-btns">
<div>
<el-link @click="onEditDoc(item)" :underline="false" type="success" icon="MagicStick"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?" width="160">
<template #reference>
<el-link v-auth="perms.delData" :underline="false" type="danger" icon="DocumentDelete"> </el-link>
</template>
</el-popconfirm>
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-row>
</div>
</el-col>
</el-row>
@@ -120,7 +146,7 @@
<template #footer>
<div>
<el-button @click="docEditDialog.visible = false"> </el-button>
<el-button @click="onSaveDoc" type="primary"> </el-button>
<el-button v-auth="perms.saveData" @click="onSaveDoc" type="primary"> </el-button>
</div>
</template>
</el-dialog>
@@ -131,7 +157,7 @@
<script lang="ts" setup>
import { mongoApi } from './api';
import { defineAsyncComponent, reactive, ref, toRefs } from 'vue';
import { computed, defineAsyncComponent, reactive, ref, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { isTrue, notBlank } from '@/common/assert';
@@ -141,6 +167,11 @@ import { formatByteSize } from '@/common/utils/format';
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
const perms = {
saveData: 'mongo:data:save',
delData: 'mongo:data:del',
};
/**
* 树节点类型
*/
@@ -154,7 +185,7 @@ class NodeType {
const findParamInputRef: any = ref(null);
const state = reactive({
tags: [],
dataHeight: `${window.innerHeight - 194}px`,
dataHeight: `${window.innerHeight - 194 - 35}px`,
mongoList: [] as any,
activeName: '', // 当前操作的tab
dataTabs: {} as any, // 数据tabs
@@ -185,6 +216,10 @@ const state = reactive({
const { dataHeight, findDialog, docEditDialog } = toRefs(state);
const nowColl = computed(() => {
return getNowDataTab();
});
/**
* instmap; tagPaht -> mongo info[]
*/
@@ -279,15 +314,15 @@ const getCollections = async (id: any, database: string) => {
});
};
const nodeClick = (data: any) => {
const nodeClick = async (data: any) => {
// 点击集合
if (data.type === NodeType.Coll) {
const { id, database, collection } = data.params;
changeCollection(id, database, collection);
await changeCollection(id, database, collection);
}
};
const changeCollection = (id: any, schema: string, collection: string) => {
const changeCollection = async (id: any, schema: string, collection: string) => {
const label = `${id}:\`${schema}\`.${collection}`;
let dataTab = state.dataTabs[label];
if (!dataTab) {
@@ -345,6 +380,7 @@ const findCommand = async (key: string) => {
ElMessage.error('filter或sort字段json字符串值错误。注意: json key需双引号');
return;
}
const datas = await mongoApi.findCommand.request({
id: dataTab.mongoId,
database: dataTab.database,
@@ -355,6 +391,17 @@ const findCommand = async (key: string) => {
skip: findParma.skip || 0,
});
state.dataTabs[key].datas = wrapDatas(datas);
// 获取coll stats
state.dataTabs[key].stats = await mongoApi.runCommand.request({
id: dataTab.mongoId,
database: dataTab.database,
command: [
{
collStats: dataTab.collection,
},
],
});
};
/**

View File

@@ -0,0 +1,352 @@
<template>
<div>
<el-dialog width="800px" title="数据库列表" :before-close="close" v-model="databaseDialog.visible">
<div class="mb5">
<el-button @click="showCreateDbDialog" type="primary" icon="plus" size="small">新建</el-button>
</div>
<el-table :data="databaseDialog.data" :max-height="500">
<el-table-column min-width="130" property="Name" label="库名" />
<el-table-column min-width="90" property="SizeOnDisk" label="size">
<template #default="scope">
{{ formatByteSize(scope.row.SizeOnDisk) }}
</template>
</el-table-column>
<el-table-column min-width="80" property="Empty" label="是否为空" />
<el-table-column min-width="150" label="操作">
<template #default="scope">
<el-link type="success" @click="showDatabaseStats(scope.row.Name)" plain size="small" :underline="false">stats</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link type="primary" @click="showCollections(scope.row.Name)" plain size="small" :underline="false">集合</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteDb(scope.row.Name)" title="确定删除该库?">
<template #reference>
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-dialog width="700px" :title="databaseDialog.statsDialog.title" v-model="databaseDialog.statsDialog.visible">
<el-descriptions title="库状态信息" :column="3" border>
<el-descriptions-item label="db" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.db }}
</el-descriptions-item>
<el-descriptions-item label="collections" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.collections }}
</el-descriptions-item>
<el-descriptions-item label="objects" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.objects }}
</el-descriptions-item>
<el-descriptions-item label="indexes" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.indexes }}
</el-descriptions-item>
<el-descriptions-item label="avgObjSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.avgObjSize) }}
</el-descriptions-item>
<el-descriptions-item label="dataSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.dataSize) }}
</el-descriptions-item>
<el-descriptions-item label="totalSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.totalSize) }}
</el-descriptions-item>
<el-descriptions-item label="storageSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.storageSize) }}
</el-descriptions-item>
<el-descriptions-item label="fsTotalSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.fsTotalSize) }}
</el-descriptions-item>
<el-descriptions-item label="fsUsedSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.fsUsedSize) }}
</el-descriptions-item>
<el-descriptions-item label="indexSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.indexSize) }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</el-dialog>
<el-dialog width="600px" :title="collectionsDialog.title" v-model="collectionsDialog.visible">
<div class="mb5">
<el-button @click="showCreateCollectionDialog" type="primary" icon="plus" size="small">新建</el-button>
</div>
<el-table stripe :data="collectionsDialog.data" :max-height="500">
<el-table-column prop="name" label="名称" show-overflow-tooltip> </el-table-column>
<el-table-column min-width="80" label="操作">
<template #default="scope">
<el-link type="success" @click="showCollectionStats(scope.row.name)" plain size="small" :underline="false">stats</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteCollection(scope.row.name)" width="160" title="确定删除该集合?">
<template #reference>
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-dialog width="700px" :title="collectionsDialog.statsDialog.title" v-model="collectionsDialog.statsDialog.visible">
<el-descriptions title="集合状态信息" :column="3" border>
<el-descriptions-item label="ns" label-align="right" :span="2" align="center">
{{ collectionsDialog.statsDialog.data.ns }}
</el-descriptions-item>
<el-descriptions-item label="count" label-align="right" align="center">
{{ collectionsDialog.statsDialog.data.count }}
</el-descriptions-item>
<el-descriptions-item label="avgObjSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.avgObjSize) }}
</el-descriptions-item>
<el-descriptions-item label="nindexes" label-align="right" align="center">
{{ collectionsDialog.statsDialog.data.nindexes }}
</el-descriptions-item>
<el-descriptions-item label="size" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.size) }}
</el-descriptions-item>
<el-descriptions-item label="totalSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.totalSize) }}
</el-descriptions-item>
<el-descriptions-item label="storageSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.storageSize) }}
</el-descriptions-item>
<el-descriptions-item label="freeStorageSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.freeStorageSize) }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</el-dialog>
<el-dialog width="400px" title="新建库&集合" v-model="createDbDialog.visible" :destroy-on-close="true">
<el-form :model="createDbDialog.form" label-width="auto">
<el-form-item prop="dbName" label="库名" required>
<el-input v-model="createDbDialog.form.dbName" clearable></el-input>
</el-form-item>
<el-form-item prop="collectionName" label="集合名" required>
<el-input v-model="createDbDialog.form.collectionName" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<div>
<el-button @click="createDbDialog.visible = false"> </el-button>
<el-button @click="onCreateDb" type="primary"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog width="400px" title="新建集合" v-model="createCollectionDialog.visible" :destroy-on-close="true">
<el-form :model="createCollectionDialog.form" label-width="auto">
<el-form-item prop="name" label="集合名" required>
<el-input v-model="createCollectionDialog.form.name" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<div>
<el-button @click="createCollectionDialog.visible = false"> </el-button>
<el-button @click="onCreateCollection" type="primary"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { mongoApi } from './api';
import { watch, toRefs, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { formatByteSize } from '@/common/utils/format';
const props = defineProps({
visible: {
type: Boolean,
},
id: {
type: [Number],
required: true,
},
});
//定义事件
const emit = defineEmits(['update:visible']);
const state = reactive({
databaseDialog: {
visible: false,
data: [],
statsDialog: {
visible: false,
data: {} as any,
title: '',
},
},
collectionsDialog: {
database: '',
visible: false,
data: [],
title: '',
statsDialog: {
visible: false,
data: {} as any,
title: '',
},
},
createCollectionDialog: {
visible: false,
form: {
name: '',
},
},
createDbDialog: {
visible: false,
form: {
dbName: '',
collectionName: '',
},
},
});
const { databaseDialog, collectionsDialog, createCollectionDialog, createDbDialog } = toRefs(state);
watch(props, async (newValue: any) => {
if (!newValue.visible) {
state.databaseDialog.visible = false;
return;
}
showDatabases();
});
const close = () => {
emit('update:visible', false);
};
const showDatabases = async () => {
state.databaseDialog.data = (await mongoApi.databases.request({ id: props.id })).Databases;
state.databaseDialog.visible = true;
};
const showDatabaseStats = async (dbName: string) => {
state.databaseDialog.statsDialog.data = await mongoApi.runCommand.request({
id: props.id,
database: dbName,
command: [
{
dbStats: 1,
},
],
});
state.databaseDialog.statsDialog.title = `'${dbName}' stats`;
state.databaseDialog.statsDialog.visible = true;
};
const showCollections = async (database: string) => {
state.collectionsDialog.database = database;
state.collectionsDialog.data = [];
setCollections(database);
state.collectionsDialog.title = `'${database}' 集合`;
state.collectionsDialog.visible = true;
};
const setCollections = async (database: string) => {
const res = await mongoApi.collections.request({ id: props.id, database });
const collections = [] as any;
for (let r of res) {
collections.push({ name: r });
}
state.collectionsDialog.data = collections;
};
/**
* 显示集合状态
*/
const showCollectionStats = async (collection: string) => {
state.collectionsDialog.statsDialog.data = await mongoApi.runCommand.request({
id: props.id,
database: state.collectionsDialog.database,
command: [
{
collStats: collection,
},
],
});
state.collectionsDialog.statsDialog.title = `'${collection}' stats`;
state.collectionsDialog.statsDialog.visible = true;
};
/**
* 删除集合
*/
const onDeleteCollection = async (collection: string) => {
await mongoApi.runCommand.request({
id: props.id,
database: state.collectionsDialog.database,
command: [
{
drop: collection,
},
],
});
ElMessage.success('集合删除成功');
setCollections(state.collectionsDialog.database);
};
const showCreateCollectionDialog = () => {
state.createCollectionDialog.visible = true;
};
const onCreateCollection = async () => {
const form = state.createCollectionDialog.form;
await mongoApi.runCommand.request({
id: props.id,
database: state.collectionsDialog.database,
command: [
{
create: form.name,
},
],
});
ElMessage.success('集合创建成功');
state.createCollectionDialog.visible = false;
state.createCollectionDialog.form = {} as any;
setCollections(state.collectionsDialog.database);
};
const showCreateDbDialog = () => {
state.createDbDialog.visible = true;
};
const onCreateDb = async () => {
const form = state.createDbDialog.form;
await mongoApi.runCommand.request({
id: props.id,
database: form.dbName,
command: [
{
create: form.collectionName,
},
],
});
ElMessage.success('数据库与集合创建成功');
state.createDbDialog.visible = false;
state.createDbDialog.form = {} as any;
showDatabases();
};
const onDeleteDb = async (db: string) => {
await mongoApi.runCommand.request({
id: props.id,
database: db,
command: [
{
dropDatabase: 1,
},
],
});
ElMessage.success('库删除成功');
showDatabases();
};
</script>
<style></style>

View File

@@ -34,136 +34,15 @@
<template #action="{ data }">
<el-button @click="showDatabases(data.id)" link>数据库</el-button>
<el-button type="primary" @click="editMongo(data)" link>编辑</el-button>
<el-button @click="showUsers(data.id)" link type="success">cmd</el-button>
<el-button @click="editMongo(data)" link type="primary">编辑</el-button>
</template>
</page-table>
<el-dialog width="800px" :title="databaseDialog.title" v-model="databaseDialog.visible">
<el-table :data="databaseDialog.data" size="small">
<el-table-column min-width="130" property="Name" label="库名" />
<el-table-column min-width="90" property="SizeOnDisk" label="size">
<template #default="scope">
{{ formatByteSize(scope.row.SizeOnDisk) }}
</template>
</el-table-column>
<el-table-column min-width="80" property="Empty" label="是否为空" />
<mongo-dbs v-model:visible="dbsVisible" :id="state.dbOps.dbId"></mongo-dbs>
<el-table-column min-width="150" label="操作">
<template #default="scope">
<el-link type="success" @click="showDatabaseStats(scope.row.Name)" plain size="small" :underline="false">stats</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link type="primary" @click="showCollections(scope.row.Name)" plain size="small" :underline="false">集合</el-link>
</template>
</el-table-column>
</el-table>
<el-dialog width="700px" :title="databaseDialog.statsDialog.title" v-model="databaseDialog.statsDialog.visible">
<el-descriptions title="库状态信息" :column="3" border size="small">
<el-descriptions-item label="db" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.db }}
</el-descriptions-item>
<el-descriptions-item label="collections" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.collections }}
</el-descriptions-item>
<el-descriptions-item label="objects" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.objects }}
</el-descriptions-item>
<el-descriptions-item label="indexes" label-align="right" align="center">
{{ databaseDialog.statsDialog.data.indexes }}
</el-descriptions-item>
<el-descriptions-item label="avgObjSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.avgObjSize) }}
</el-descriptions-item>
<el-descriptions-item label="dataSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.dataSize) }}
</el-descriptions-item>
<el-descriptions-item label="totalSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.totalSize) }}
</el-descriptions-item>
<el-descriptions-item label="storageSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.storageSize) }}
</el-descriptions-item>
<el-descriptions-item label="fsTotalSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.fsTotalSize) }}
</el-descriptions-item>
<el-descriptions-item label="fsUsedSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.fsUsedSize) }}
</el-descriptions-item>
<el-descriptions-item label="indexSize" label-align="right" align="center">
{{ formatByteSize(databaseDialog.statsDialog.data.indexSize) }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</el-dialog>
<el-dialog width="600px" :title="collectionsDialog.title" v-model="collectionsDialog.visible">
<div>
<el-button @click="showCreateCollectionDialog" type="primary" icon="plus" size="small">新建</el-button>
</div>
<el-table border stripe :data="collectionsDialog.data" size="small">
<el-table-column prop="name" label="名称" show-overflow-tooltip> </el-table-column>
<el-table-column min-width="80" label="操作">
<template #default="scope">
<el-link type="success" @click="showCollectionStats(scope.row.name)" plain size="small" :underline="false">stats</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteCollection(scope.row.name)" title="确定删除该集合?">
<template #reference>
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-dialog width="700px" :title="collectionsDialog.statsDialog.title" v-model="collectionsDialog.statsDialog.visible">
<el-descriptions title="集合状态信息" :column="3" border size="small">
<el-descriptions-item label="ns" label-align="right" :span="2" align="center">
{{ collectionsDialog.statsDialog.data.ns }}
</el-descriptions-item>
<el-descriptions-item label="count" label-align="right" align="center">
{{ collectionsDialog.statsDialog.data.count }}
</el-descriptions-item>
<el-descriptions-item label="avgObjSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.avgObjSize) }}
</el-descriptions-item>
<el-descriptions-item label="nindexes" label-align="right" align="center">
{{ collectionsDialog.statsDialog.data.nindexes }}
</el-descriptions-item>
<el-descriptions-item label="size" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.size) }}
</el-descriptions-item>
<el-descriptions-item label="totalSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.totalSize) }}
</el-descriptions-item>
<el-descriptions-item label="storageSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.storageSize) }}
</el-descriptions-item>
<el-descriptions-item label="freeStorageSize" label-align="right" align="center">
{{ formatByteSize(collectionsDialog.statsDialog.data.freeStorageSize) }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</el-dialog>
<el-dialog width="400px" title="新建集合" v-model="createCollectionDialog.visible" :destroy-on-close="true">
<el-form :model="createCollectionDialog.form" label-width="auto">
<el-form-item prop="name" label="集合名" required>
<el-input v-model="createCollectionDialog.form.name" clearable></el-input>
</el-form-item>
<!-- <el-form-item label="描述:">
<el-input v-model="showEnvDialog.envForm.remark" auto-complete="off"></el-input>
</el-form-item> -->
</el-form>
<template #footer>
<div>
<el-button @click="createCollectionDialog.visible = false"> </el-button>
<el-button @click="onCreateCollection" type="primary"> </el-button>
</div>
</template>
</el-dialog>
<mongo-run-command v-model:visible="usersVisible" :id="state.dbOps.dbId" />
<mongo-edit
@val-change="valChange"
@@ -176,14 +55,16 @@
<script lang="ts" setup>
import { mongoApi } from './api';
import { ref, toRefs, reactive, onMounted } from 'vue';
import { defineAsyncComponent, ref, toRefs, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import MongoEdit from './MongoEdit.vue';
import { formatByteSize } from '@/common/utils/format';
import TagInfo from '../component/TagInfo.vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable';
const MongoEdit = defineAsyncComponent(() => import('./MongoEdit.vue'));
const MongoDbs = defineAsyncComponent(() => import('./MongoDbs.vue'));
const MongoRunCommand = defineAsyncComponent(() => import('./MongoRunCommand.vue'));
const pageTableRef: any = ref(null);
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')];
@@ -193,7 +74,7 @@ const columns = ref([
TableColumn.new('uri', '连接uri'),
TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('creator', '创建人'),
TableColumn.new('action', '操作').isSlot().setMinWidth(100).fixedRight().alignCenter(),
TableColumn.new('action', '操作').isSlot().setMinWidth(145).fixedRight().alignCenter(),
]);
const state = reactive({
@@ -215,126 +96,24 @@ const state = reactive({
data: null as any,
title: '新增mongo',
},
databaseDialog: {
visible: false,
data: [],
title: '',
statsDialog: {
visible: false,
data: {} as any,
title: '',
},
},
collectionsDialog: {
database: '',
visible: false,
data: [],
title: '',
statsDialog: {
visible: false,
data: {} as any,
title: '',
},
},
createCollectionDialog: {
visible: false,
form: {
name: '',
},
},
dbsVisible: false,
usersVisible: false,
});
const { tags, list, total, selectionData, query, mongoEditDialog, databaseDialog, collectionsDialog, createCollectionDialog } = toRefs(state);
const { tags, list, total, selectionData, query, mongoEditDialog, dbsVisible, usersVisible } = toRefs(state);
onMounted(async () => {
search();
});
const showDatabases = async (id: number) => {
// state.query.tagPath = row.tagPath
state.dbOps.dbId = id;
state.databaseDialog.data = (await mongoApi.databases.request({ id })).Databases;
state.databaseDialog.title = `数据库列表`;
state.databaseDialog.visible = true;
state.dbsVisible = true;
};
const showDatabaseStats = async (dbName: string) => {
state.databaseDialog.statsDialog.data = await mongoApi.runCommand.request({
id: state.dbOps.dbId,
database: dbName,
command: {
dbStats: 1,
},
});
state.databaseDialog.statsDialog.title = `'${dbName}' stats`;
state.databaseDialog.statsDialog.visible = true;
};
const showCollections = async (database: string) => {
state.collectionsDialog.database = database;
state.collectionsDialog.data = [];
setCollections(database);
state.collectionsDialog.title = `'${database}' 集合`;
state.collectionsDialog.visible = true;
};
const setCollections = async (database: string) => {
const res = await mongoApi.collections.request({ id: state.dbOps.dbId, database });
const collections = [] as any;
for (let r of res) {
collections.push({ name: r });
}
state.collectionsDialog.data = collections;
};
/**
* 显示集合状态
*/
const showCollectionStats = async (collection: string) => {
state.collectionsDialog.statsDialog.data = await mongoApi.runCommand.request({
id: state.dbOps.dbId,
database: state.collectionsDialog.database,
command: {
collStats: collection,
},
});
state.collectionsDialog.statsDialog.title = `'${collection}' stats`;
state.collectionsDialog.statsDialog.visible = true;
};
/**
* 删除集合
*/
const onDeleteCollection = async (collection: string) => {
await mongoApi.runCommand.request({
id: state.dbOps.dbId,
database: state.collectionsDialog.database,
command: {
drop: collection,
},
});
ElMessage.success('集合删除成功');
setCollections(state.collectionsDialog.database);
};
const showCreateCollectionDialog = () => {
state.createCollectionDialog.visible = true;
};
const onCreateCollection = async () => {
const form = state.createCollectionDialog.form;
await mongoApi.runCommand.request({
id: state.dbOps.dbId,
database: state.collectionsDialog.database,
command: {
create: form.name,
},
});
ElMessage.success('集合创建成功');
state.createCollectionDialog.visible = false;
state.createCollectionDialog.form = {} as any;
setCollections(state.collectionsDialog.database);
const showUsers = async (id: number) => {
state.dbOps.dbId = id;
state.usersVisible = true;
};
const deleteMongo = async () => {

View File

@@ -0,0 +1,196 @@
<template>
<div>
<el-dialog width="700px" title="runCommand" v-model="runCmdDialog.visible" :before-close="close" :destroy-on-close="true">
<el-form label-width="auto">
<el-row class="mb10">
<el-col :span="12">
<el-form-item label="模板">
<el-select class="w100" @change="changeCmd" filterable v-model="runCmdDialog.cmdName" placeholder="选择命令模板">
<el-option v-for="item in mongoCmds" :key="item.name" :label="`${item.name} | ${item.description}`" :value="item.name" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="库">
<el-select v-model="runCmdDialog.db" filterable placeholder="选择库">
<el-option v-for="item in dbs" :key="item.Name" :label="item.Name" :value="item.Name" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item class="ml10">
<el-button @click="onRunCommand" type="primary">Run</el-button>
<el-tooltip effect="dark" placement="top">
<template #content> 更多命令查看-> https://www.mongodb.com/docs/manual/reference/command/ </template>
<span class="ml10">
<el-icon><InfoFilled /></el-icon>
</span>
</el-tooltip>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="cmd">
<monaco-editor style="width: 100%" height="235px" v-model="runCmdDialog.cmd" language="json" />
</el-form-item>
<el-form-item label="res">
<monaco-editor style="width: 100%" height="235px" v-model="runCmdDialog.cmdRes" language="json" />
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { mongoApi } from './api';
import { watch, defineAsyncComponent, toRefs, reactive } from 'vue';
import { ElMessage } from 'element-plus';
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
const props = defineProps({
visible: {
type: Boolean,
},
id: {
type: [Number],
required: true,
},
});
//定义事件
const emit = defineEmits(['update:visible']);
const mongoCmds = {
usersInfo: {
name: 'usersInfo',
description: '获取用户信息',
cmd: {
usersInfo: 1,
showCredentials: false,
showCustomData: false,
showPrivileges: false,
showAuthenticationRestrictions: false,
filter: {},
},
},
createUser: {
name: 'createUser',
description: '创建新用户',
cmd: {
createUser: '<username>',
pwd: '<cleartext password>',
roles: [
{
role: '<role>',
db: '<database>',
},
],
},
},
grantRolesToUser: {
name: 'grantRolesToUser',
description: '授予对用户的额外角色',
cmd: {
grantRolesToUser: '<user>',
roles: [''],
},
},
dropUser: {
name: 'dropUser',
description: '删除用户',
cmd: {
dropUser: '<user>',
},
},
roleInfo: {
name: 'roleInfo',
description: '获取角色信息',
cmd: {
rolesInfo: 1,
showAuthenticationRestrictions: false,
showBuiltinRoles: true,
showPrivileges: false,
},
},
createRole: {
name: 'createRole',
description: '创建角色',
cmd: {
createRole: '<new role>',
privileges: [{ resource: {}, actions: ['<action>'] }],
roles: [{ role: '<role>', db: '<database>' }],
authenticationRestrictions: [
{
clientSource: ['<IP> | <CIDR range>'],
serverAddress: ['<IP> |<CIDR range>'],
},
],
writeConcern: '<write concern document>',
comment: '<any>',
},
},
};
const state = reactive({
dbs: [] as any,
selectDbDisabled: false,
runCmdDialog: {
visible: false,
cmdName: '',
db: '',
cmd: '',
cmdRes: '',
},
});
const { dbs, runCmdDialog } = toRefs(state);
watch(props, async (newValue: any) => {
if (!newValue.visible) {
state.runCmdDialog.visible = false;
return;
}
state.runCmdDialog.visible = newValue.visible;
state.dbs = (await mongoApi.databases.request({ id: props.id })).Databases;
});
const close = () => {
emit('update:visible', false);
state.runCmdDialog.cmd = '';
state.runCmdDialog.cmdRes = '';
state.runCmdDialog.cmdName = '';
state.runCmdDialog.db = '';
state.dbs = [];
};
const changeCmd = (val: any) => {
const mongoCmd = mongoCmds[val];
state.runCmdDialog.cmd = JSON.stringify(mongoCmd.cmd, null, 4);
state.runCmdDialog.db = state?.dbs[0]?.Name;
state.runCmdDialog.cmdRes = '';
};
const onRunCommand = async () => {
const orderCmds = [] as any;
const cmdObj = JSON.parse(state.runCmdDialog.cmd);
for (let item of Object.keys(cmdObj)) {
let obj = {};
obj[item] = cmdObj[item];
orderCmds.push(obj);
}
state.runCmdDialog.cmdRes = '';
const res = await mongoApi.runCommand.request({
id: props.id,
database: state.runCmdDialog.db,
command: orderCmds,
});
state.runCmdDialog.cmdRes = JSON.stringify(res, null, 4);
ElMessage.success('执行成功');
};
</script>
<style></style>

View File

@@ -701,10 +701,10 @@ concat-map@0.0.1:
resolved "https://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
countup.js@^2.0.7:
version "2.0.8"
resolved "https://registry.nlark.com/countup.js/download/countup.js-2.0.8.tgz"
integrity sha1-7KDDHJ2z93acuklNkxXNUtuq8bk=
countup.js@^2.7.0:
version "2.7.0"
resolved "https://registry.npmmirror.com/countup.js/-/countup.js-2.7.0.tgz#a5521bd935f0ae83417d0128e73f2a2d2543c9c7"
integrity sha512-IP9nYLGgW//0If73eXQdFlReGhpFGHaStqB1v82FknxnUWueF6HFuuOXySW4sEDMc88PsZL1EOn/NPkfTZalmQ==
cropperjs@^1.5.11:
version "1.5.12"
@@ -1414,10 +1414,10 @@ mitt@^3.0.1:
resolved "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
monaco-editor@^0.40.0:
version "0.40.0"
resolved "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.40.0.tgz#d10834e15ad50a15ec61fd01892e508464ebe2fe"
integrity sha512-1wymccLEuFSMBvCk/jT1YDW/GuxMLYwnFwF9CDyYCxoTw2Pt379J3FUhwy9c43j51JdcxVPjwk0jm0EVDsBS2g==
monaco-editor@^0.41.0:
version "0.41.0"
resolved "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.41.0.tgz#2ba31e5af7e3ae93ac5d7467ec2772ef9b3d967f"
integrity sha512-1o4olnZJsiLmv5pwLEAmzHTE/5geLKQ07BrGxlF4Ri/AXAc2yyDGZwHjiTqD8D/ROKUZmwMA28A+yEowLNOEcA==
monaco-sql-languages@^0.11.0:
version "0.11.0"
@@ -1575,7 +1575,7 @@ postcss@^8.1.10:
picocolors "^1.0.0"
source-map-js "^1.0.1"
postcss@^8.4.26:
postcss@^8.4.27:
version "8.4.27"
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
@@ -1666,10 +1666,10 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rollup@^3.25.2:
version "3.26.2"
resolved "https://registry.npmmirror.com/rollup/-/rollup-3.26.2.tgz#2e76a37606cb523fc9fef43e6f59c93f86d95e7c"
integrity sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==
rollup@^3.27.1:
version "3.28.0"
resolved "https://registry.npmmirror.com/rollup/-/rollup-3.28.0.tgz#a3c70004b01934760c0cb8df717c7a1d932389a2"
integrity sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==
optionalDependencies:
fsevents "~2.3.2"
@@ -1855,14 +1855,14 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
vite@^4.4.7:
version "4.4.7"
resolved "https://registry.npmmirror.com/vite/-/vite-4.4.7.tgz#71b8a37abaf8d50561aca084dbb77fa342824154"
integrity sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==
vite@^4.4.9:
version "4.4.9"
resolved "https://registry.npmmirror.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d"
integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==
dependencies:
esbuild "^0.18.10"
postcss "^8.4.26"
rollup "^3.25.2"
postcss "^8.4.27"
rollup "^3.27.1"
optionalDependencies:
fsevents "~2.3.2"

View File

@@ -1,30 +1,33 @@
module mayfly-go
go 1.20
go 1.21
require (
github.com/buger/jsonparser v1.1.1
github.com/gin-gonic/gin v1.9.1
github.com/go-gormigrate/gormigrate/v2 v2.1.0
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.14.0
github.com/go-sql-driver/mysql v1.7.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/gorilla/websocket v1.5.0
github.com/lib/pq v1.10.7
github.com/lib/pq v1.10.9
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
github.com/mojocn/base64Captcha v1.3.5 //
github.com/pkg/sftp v1.13.5
github.com/pkg/sftp v1.13.6
github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.0.5
github.com/redis/go-redis/v9 v9.1.0
github.com/robfig/cron/v3 v3.0.1 //
github.com/sirupsen/logrus v1.9.3
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
go.mongodb.org/mongo-driver v1.11.4 // mongo
golang.org/x/crypto v0.11.0 // ssh
golang.org/x/oauth2 v0.10.0
go.mongodb.org/mongo-driver v1.12.1 // mongo
golang.org/x/crypto v0.12.0 // ssh
golang.org/x/oauth2 v0.11.0
gopkg.in/yaml.v3 v3.0.1
// gorm
gorm.io/driver/mysql v1.5.1
gorm.io/gorm v1.25.2
gorm.io/gorm v1.25.4
)
require (
@@ -35,9 +38,6 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
@@ -48,25 +48,26 @@ require (
github.com/klauspost/compress v1.13.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

View File

@@ -16,8 +16,8 @@ type MongoCommand struct {
}
type MongoRunCommand struct {
Database string `binding:"required" json:"database"`
Command map[string]any `json:"command"`
Database string `binding:"required" json:"database"`
Command []map[string]any `json:"command"`
}
type MongoFindCommand struct {

View File

@@ -91,12 +91,24 @@ func (m *Mongo) RunCommand(rc *req.Ctx) {
commandForm := new(form.MongoRunCommand)
ginx.BindJsonAndValid(rc.GinCtx, commandForm)
cli := m.MongoApp.GetMongoCli(m.GetMongoId(rc.GinCtx))
rc.ReqParam = commandForm
// 顺序执行
commands := bson.D{}
for _, cmd := range commandForm.Command {
e := bson.E{}
for k, v := range cmd {
e.Key = k
e.Value = v
}
commands = append(commands, e)
}
ctx := context.TODO()
var bm bson.M
err := cli.Database(commandForm.Database).RunCommand(
ctx,
commandForm.Command,
commands,
).Decode(&bm)
biz.ErrIsNilAppendErr(err, "执行命令失败: %s")

View File

@@ -17,6 +17,8 @@ func InitMongoRouter(router *gin.RouterGroup) {
TagApp: tagapp.GetTagTreeApp(),
}
saveDataPerm := req.NewPermission("mongo:data:save")
reqs := [...]*req.Conf{
// 获取所有mongo列表
req.NewGet("", ma.Mongos),
@@ -39,13 +41,13 @@ func InitMongoRouter(router *gin.RouterGroup) {
// 执行mongo find命令
req.NewPost(":id/command/find", ma.FindCommand),
req.NewPost(":id/command/update-by-id", ma.UpdateByIdCommand).Log(req.NewLogSave("mongo-更新文档")),
req.NewPost(":id/command/update-by-id", ma.UpdateByIdCommand).RequiredPermission(saveDataPerm).Log(req.NewLogSave("mongo-更新文档")),
// 执行mongo delete by id命令
req.NewPost(":id/command/delete-by-id", ma.DeleteByIdCommand).Log(req.NewLogSave("mongo-删除文档")),
req.NewPost(":id/command/delete-by-id", ma.DeleteByIdCommand).RequiredPermission(req.NewPermission("mongo:data:del")).Log(req.NewLogSave("mongo-删除文档")),
// 执行mongo insert 命令
req.NewPost(":id/command/insert", ma.InsertOneCommand).Log(req.NewLogSave("mogno-插入文档")),
req.NewPost(":id/command/insert", ma.InsertOneCommand).RequiredPermission(saveDataPerm).Log(req.NewLogSave("mogno-插入文档")),
}
req.BatchSetGroup(m, reqs[:])