feat: 远程数据库增加连接性测试 (#1928)

Refs #1924
This commit is contained in:
ssongliu 2023-08-11 22:20:15 +08:00 committed by GitHub
parent 98df3806f5
commit 3e6cd1cab1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 263 additions and 53 deletions

View file

@ -34,6 +34,28 @@ func (b *BaseApi) CreateRemoteDB(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags Database
// @Summary Check remote database
// @Description 检测远程数据库连接性
// @Accept json
// @Param request body dto.RemoteDBCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /databases/remote/check [post]
// @x-panel-log {"bodyKeys":["name", "type"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"检测远程数据库 [name][type] 连接性","formatEN":"check if remote database [name][type] is connectable"}
func (b *BaseApi) CheckeRemoteDB(c *gin.Context) {
var req dto.RemoteDBCreate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
helper.SuccessWithData(c, remoteDBService.CheckeRemoteDB(req))
}
// @Tags Database
// @Summary Page remote databases
// @Description 获取远程数据库列表分页

View file

@ -16,6 +16,7 @@ type RemoteDBService struct{}
type IRemoteDBService interface {
Get(name string) (dto.RemoteDBInfo, error)
SearchWithPage(search dto.RemoteDBSearch) (int64, interface{}, error)
CheckeRemoteDB(req dto.RemoteDBCreate) bool
Create(req dto.RemoteDBCreate) error
Update(req dto.RemoteDBUpdate) error
Delete(id uint) error
@ -68,6 +69,20 @@ func (u *RemoteDBService) List(dbType string) ([]dto.RemoteDBOption, error) {
return datas, err
}
func (u *RemoteDBService) CheckeRemoteDB(req dto.RemoteDBCreate) bool {
if _, err := mysql.NewMysqlClient(client.DBInfo{
From: "remote",
Address: req.Address,
Port: req.Port,
Username: req.Username,
Password: req.Password,
Timeout: 6,
}); err != nil {
return false
}
return true
}
func (u *RemoteDBService) Create(req dto.RemoteDBCreate) error {
db, _ := remoteDBRepo.Get(commonRepo.WithByName(req.Name))
if db.ID != 0 {
@ -79,7 +94,7 @@ func (u *RemoteDBService) Create(req dto.RemoteDBCreate) error {
Port: req.Port,
Username: req.Username,
Password: req.Password,
Timeout: 300,
Timeout: 6,
}); err != nil {
return err
}

View file

@ -43,6 +43,7 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) {
cmdRouter.POST("/redis/conffile/update", baseApi.UpdateRedisConfByFile)
cmdRouter.POST("/redis/persistence/update", baseApi.UpdateRedisPersistenceConf)
cmdRouter.POST("/remote/check", baseApi.CheckeRemoteDB)
cmdRouter.POST("/remote", baseApi.CreateRemoteDB)
cmdRouter.GET("/remote/:name", baseApi.GetRemoteDB)
cmdRouter.GET("/remote/list/:type", baseApi.ListRemoteDB)

View file

@ -1,8 +1,10 @@
package mysql
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
@ -38,9 +40,16 @@ func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) {
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(conn.Timeout)*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, err
}
if ctx.Err() == context.DeadlineExceeded {
return nil, buserr.New(constant.ErrExecTimeOut)
}
return client.NewRemote(client.Remote{
Client: db,
From: conn.From,

View file

@ -4410,6 +4410,49 @@ const docTemplate = `{
}
}
},
"/databases/remote/check": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "检测远程数据库连接性",
"consumes": [
"application/json"
],
"tags": [
"Database"
],
"summary": "Check remote database",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RemoteDBCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"name",
"type"
],
"formatEN": "check if remote database [name][type] is connectable",
"formatZH": "检测远程数据库 [name][type] 连接性",
"paramKeys": []
}
}
},
"/databases/remote/del": {
"post": {
"security": [
@ -11621,7 +11664,6 @@ const docTemplate = `{
"dto.ChangeDBInfo": {
"type": "object",
"required": [
"from",
"value"
],
"properties": {
@ -12923,13 +12965,15 @@ const docTemplate = `{
"type": "integer"
},
"password": {
"type": "string"
"type": "string",
"maxLength": 256
},
"protocol": {
"type": "string"
},
"username": {
"type": "string"
"type": "string",
"maxLength": 256
}
}
},
@ -13776,7 +13820,8 @@ const docTemplate = `{
]
},
"name": {
"type": "string"
"type": "string",
"maxLength": 256
},
"password": {
"type": "string"
@ -13817,7 +13862,8 @@ const docTemplate = `{
"type": "integer"
},
"name": {
"type": "string"
"type": "string",
"maxLength": 256
},
"password": {
"type": "string"

View file

@ -4403,6 +4403,49 @@
}
}
},
"/databases/remote/check": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "检测远程数据库连接性",
"consumes": [
"application/json"
],
"tags": [
"Database"
],
"summary": "Check remote database",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RemoteDBCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"name",
"type"
],
"formatEN": "check if remote database [name][type] is connectable",
"formatZH": "检测远程数据库 [name][type] 连接性",
"paramKeys": []
}
}
},
"/databases/remote/del": {
"post": {
"security": [
@ -11614,7 +11657,6 @@
"dto.ChangeDBInfo": {
"type": "object",
"required": [
"from",
"value"
],
"properties": {
@ -12916,13 +12958,15 @@
"type": "integer"
},
"password": {
"type": "string"
"type": "string",
"maxLength": 256
},
"protocol": {
"type": "string"
},
"username": {
"type": "string"
"type": "string",
"maxLength": 256
}
}
},
@ -13769,7 +13813,8 @@
]
},
"name": {
"type": "string"
"type": "string",
"maxLength": 256
},
"password": {
"type": "string"
@ -13810,7 +13855,8 @@
"type": "integer"
},
"name": {
"type": "string"
"type": "string",
"maxLength": 256
},
"password": {
"type": "string"

View file

@ -127,7 +127,6 @@ definitions:
value:
type: string
required:
- from
- value
type: object
dto.ChangeHostGroup:
@ -1000,10 +999,12 @@ definitions:
id:
type: integer
password:
maxLength: 256
type: string
protocol:
type: string
username:
maxLength: 256
type: string
type: object
dto.ImageSave:
@ -1565,6 +1566,7 @@ definitions:
- remote
type: string
name:
maxLength: 256
type: string
password:
type: string
@ -1599,6 +1601,7 @@ definitions:
id:
type: integer
name:
maxLength: 256
type: string
password:
type: string
@ -6648,6 +6651,34 @@ paths:
summary: Get remote databases
tags:
- Database
/databases/remote/check:
post:
consumes:
- application/json
description: 检测远程数据库连接性
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.RemoteDBCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Check remote database
tags:
- Database
x-panel-log:
BeforeFuntions: []
bodyKeys:
- name
- type
formatEN: check if remote database [name][type] is connectable
formatZH: 检测远程数据库 [name][type] 连接性
paramKeys: []
/databases/remote/del:
post:
consumes:

View file

@ -100,11 +100,14 @@ export const searchRemoteDBs = (params: Database.SearchRemoteDBPage) => {
export const listRemoteDBs = (type: string) => {
return http.get<Array<Database.RemoteDBOption>>(`/databases/remote/list/${type}`);
};
export const checkRemoteDB = (params: Database.RemoteDBCreate) => {
return http.post<boolean>(`/databases/remote/check`, params, 40000);
};
export const addRemoteDB = (params: Database.RemoteDBCreate) => {
return http.post(`/databases/remote`, params);
return http.post(`/databases/remote`, params, 40000);
};
export const editRemoteDB = (params: Database.RemoteDBUpdate) => {
return http.post(`/databases/remote/update`, params);
return http.post(`/databases/remote/update`, params, 40000);
};
export const deleteRemoteDB = (id: number) => {
return http.post(`/databases/remote/del`, { id: id });

View file

@ -8,7 +8,7 @@
:back="handleClose"
/>
</template>
<el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules">
<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">
@ -20,7 +20,7 @@
<el-tag v-else>{{ dialogData.rowData!.name }}</el-tag>
</el-form-item>
<el-form-item :label="$t('database.version')" prop="version">
<el-select v-model="dialogData.rowData!.version">
<el-select @change="isOK = false" v-model="dialogData.rowData!.version">
<el-option value="5.6" label="5.6" />
<el-option value="5.7" label="5.7" />
<el-option value="8.0" label="8.0" />
@ -28,17 +28,23 @@
<span class="input-help">{{ $t('database.versionHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('database.address')" prop="address">
<el-input clearable v-model.trim="dialogData.rowData!.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 clearable v-model.number="dialogData.rowData!.port" />
<el-input @change="isOK = false" clearable v-model.number="dialogData.rowData!.port" />
</el-form-item>
<el-form-item :label="$t('commons.login.username')" prop="username">
<el-input clearable v-model.trim="dialogData.rowData!.username" />
<el-input @change="isOK = false" clearable v-model.trim="dialogData.rowData!.username" />
<span class="input-help">{{ $t('database.userHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password">
<el-input type="password" clearable show-password v-model.trim="dialogData.rowData!.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" />
@ -49,7 +55,10 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
<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>
@ -63,9 +72,9 @@ import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { Database } from '@/api/interface/database';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { Rules } from '@/global/form-rules';
import { addRemoteDB, editRemoteDB } from '@/api/modules/database';
import { addRemoteDB, checkRemoteDB, editRemoteDB } from '@/api/modules/database';
interface DialogProps {
title: string;
@ -77,6 +86,9 @@ const drawerVisiable = ref(false);
const dialogData = ref<DialogProps>({
title: '',
});
const isOK = ref(false);
const loading = ref();
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
title.value = i18n.global.t('database.' + dialogData.value.title + 'RemoteDB');
@ -100,40 +112,65 @@ const rules = reactive({
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const onSubmit = async (formEl: FormInstance | undefined) => {
const onSubmit = async (formEl: FormInstance | undefined, operation: string) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (dialogData.value.title === 'create') {
let param = {
name: dialogData.value.rowData.name,
type: 'mysql',
version: dialogData.value.rowData.version,
from: 'remote',
address: dialogData.value.rowData.address,
port: dialogData.value.rowData.port,
username: dialogData.value.rowData.username,
password: dialogData.value.rowData.password,
description: dialogData.value.rowData.description,
};
await addRemoteDB(param);
}
if (dialogData.value.title === 'edit') {
let param = {
id: dialogData.value.rowData.id,
version: dialogData.value.rowData.version,
address: dialogData.value.rowData.address,
port: dialogData.value.rowData.port,
username: dialogData.value.rowData.username,
password: dialogData.value.rowData.password,
description: dialogData.value.rowData.description,
};
await editRemoteDB(param);
let param = {
id: dialogData.value.rowData.id,
name: dialogData.value.rowData.name,
type: 'mysql',
version: dialogData.value.rowData.version,
from: 'remote',
address: dialogData.value.rowData.address,
port: dialogData.value.rowData.port,
username: dialogData.value.rowData.username,
password: dialogData.value.rowData.password,
description: dialogData.value.rowData.description,
};
loading.value = true;
if (operation === 'check') {
await checkRemoteDB(param)
.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'));
});
}
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisiable.value = false;
if (operation === 'create') {
await addRemoteDB(param)
.then(() => {
loading.value = true;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisiable.value = false;
})
.catch(() => {
loading.value = false;
});
}
if (operation === 'edit') {
await editRemoteDB(param)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisiable.value = false;
})
.catch(() => {
loading.value = false;
});
}
});
};