fix: 远程数据库增加删除提示 (#2454)

pull/2457/head
ssongliu 2023-10-08 14:52:14 +08:00 committed by GitHub
parent 4c650af0e9
commit 9d0eca723c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 365 additions and 23 deletions

View File

@ -126,15 +126,14 @@ func (b *BaseApi) GetDatabase(c *gin.Context) {
}
// @Tags Database
// @Summary Delete database
// @Description 删除远程数据库
// @Summary Check before delete remote database
// @Description Mysql 远程数据库删除前检查
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200
// @Success 200 {array} string
// @Security ApiKeyAuth
// @Router /databases/db/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"databases","output_column":"name","output_value":"names"}],"formatZH":"删除远程数据库 [names]","formatEN":"delete database [names]"}
func (b *BaseApi) DeleteDatabase(c *gin.Context) {
// @Router /db/remote/del/check [post]
func (b *BaseApi) DeleteCheckDatabase(c *gin.Context) {
var req dto.OperateByID
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
@ -145,7 +144,35 @@ func (b *BaseApi) DeleteDatabase(c *gin.Context) {
return
}
if err := databaseService.Delete(req.ID); err != nil {
apps, err := databaseService.DeleteCheck(req.ID)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, apps)
}
// @Tags Database
// @Summary Delete database
// @Description 删除远程数据库
// @Accept json
// @Param request body dto.DatabaseDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /databases/db/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"databases","output_column":"name","output_value":"names"}],"formatZH":"删除远程数据库 [names]","formatEN":"delete database [names]"}
func (b *BaseApi) DeleteDatabase(c *gin.Context) {
var req dto.DatabaseDelete
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
}
if err := databaseService.Delete(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}

View File

@ -271,3 +271,9 @@ type DatabaseUpdate struct {
Password string `json:"password" validate:"required"`
Description string `json:"description"`
}
type DatabaseDelete struct {
ID uint `json:"id" validate:"required"`
ForceDelete bool `json:"forceDelete"`
DeleteBackup bool `json:"deleteBackup"`
}

View File

@ -3,10 +3,13 @@ package service
import (
"context"
"fmt"
"os"
"path"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/encrypt"
"github.com/1Panel-dev/1Panel/backend/utils/mysql"
"github.com/1Panel-dev/1Panel/backend/utils/mysql/client"
@ -22,7 +25,8 @@ type IDatabaseService interface {
CheckDatabase(req dto.DatabaseCreate) bool
Create(req dto.DatabaseCreate) error
Update(req dto.DatabaseUpdate) error
Delete(id uint) error
DeleteCheck(id uint) ([]string, error)
Delete(req dto.DatabaseDelete) error
List(dbType string) ([]dto.DatabaseOption, error)
}
@ -114,16 +118,47 @@ func (u *DatabaseService) Create(req dto.DatabaseCreate) error {
return nil
}
func (u *DatabaseService) Delete(id uint) error {
db, _ := databaseRepo.Get(commonRepo.WithByID(id))
func (u *DatabaseService) DeleteCheck(id uint) ([]string, error) {
var appInUsed []string
apps, _ := appInstallResourceRepo.GetBy(databaseRepo.WithByFrom("remote"), appInstallResourceRepo.WithLinkId(id))
for _, app := range apps {
appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId))
if appInstall.ID != 0 {
appInUsed = append(appInUsed, appInstall.Name)
}
}
return appInUsed, nil
}
func (u *DatabaseService) Delete(req dto.DatabaseDelete) error {
db, _ := databaseRepo.Get(commonRepo.WithByID(req.ID))
if db.ID == 0 {
return constant.ErrRecordNotFound
}
if err := databaseRepo.Delete(context.Background(), commonRepo.WithByID(id)); err != nil {
if req.DeleteBackup {
uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/database/%s/%s", db.Type, db.Name))
if _, err := os.Stat(uploadDir); err == nil {
_ = os.RemoveAll(uploadDir)
}
localDir, err := loadLocalDir()
if err != nil && !req.ForceDelete {
return err
}
backupDir := path.Join(localDir, fmt.Sprintf("database/%s/%s", db.Type, db.Name))
if _, err := os.Stat(backupDir); err == nil {
_ = os.RemoveAll(backupDir)
}
_ = backupRepo.DeleteRecord(context.Background(), commonRepo.WithByType(db.Type), commonRepo.WithByName(db.Name))
global.LOG.Infof("delete database %s-%s backups successful", db.Type, db.Name)
}
if err := databaseRepo.Delete(context.Background(), commonRepo.WithByID(req.ID)); err != nil && !req.ForceDelete {
return err
}
if db.From != "local" {
if err := mysqlRepo.Delete(context.Background(), mysqlRepo.WithByMysqlName(db.Name)); err != nil {
if err := mysqlRepo.Delete(context.Background(), mysqlRepo.WithByMysqlName(db.Name)); err != nil && !req.ForceDelete {
return err
}
}

View File

@ -49,6 +49,7 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) {
cmdRouter.GET("/db/list/:type", baseApi.ListDatabase)
cmdRouter.POST("/db/update", baseApi.UpdateDatabase)
cmdRouter.POST("/db/search", baseApi.SearchDatabase)
cmdRouter.POST("/db/del/check", baseApi.DeleteCheckDatabase)
cmdRouter.POST("/db/del", baseApi.DeleteDatabase)
}
}

View File

@ -4068,7 +4068,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
"$ref": "#/definitions/dto.DatabaseDelete"
}
}
],
@ -4889,6 +4889,45 @@ const docTemplate = `{
}
}
},
"/db/remote/del/check": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Mysql 远程数据库删除前检查",
"consumes": [
"application/json"
],
"tags": [
"Database"
],
"summary": "Check before delete remote database",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"/files": {
"post": {
"security": [
@ -13117,6 +13156,23 @@ const docTemplate = `{
}
}
},
"dto.DatabaseDelete": {
"type": "object",
"required": [
"id"
],
"properties": {
"deleteBackup": {
"type": "boolean"
},
"forceDelete": {
"type": "boolean"
},
"id": {
"type": "integer"
}
}
},
"dto.DatabaseInfo": {
"type": "object",
"properties": {
@ -17729,6 +17785,9 @@ const docTemplate = `{
"type": "object",
"properties": {
"config": {},
"from": {
"type": "string"
},
"label": {
"type": "string"
},

View File

@ -4061,7 +4061,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
"$ref": "#/definitions/dto.DatabaseDelete"
}
}
],
@ -4882,6 +4882,45 @@
}
}
},
"/db/remote/del/check": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Mysql 远程数据库删除前检查",
"consumes": [
"application/json"
],
"tags": [
"Database"
],
"summary": "Check before delete remote database",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"/files": {
"post": {
"security": [
@ -13110,6 +13149,23 @@
}
}
},
"dto.DatabaseDelete": {
"type": "object",
"required": [
"id"
],
"properties": {
"deleteBackup": {
"type": "boolean"
},
"forceDelete": {
"type": "boolean"
},
"id": {
"type": "integer"
}
}
},
"dto.DatabaseInfo": {
"type": "object",
"properties": {
@ -17722,6 +17778,9 @@
"type": "object",
"properties": {
"config": {},
"from": {
"type": "string"
},
"label": {
"type": "string"
},

View File

@ -786,6 +786,17 @@ definitions:
- username
- version
type: object
dto.DatabaseDelete:
properties:
deleteBackup:
type: boolean
forceDelete:
type: boolean
id:
type: integer
required:
- id
type: object
dto.DatabaseInfo:
properties:
address:
@ -3879,6 +3890,8 @@ definitions:
response.AppService:
properties:
config: {}
from:
type: string
label:
type: string
value:
@ -6716,7 +6729,7 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/dto.OperateByID'
$ref: '#/definitions/dto.DatabaseDelete'
responses:
"200":
description: OK
@ -7234,6 +7247,30 @@ paths:
formatEN: adjust mysql database performance parameters
formatZH: 调整 mysql 数据库性能参数
paramKeys: []
/db/remote/del/check:
post:
consumes:
- application/json
description: Mysql 远程数据库删除前检查
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.OperateByID'
responses:
"200":
description: OK
schema:
items:
type: string
type: array
security:
- ApiKeyAuth: []
summary: Check before delete remote database
tags:
- Database
/files:
post:
consumes:

View File

@ -250,4 +250,9 @@ export namespace Database {
password: string;
description: string;
}
export interface DatabaseDelete {
id: number;
forceDelete: boolean;
deleteBackup: boolean;
}
}

View File

@ -109,6 +109,9 @@ export const addDatabase = (params: Database.DatabaseCreate) => {
export const editDatabase = (params: Database.DatabaseUpdate) => {
return http.post(`/databases/db/update`, params, TimeoutEnum.T_40S);
};
export const deleteDatabase = (id: number) => {
return http.post(`/databases/db/del`, { id: id });
export const deleteCheckDatabase = (id: number) => {
return http.post<Array<string>>(`/databases/db/del/check`, { id: id });
};
export const deleteDatabase = (params: Database.DatabaseDelete) => {
return http.post(`/databases/db/del`, params);
};

View File

@ -327,6 +327,7 @@ const message = {
},
database: {
database: 'database',
deleteBackupHelper: 'Delete database backups simultaneously',
delete: 'Delete operation cannot be rolled back, please input "',
deleteHelper: '" to delete this database',
create: 'Create database',

View File

@ -323,6 +323,7 @@ const message = {
},
database: {
database: '',
deleteBackupHelper: '',
delete: ' "',
deleteHelper: '" ',
create: '',

View File

@ -323,6 +323,7 @@ const message = {
},
database: {
database: '',
deleteBackupHelper: '',
delete: ' "',
deleteHelper: '" ',
create: '',

View File

@ -15,7 +15,7 @@
<el-form-item>
<el-checkbox v-model="deleteReq.deleteBackup" :label="$t('app.deleteBackup')" />
<span class="input-help">
{{ $t('app.deleteBackupHelper') }}
{{ $t('database.deleteBackupHelper') }}
</span>
</el-form-item>
<el-form-item>

View File

@ -0,0 +1,95 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="$t('commons.button.delete') + ' - ' + deleteReq.database"
width="30%"
:close-on-click-modal="false"
>
<el-form ref="deleteForm" v-loading="loading" @submit.prevent>
<el-form-item>
<el-checkbox v-model="deleteReq.forceDelete" :label="$t('app.forceDelete')" />
<span class="input-help">
{{ $t('app.forceDeleteHelper') }}
</span>
</el-form-item>
<el-form-item>
<el-checkbox v-model="deleteReq.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">{{ deleteReq.database }}</span>
<span style="font-size: 12px">{{ $t('database.deleteHelper') }}</span>
</div>
<el-input v-model="deleteInfo" :placeholder="deleteReq.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="deleteInfo != deleteReq.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 deleteReq = ref({
id: 0,
database: '',
deleteBackup: false,
forceDelete: false,
});
let dialogVisible = ref(false);
let loading = ref(false);
let deleteInfo = ref('');
const deleteForm = ref<FormInstance>();
interface DialogProps {
id: number;
name: string;
database: string;
}
const emit = defineEmits<{ (e: 'search'): void }>();
const acceptParams = async (prop: DialogProps) => {
deleteReq.value = {
id: prop.id,
database: prop.database,
deleteBackup: false,
forceDelete: false,
};
dialogVisible.value = true;
};
const submit = async () => {
loading.value = true;
deleteDatabase(deleteReq.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

@ -84,25 +84,30 @@
</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 { deleteDatabase, searchDatabases } from '@/api/modules/database';
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
import AppResources from '@/views/database/mysql/check/index.vue';
import OperateDialog from '@/views/database/mysql/remote/operate/index.vue';
import DeleteDialog from '@/views/database/mysql/remote/delete/index.vue';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
import useClipboard from 'vue-clipboard3';
import { Database } from '@/api/interface/database';
import { useDeleteData } from '@/hooks/use-delete-data';
import useClipboard from 'vue-clipboard3';
const { toClipboard } = useClipboard();
const loading = ref(false);
const dialogRef = ref();
const checkRef = ref();
const deleteRef = ref();
const data = ref();
const paginationConfig = reactive({
@ -161,8 +166,15 @@ const onCopy = async (row: any) => {
};
const onDelete = async (row: Database.DatabaseInfo) => {
await useDeleteData(deleteDatabase, row.id, 'commons.msg.delete');
search();
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 = [