feat: Redis 支持远程数据库 (#5014)

This commit is contained in:
ssongliu 2024-05-15 11:56:40 +08:00 committed by GitHub
parent c56435970a
commit 94cebfd8cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 796 additions and 35 deletions

View File

@ -16,7 +16,7 @@ import (
// @Param request body dto.OperationWithName true "request"
// @Success 200 {object} dto.RedisStatus
// @Security ApiKeyAuth
// @Router /databases/redis/status [get]
// @Router /databases/redis/status [post]
func (b *BaseApi) LoadRedisStatus(c *gin.Context) {
var req dto.OperationWithName
if err := helper.CheckBind(&req, c); err != nil {
@ -38,7 +38,7 @@ func (b *BaseApi) LoadRedisStatus(c *gin.Context) {
// @Param request body dto.OperationWithName true "request"
// @Success 200 {object} dto.RedisConf
// @Security ApiKeyAuth
// @Router /databases/redis/conf [get]
// @Router /databases/redis/conf [post]
func (b *BaseApi) LoadRedisConf(c *gin.Context) {
var req dto.OperationWithName
if err := helper.CheckBind(&req, c); err != nil {
@ -60,7 +60,7 @@ func (b *BaseApi) LoadRedisConf(c *gin.Context) {
// @Param request body dto.OperationWithName true "request"
// @Success 200 {object} dto.RedisPersistence
// @Security ApiKeyAuth
// @Router /databases/redis/persistence/conf [get]
// @Router /databases/redis/persistence/conf [post]
func (b *BaseApi) LoadPersistenceConf(c *gin.Context) {
var req dto.OperationWithName
if err := helper.CheckBind(&req, c); err != nil {
@ -75,6 +75,25 @@ func (b *BaseApi) LoadPersistenceConf(c *gin.Context) {
helper.SuccessWithData(c, data)
}
func (b *BaseApi) CheckHasCli(c *gin.Context) {
helper.SuccessWithData(c, redisService.CheckHasCli())
}
// @Tags Database Redis
// @Summary Install redis-cli
// @Description 安装 redis cli
// @Success 200
// @Security ApiKeyAuth
// @Router /databases/redis/install/cli [post]
func (b *BaseApi) InstallCli(c *gin.Context) {
if err := redisService.InstallCli(); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags Database Redis
// @Summary Update redis conf
// @Description 更新 redis 配置信息

View File

@ -3,6 +3,7 @@ package v1
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
@ -88,23 +89,38 @@ func (b *BaseApi) RedisWsSsh(c *gin.Context) {
return
}
name := c.Query("name")
redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Type: "redis", Name: name})
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
from := c.Query("from")
defer wsConn.Close()
commands := []string{"redis-cli"}
if len(redisInfo.Password) != 0 {
commands = []string{"redis-cli", "-a", redisInfo.Password, "--no-auth-warning"}
database, err := databaseService.Get(name)
if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) {
return
}
pidMap := loadMapFromDockerTop(redisInfo.Password)
itemCmds := append([]string{"exec", "-it", redisInfo.ContainerName}, commands...)
if from == "local" {
redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"})
if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) {
return
}
name = redisInfo.ContainerName
if len(database.Password) != 0 {
commands = []string{"redis-cli", "-a", database.Password, "--no-auth-warning"}
}
} else {
itemPort := fmt.Sprintf("%v", database.Port)
commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort}
if len(database.Password) != 0 {
commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort, "-a", database.Password, "--no-auth-warning"}
}
name = "1Panel-redis-cli-tools"
}
pidMap := loadMapFromDockerTop(name)
itemCmds := append([]string{"exec", "-it", name}, commands...)
slave, err := terminal.NewCommand(itemCmds)
if wshandleError(wsConn, err) {
return
}
defer killBash(redisInfo.ContainerName, strings.Join(commands, " "), pidMap)
defer killBash(name, strings.Join(commands, " "), pidMap)
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)

View File

@ -7,7 +7,8 @@ import (
"path"
"github.com/1Panel-dev/1Panel/backend/utils/postgresql"
client2 "github.com/1Panel-dev/1Panel/backend/utils/postgresql/client"
pg_client "github.com/1Panel-dev/1Panel/backend/utils/postgresql/client"
redis_client "github.com/1Panel-dev/1Panel/backend/utils/redis"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr"
@ -113,7 +114,7 @@ func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) {
func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
switch req.Type {
case constant.AppPostgresql:
_, err := postgresql.NewPostgresqlClient(client2.DBInfo{
_, err := postgresql.NewPostgresqlClient(pg_client.DBInfo{
From: "remote",
Address: req.Address,
Port: req.Port,
@ -122,6 +123,13 @@ func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
Timeout: 6,
})
return err == nil
case constant.AppRedis:
_, err := redis_client.NewRedisClient(redis_client.DBInfo{
Address: req.Address,
Port: req.Port,
Password: req.Password,
})
return err == nil
case "mysql", "mariadb":
_, err := mysql.NewMysqlClient(client.DBInfo{
From: "remote",
@ -153,7 +161,7 @@ func (u *DatabaseService) Create(req dto.DatabaseCreate) error {
}
switch req.Type {
case constant.AppPostgresql:
if _, err := postgresql.NewPostgresqlClient(client2.DBInfo{
if _, err := postgresql.NewPostgresqlClient(pg_client.DBInfo{
From: "remote",
Address: req.Address,
Port: req.Port,
@ -163,6 +171,14 @@ func (u *DatabaseService) Create(req dto.DatabaseCreate) error {
}); err != nil {
return err
}
case constant.AppRedis:
if _, err := redis_client.NewRedisClient(redis_client.DBInfo{
Address: req.Address,
Port: req.Port,
Password: req.Password,
}); err != nil {
return err
}
case "mysql", "mariadb":
if _, err := mysql.NewMysqlClient(client.DBInfo{
From: "remote",
@ -249,7 +265,7 @@ func (u *DatabaseService) Delete(req dto.DatabaseDelete) error {
func (u *DatabaseService) Update(req dto.DatabaseUpdate) error {
switch req.Type {
case constant.AppPostgresql:
if _, err := postgresql.NewPostgresqlClient(client2.DBInfo{
if _, err := postgresql.NewPostgresqlClient(pg_client.DBInfo{
From: "remote",
Address: req.Address,
Port: req.Port,

View File

@ -1,6 +1,7 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
@ -11,6 +12,8 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/docker/docker/api/types/container"
_ "github.com/go-sql-driver/mysql"
)
@ -24,6 +27,9 @@ type IRedisService interface {
LoadStatus(req dto.OperationWithName) (*dto.RedisStatus, error)
LoadConf(req dto.OperationWithName) (*dto.RedisConf, error)
LoadPersistenceConf(req dto.OperationWithName) (*dto.RedisPersistence, error)
CheckHasCli() bool
InstallCli() error
}
func NewIRedisService() IRedisService {
@ -50,6 +56,33 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
return nil
}
func (u *RedisService) CheckHasCli() bool {
client, err := docker.NewDockerClient()
if err != nil {
return false
}
defer client.Close()
containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
if err != nil {
return false
}
for _, item := range containerLists {
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
return true
}
}
return false
}
func (u *RedisService) InstallCli() error {
item := dto.ContainerOperate{
Name: "1Panel-redis-cli-tools",
Image: "redis:7.2.4",
Network: "1panel-network",
}
return NewIContainerService().ContainerCreate(item)
}
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
if err := updateInstallInfoInDB("redis", req.Database, "password", req.Value); err != nil {
return err

View File

@ -39,6 +39,8 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.POST("/redis/status", baseApi.LoadRedisStatus)
cmdRouter.POST("/redis/conf", baseApi.LoadRedisConf)
cmdRouter.GET("/redis/exec", baseApi.RedisWsSsh)
cmdRouter.GET("/redis/check", baseApi.CheckHasCli)
cmdRouter.POST("/redis/install/cli", baseApi.InstallCli)
cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword)
cmdRouter.POST("/redis/conf/update", baseApi.UpdateRedisConf)
cmdRouter.POST("/redis/persistence/update", baseApi.UpdateRedisPersistenceConf)

View File

@ -0,0 +1,26 @@
package redis
import (
"fmt"
"github.com/go-redis/redis"
)
type DBInfo struct {
Address string `json:"address"`
Port uint `json:"port"`
Password string `json:"password"`
}
func NewRedisClient(conn DBInfo) (*redis.Client, error) {
client := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%v", conn.Address, conn.Port),
Password: conn.Password,
DB: 0,
})
if _, err := client.Ping().Result(); err != nil {
return client, err
}
return client, nil
}

View File

@ -5110,7 +5110,7 @@ const docTemplate = `{
}
},
"/databases/redis/conf": {
"get": {
"post": {
"security": [
{
"ApiKeyAuth": []
@ -5185,6 +5185,25 @@ const docTemplate = `{
}
}
},
"/databases/redis/install/cli": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "安装 redis cli",
"tags": [
"Database Redis"
],
"summary": "Install redis-cli",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/databases/redis/password": {
"post": {
"security": [
@ -5226,7 +5245,7 @@ const docTemplate = `{
}
},
"/databases/redis/persistence/conf": {
"get": {
"post": {
"security": [
{
"ApiKeyAuth": []
@ -5302,7 +5321,7 @@ const docTemplate = `{
}
},
"/databases/redis/status": {
"get": {
"post": {
"security": [
{
"ApiKeyAuth": []

View File

@ -5103,7 +5103,7 @@
}
},
"/databases/redis/conf": {
"get": {
"post": {
"security": [
{
"ApiKeyAuth": []
@ -5178,6 +5178,25 @@
}
}
},
"/databases/redis/install/cli": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "安装 redis cli",
"tags": [
"Database Redis"
],
"summary": "Install redis-cli",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/databases/redis/password": {
"post": {
"security": [
@ -5219,7 +5238,7 @@
}
},
"/databases/redis/persistence/conf": {
"get": {
"post": {
"security": [
{
"ApiKeyAuth": []
@ -5295,7 +5314,7 @@
}
},
"/databases/redis/status": {
"get": {
"post": {
"security": [
{
"ApiKeyAuth": []

View File

@ -8349,7 +8349,7 @@ paths:
tags:
- Database Postgresql
/databases/redis/conf:
get:
post:
consumes:
- application/json
description: 获取 redis 配置信息
@ -8396,6 +8396,17 @@ paths:
formatEN: update the redis database configuration information
formatZH: 更新 redis 数据库配置信息
paramKeys: []
/databases/redis/install/cli:
post:
description: 安装 redis cli
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Install redis-cli
tags:
- Database Redis
/databases/redis/password:
post:
consumes:
@ -8423,7 +8434,7 @@ paths:
formatZH: 修改 redis 数据库密码
paramKeys: []
/databases/redis/persistence/conf:
get:
post:
consumes:
- application/json
description: 获取 redis 持久化配置
@ -8471,7 +8482,7 @@ paths:
formatZH: redis 数据库持久化配置更新
paramKeys: []
/databases/redis/status:
get:
post:
consumes:
- application/json
description: 获取 redis 状态信息

View File

@ -117,6 +117,12 @@ export const loadRedisConf = (database: string) => {
export const redisPersistenceConf = (database: string) => {
return http.post<Database.RedisPersistenceConf>(`/databases/redis/persistence/conf`, { name: database });
};
export const checkRedisCli = () => {
return http.get<boolean>(`/databases/redis/check`);
};
export const installRedisCli = () => {
return http.post(`/databases/redis/install/cli`, {}, TimeoutEnum.T_5M);
};
export const changeRedisPassword = (database: string, password: string) => {
if (password) {
password = Base64.encode(password);

View File

@ -514,6 +514,7 @@ const message = {
keyspaceMisses: 'Number of failed attempts to find the database key',
hit: 'Find the database key hit ratio',
latestForkUsec: 'The number of microseconds spent on the last fork() operation',
redisCliHelper: 'redis-cli service not detected, please enable the service first!',
recoverHelper: 'Data is about to be overwritten with [{0}]. Do you want to continue?',
submitIt: 'Overwrite the data',

View File

@ -502,6 +502,7 @@ const message = {
keyspaceMisses: '查找數據庫鍵失敗的次數',
hit: '查找數據庫鍵命中率',
latestForkUsec: '最近一次 fork() 操作耗費的微秒數',
redisCliHelper: '未檢測到 redis-cli 服務請先啟用服務',
recoverHelper: '即將使用 [{0}] 對數據進行覆蓋是否繼續?',
submitIt: '覆蓋數據',

View File

@ -502,6 +502,7 @@ const message = {
keyspaceMisses: '查找数据库键失败的次数',
hit: '查找数据库键命中率',
latestForkUsec: '最近一次 fork() 操作耗费的微秒数',
redisCliHelper: '未检测到 redis-cli 服务请先启用服务',
recoverHelper: '即将使用 [{0}] 对数据进行覆盖是否继续?',
submitIt: '覆盖数据',

View File

@ -89,6 +89,16 @@ const databaseRouter = {
requiresAuth: false,
},
},
{
path: 'redis/remote',
name: 'Redis-Remote',
component: () => import('@/views/database/redis/remote/index.vue'),
hidden: true,
meta: {
activeMenu: '/databases',
requiresAuth: false,
},
},
],
},
],

View File

@ -28,6 +28,7 @@ export interface GlobalState {
device: DeviceType;
lastFilePath: string;
currentDB: string;
currentRedisDB: string;
showEntranceWarn: boolean;
defaultNetwork: string;

View File

@ -32,6 +32,7 @@ const GlobalStore = defineStore({
device: DeviceType.Desktop,
lastFilePath: '',
currentDB: '',
currentRedisDB: '',
showEntranceWarn: true,
defaultNetwork: 'all',
@ -80,6 +81,9 @@ const GlobalStore = defineStore({
setCurrentDB(name: string) {
this.currentDB = name;
},
setCurrentRedisDB(name: string) {
this.currentRedisDB = name;
},
setShowEntranceWarn(show: boolean) {
this.showEntranceWarn = show;
},

View File

@ -0,0 +1,54 @@
<template>
<el-dialog
v-model="open"
:title="$t('app.checkTitle')"
width="50%"
:close-on-click-modal="false"
:destroy-on-close="true"
>
<el-row>
<el-col :span="20" :offset="2" v-if="open">
<el-alert
type="error"
:description="$t('app.deleteHelper', [$t('app.database')])"
center
show-icon
:closable="false"
/>
<br />
<el-descriptions border :column="1">
<el-descriptions-item>
<template #label>
<a href="javascript:void(0);" @click="toApp()">{{ $t('app.app') }}</a>
</template>
{{ installData.join(',') }}
</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
interface InstallProps {
items: Array<string>;
}
const installData = ref();
let open = ref(false);
const acceptParams = (props: InstallProps) => {
installData.value = props.items;
open.value = true;
};
const toApp = () => {
router.push({ name: 'AppInstalled' });
};
defineExpose({
acceptParams,
});
</script>

View File

@ -1,5 +1,13 @@
<template>
<div v-loading="loading">
<div class="app-status" style="margin-top: 20px" v-if="currentDB?.from === 'remote'">
<el-card>
<div>
<el-tag style="float: left" effect="dark" type="success">Redis</el-tag>
<el-tag class="status-content">{{ $t('app.version') }}: {{ currentDB?.version }}</el-tag>
</div>
</el-card>
</div>
<LayoutContent :title="'Redis ' + $t('menu.database')">
<template #app v-if="currentDB?.from === 'local'">
<AppStatus
@ -35,9 +43,6 @@
<el-tooltip v-else :content="item.database" placement="top">
<span>{{ item.database.substring(0, 25) }}...</span>
</el-tooltip>
<el-tag class="tagClass">
{{ item.type === 'mysql' ? 'MySQL' : 'MariaDB' }}
</el-tag>
</el-option>
</div>
<el-button link type="primary" class="jumpAdd" @click="goRouter('remote')" icon="Position">
@ -51,6 +56,9 @@
<el-button type="primary" plain @click="onChangePassword">
{{ $t('database.databaseConnInfo') }}
</el-button>
<el-button @click="goRemoteDB" type="primary" plain>
{{ $t('database.remoteDB') }}
</el-button>
<el-button type="primary" plain @click="goDashboard" icon="Position">Redis-Commander</el-button>
</div>
</template>
@ -59,13 +67,21 @@
:style="{ height: `calc(100vh - ${loadHeight()})` }"
:key="isRefresh"
ref="terminalRef"
v-show="terminalShow && redisStatus === 'Running'"
v-show="redisStatus === 'Running' && terminalShow"
/>
<el-empty
v-if="redisStatus !== 'Running'"
v-if="redisStatus !== 'Running' || (currentDB.from === 'remote' && !redisCliExist)"
:style="{ height: `calc(100vh - ${loadHeight()})`, 'background-color': '#000' }"
:description="$t('commons.service.serviceNotStarted', ['Redis'])"
></el-empty>
:description="
currentDB.from !== 'remote'
? $t('commons.service.serviceNotStarted', ['Redis'])
: $t('database.redisCliHelper')
"
>
<el-button v-if="currentDB.from === 'remote'" type="primary" @click="installCli">
{{ $t('commons.button.enable') }}
</el-button>
</el-empty>
</template>
</LayoutContent>
@ -104,8 +120,10 @@ import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import { CheckAppInstalled, GetAppPort } from '@/api/modules/app';
import router from '@/routers';
import { GlobalStore } from '@/store';
import { listDatabases } from '@/api/modules/database';
import { listDatabases, checkRedisCli, installRedisCli } from '@/api/modules/database';
import { Database } from '@/api/interface/database';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
const globalStore = GlobalStore();
const loading = ref(false);
@ -121,6 +139,8 @@ const terminalShow = ref(false);
const redisCommandPort = ref();
const commandVisible = ref(false);
const redisCliExist = ref();
const appKey = ref('redis');
const appName = ref();
const dbOptionsLocal = ref<Array<Database.DatabaseOption>>([]);
@ -153,6 +173,12 @@ const goDashboard = async () => {
const getAppDetail = (key: string) => {
router.push({ name: 'AppAll', query: { install: key } });
};
const goRemoteDB = async () => {
if (currentDB.value) {
globalStore.setCurrentRedisDB(currentDBName.value);
}
router.push({ name: 'Redis-Remote' });
};
const loadDashboardPort = async () => {
const res = await GetAppPort('redis-commander', '');
@ -237,17 +263,39 @@ const reOpenTerminal = async () => {
const initTerminal = async () => {
loading.value = true;
if (currentDB.value.from === 'remote') {
if (!redisCliExist.value) {
loading.value = false;
return;
}
isRefresh.value = !isRefresh.value;
loading.value = false;
redisIsExist.value = true;
nextTick(() => {
terminalShow.value = true;
redisStatus.value = 'Running';
terminalRef.value.acceptParams({
endpoint: '/api/v1/databases/redis/exec',
args: `name=${currentDBName.value}&from=${currentDB.value.from}`,
error: '',
initCmd: '',
});
});
isRefresh.value = !isRefresh.value;
return;
}
await CheckAppInstalled('redis', currentDBName.value)
.then((res) => {
redisIsExist.value = res.data.isExist;
redisStatus.value = res.data.status;
console.log(redisStatus.value);
loading.value = false;
nextTick(() => {
if (res.data.status === 'Running') {
terminalShow.value = true;
terminalRef.value.acceptParams({
endpoint: '/api/v1/databases/redis/exec',
args: `name=${currentDBName.value}`,
args: `name=${currentDBName.value}&from=${currentDB.value.from}`,
error: '',
initCmd: '',
});
@ -266,9 +314,27 @@ const closeTerminal = async (isKeepShow: boolean) => {
terminalShow.value = isKeepShow;
};
const checkCliValid = async () => {
const res = await checkRedisCli();
redisCliExist.value = res.data;
};
const installCli = async () => {
loading.value = true;
await installRedisCli()
.then(() => {
loading.value = false;
redisCliExist.value = true;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
};
onMounted(() => {
loadDBOptions();
loadDashboardPort();
checkCliValid();
});
const onBefore = () => {
closeTerminal(false);

View File

@ -0,0 +1,100 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="$t('commons.button.delete') + ' - ' + deleteMysqlReq.database"
width="30%"
:close-on-click-modal="false"
>
<el-form ref="deleteForm" v-loading="loading" @submit.prevent>
<el-form-item>
<el-checkbox v-model="deleteMysqlReq.forceDelete" :label="$t('app.forceDelete')" />
<span class="input-help">
{{ $t('app.forceDeleteHelper') }}
</span>
</el-form-item>
<el-form-item>
<el-checkbox v-model="deleteMysqlReq.deleteBackup" :label="$t('app.deleteBackup')" />
<span class="input-help">
{{ $t('database.deleteBackupHelper') }}
</span>
</el-form-item>
<el-form-item>
<div>
<span style="font-size: 12px">{{ $t('database.delete') }}</span>
<span style="font-size: 12px; color: red; font-weight: 500">{{ deleteMysqlReq.database }}</span>
<span style="font-size: 12px">{{ $t('database.deleteHelper') }}</span>
</div>
<el-input v-model="delMysqlInfo" :placeholder="deleteMysqlReq.database"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false" :disabled="loading">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button
type="primary"
@click="submit"
:disabled="delMysqlInfo != deleteMysqlReq.database || loading"
>
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import i18n from '@/lang';
import { deleteDatabase } from '@/api/modules/database';
import { MsgSuccess } from '@/utils/message';
let deleteMysqlReq = ref({
id: 0,
database: '',
deleteBackup: false,
forceDelete: false,
});
let dialogVisible = ref(false);
let loading = ref(false);
let delMysqlInfo = ref('');
const deleteForm = ref<FormInstance>();
interface DialogProps {
id: number;
name: string;
database: string;
}
const emit = defineEmits<{ (e: 'search'): void }>();
const acceptParams = async (prop: DialogProps) => {
delMysqlInfo.value = '';
deleteMysqlReq.value = {
id: prop.id,
database: prop.database,
deleteBackup: false,
forceDelete: false,
};
dialogVisible.value = true;
};
const submit = async () => {
loading.value = true;
deleteDatabase(deleteMysqlReq.value)
.then(() => {
loading.value = false;
emit('search');
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
dialogVisible.value = false;
})
.catch(() => {
loading.value = false;
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -0,0 +1,176 @@
<template>
<div v-loading="loading">
<LayoutContent>
<template #title>
<back-button name="Redis" :header="$t('database.remoteDB')" />
</template>
<template #toolbar>
<el-row>
<el-col :xs="24" :sm="20" :md="20" :lg="20" :xl="20">
<el-button type="primary" @click="onOpenDialog('create')">
{{ $t('database.createRemoteDB') }}
</el-button>
</el-col>
<el-col :xs="24" :sm="4" :md="4" :lg="4" :xl="4">
<TableSearch @search="search()" v-model:searchName="searchName" />
</el-col>
</el-row>
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" @sort-change="search" @search="search" :data="data">
<el-table-column show-overflow-tooltip :label="$t('commons.table.name')" prop="name" sortable />
<el-table-column show-overflow-tooltip :label="$t('database.address')" prop="address" />
<el-table-column :label="$t('commons.login.password')" prop="password">
<template #default="{ row }">
<div class="flex items-center">
<div class="star-center">
<span v-if="!row.showPassword">**********</span>
</div>
<div>
<span v-if="row.showPassword">
{{ row.password }}
</span>
</div>
<el-button
v-if="!row.showPassword"
link
@click="row.showPassword = true"
icon="View"
class="ml-1.5"
></el-button>
<el-button
v-if="row.showPassword"
link
@click="row.showPassword = false"
icon="Hide"
class="ml-1.5"
></el-button>
<div>
<CopyButton :content="row.password" type="icon" />
</div>
</div>
</template>
</el-table-column>
<el-table-column
prop="description"
:label="$t('commons.table.description')"
show-overflow-tooltip
/>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
/>
<fu-table-operations
width="170px"
:buttons="buttons"
:ellipsis="10"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<AppResources ref="checkRef"></AppResources>
<OperateDialog ref="dialogRef" @search="search" />
<DeleteDialog ref="deleteRef" @search="search" />
</div>
</template>
<script lang="ts" setup>
import { dateFormat } from '@/utils/util';
import { onMounted, reactive, ref } from 'vue';
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
import AppResources from '@/views/database/redis/check/index.vue';
import OperateDialog from '@/views/database/redis/remote/operate/index.vue';
import DeleteDialog from '@/views/database/redis/remote/delete/index.vue';
import i18n from '@/lang';
import { Database } from '@/api/interface/database';
const loading = ref(false);
const dialogRef = ref();
const checkRef = ref();
const deleteRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'redis-remote-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
orderBy: 'created_at',
order: 'null',
});
const searchName = ref();
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
let params = {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
info: searchName.value,
type: 'redis',
orderBy: paginationConfig.orderBy,
order: paginationConfig.order,
};
const res = await searchDatabases(params);
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
};
const onOpenDialog = async (
title: string,
rowData: Partial<Database.DatabaseInfo> = {
name: '',
type: 'redis',
from: 'remote',
version: '7.2.x',
address: '',
port: 6379,
username: '-',
password: '',
description: '',
},
) => {
let params = {
title,
rowData: { ...rowData },
};
dialogRef.value!.acceptParams(params);
};
const onDelete = async (row: Database.DatabaseInfo) => {
const res = await deleteCheckDatabase(row.id);
if (res.data && res.data.length > 0) {
checkRef.value.acceptParams({ items: res.data });
} else {
deleteRef.value.acceptParams({
id: row.id,
database: row.name,
});
}
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: (row: Database.DatabaseInfo) => {
onOpenDialog('edit', row);
},
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: Database.DatabaseInfo) => {
onDelete(row);
},
},
];
onMounted(() => {
search();
});
</script>

View File

@ -0,0 +1,174 @@
<template>
<el-drawer
v-model="drawerVisible"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
size="50%"
>
<template #header>
<DrawerHeader
:hideResource="dialogData.title === 'create'"
:header="title"
:resource="dialogData.rowData?.name"
:back="handleClose"
/>
</template>
<el-form ref="formRef" v-loading="loading" label-position="top" :model="dialogData.rowData" :rules="rules">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input
v-if="dialogData.title === 'create'"
clearable
v-model.trim="dialogData.rowData!.name"
/>
<el-tag v-else>{{ dialogData.rowData!.name }}</el-tag>
</el-form-item>
<el-form-item :label="$t('database.version')" prop="version">
<el-radio-group v-model="dialogData.rowData!.version" @change="isOK = false">
<el-radio label="6.x" value="6.x" />
<el-radio label="7.x" value="7.x" />
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('database.address')" prop="address">
<el-input @change="isOK = false" clearable v-model.trim="dialogData.rowData!.address" />
</el-form-item>
<el-form-item :label="$t('commons.table.port')" prop="port">
<el-input @change="isOK = false" clearable v-model.number="dialogData.rowData!.port" />
</el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password">
<el-input
@change="isOK = false"
type="password"
clearable
show-password
v-model.trim="dialogData.rowData!.password"
/>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input clearable v-model.trim="dialogData.rowData!.description" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button @click="onSubmit(formRef, 'check')">
{{ $t('terminal.testConn') }}
</el-button>
<el-button type="primary" :disabled="!isOK" @click="onSubmit(formRef, dialogData.title)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { Database } from '@/api/interface/database';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgError, MsgSuccess } from '@/utils/message';
import { Rules } from '@/global/form-rules';
import { addDatabase, checkDatabase, editDatabase } from '@/api/modules/database';
interface DialogProps {
title: string;
rowData?: Database.DatabaseInfo;
getTableList?: () => Promise<any>;
}
const title = ref<string>('');
const drawerVisible = ref(false);
const dialogData = ref<DialogProps>({
title: '',
});
const isOK = ref(false);
const loading = ref();
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
if (dialogData.value.rowData.version.startsWith('6.')) {
dialogData.value.rowData.version = '6.x';
}
if (dialogData.value.rowData.version.startsWith('7.')) {
dialogData.value.rowData.version = '7,x';
}
title.value = i18n.global.t('database.' + dialogData.value.title + 'RemoteDB');
drawerVisible.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const handleClose = () => {
drawerVisible.value = false;
};
const rules = reactive({
name: [Rules.requiredInput],
type: [Rules.requiredSelect],
version: [Rules.requiredSelect],
address: [Rules.ipV4V6OrDomain],
port: [Rules.port],
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const onSubmit = async (formEl: FormInstance | undefined, operation: string) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
dialogData.value.rowData.from = 'remote';
loading.value = true;
dialogData.value.rowData.rootCert = dialogData.value.rowData.hasCA ? dialogData.value.rowData.rootCert : '';
if (operation === 'check') {
await checkDatabase(dialogData.value.rowData)
.then((res) => {
loading.value = false;
if (res.data) {
isOK.value = true;
MsgSuccess(i18n.global.t('terminal.connTestOk'));
} else {
MsgError(i18n.global.t('terminal.connTestFailed'));
}
})
.catch(() => {
loading.value = false;
MsgError(i18n.global.t('terminal.connTestFailed'));
});
}
if (operation === 'create') {
await addDatabase(dialogData.value.rowData)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
});
}
if (operation === 'edit') {
await editDatabase(dialogData.value.rowData)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
});
}
});
};
defineExpose({
acceptParams,
});
</script>

1
go.mod
View File

@ -19,6 +19,7 @@ require (
github.com/go-acme/lego/v4 v4.15.0
github.com/go-gormigrate/gormigrate/v2 v2.1.1
github.com/go-playground/validator/v10 v10.18.0
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-sql-driver/mysql v1.7.1
github.com/goh-chunlin/go-onedrive v1.1.1
github.com/golang-jwt/jwt/v4 v4.5.0

5
go.sum
View File

@ -328,6 +328,8 @@ github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiR
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
@ -474,6 +476,7 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
@ -1256,6 +1259,7 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
@ -1265,6 +1269,7 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=