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 // @Tags Database
// @Summary Delete database // @Summary Check before delete remote database
// @Description 删除远程数据库 // @Description Mysql 远程数据库删除前检查
// @Accept json // @Accept json
// @Param request body dto.OperateByID true "request" // @Param request body dto.OperateByID true "request"
// @Success 200 // @Success 200 {array} string
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /databases/db/del [post] // @Router /db/remote/del/check [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) DeleteCheckDatabase(c *gin.Context) {
func (b *BaseApi) DeleteDatabase(c *gin.Context) {
var req dto.OperateByID var req dto.OperateByID
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
@ -145,7 +144,35 @@ func (b *BaseApi) DeleteDatabase(c *gin.Context) {
return 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) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }

View File

@ -271,3 +271,9 @@ type DatabaseUpdate struct {
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`
Description string `json:"description"` 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 ( import (
"context" "context"
"fmt" "fmt"
"os"
"path"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant" "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/encrypt"
"github.com/1Panel-dev/1Panel/backend/utils/mysql" "github.com/1Panel-dev/1Panel/backend/utils/mysql"
"github.com/1Panel-dev/1Panel/backend/utils/mysql/client" "github.com/1Panel-dev/1Panel/backend/utils/mysql/client"
@ -22,7 +25,8 @@ type IDatabaseService interface {
CheckDatabase(req dto.DatabaseCreate) bool CheckDatabase(req dto.DatabaseCreate) bool
Create(req dto.DatabaseCreate) error Create(req dto.DatabaseCreate) error
Update(req dto.DatabaseUpdate) 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) List(dbType string) ([]dto.DatabaseOption, error)
} }
@ -114,16 +118,47 @@ func (u *DatabaseService) Create(req dto.DatabaseCreate) error {
return nil return nil
} }
func (u *DatabaseService) Delete(id uint) error { func (u *DatabaseService) DeleteCheck(id uint) ([]string, error) {
db, _ := databaseRepo.Get(commonRepo.WithByID(id)) 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 { if db.ID == 0 {
return constant.ErrRecordNotFound 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 return err
} }
if db.From != "local" { 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 return err
} }
} }

View File

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

View File

@ -4068,7 +4068,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "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": { "/files": {
"post": { "post": {
"security": [ "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": { "dto.DatabaseInfo": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -17729,6 +17785,9 @@ const docTemplate = `{
"type": "object", "type": "object",
"properties": { "properties": {
"config": {}, "config": {},
"from": {
"type": "string"
},
"label": { "label": {
"type": "string" "type": "string"
}, },

View File

@ -4061,7 +4061,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "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": { "/files": {
"post": { "post": {
"security": [ "security": [
@ -13110,6 +13149,23 @@
} }
} }
}, },
"dto.DatabaseDelete": {
"type": "object",
"required": [
"id"
],
"properties": {
"deleteBackup": {
"type": "boolean"
},
"forceDelete": {
"type": "boolean"
},
"id": {
"type": "integer"
}
}
},
"dto.DatabaseInfo": { "dto.DatabaseInfo": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -17722,6 +17778,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"config": {}, "config": {},
"from": {
"type": "string"
},
"label": { "label": {
"type": "string" "type": "string"
}, },

View File

@ -786,6 +786,17 @@ definitions:
- username - username
- version - version
type: object type: object
dto.DatabaseDelete:
properties:
deleteBackup:
type: boolean
forceDelete:
type: boolean
id:
type: integer
required:
- id
type: object
dto.DatabaseInfo: dto.DatabaseInfo:
properties: properties:
address: address:
@ -3879,6 +3890,8 @@ definitions:
response.AppService: response.AppService:
properties: properties:
config: {} config: {}
from:
type: string
label: label:
type: string type: string
value: value:
@ -6716,7 +6729,7 @@ paths:
name: request name: request
required: true required: true
schema: schema:
$ref: '#/definitions/dto.OperateByID' $ref: '#/definitions/dto.DatabaseDelete'
responses: responses:
"200": "200":
description: OK description: OK
@ -7234,6 +7247,30 @@ paths:
formatEN: adjust mysql database performance parameters formatEN: adjust mysql database performance parameters
formatZH: 调整 mysql 数据库性能参数 formatZH: 调整 mysql 数据库性能参数
paramKeys: [] 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: /files:
post: post:
consumes: consumes:

View File

@ -250,4 +250,9 @@ export namespace Database {
password: string; password: string;
description: 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) => { export const editDatabase = (params: Database.DatabaseUpdate) => {
return http.post(`/databases/db/update`, params, TimeoutEnum.T_40S); return http.post(`/databases/db/update`, params, TimeoutEnum.T_40S);
}; };
export const deleteDatabase = (id: number) => { export const deleteCheckDatabase = (id: number) => {
return http.post(`/databases/db/del`, { id: id }); 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: 'database', database: 'database',
deleteBackupHelper: 'Delete database backups simultaneously',
delete: 'Delete operation cannot be rolled back, please input "', delete: 'Delete operation cannot be rolled back, please input "',
deleteHelper: '" to delete this database', deleteHelper: '" to delete this database',
create: 'Create database', create: 'Create database',

View File

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

View File

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

View File

@ -15,7 +15,7 @@
<el-form-item> <el-form-item>
<el-checkbox v-model="deleteReq.deleteBackup" :label="$t('app.deleteBackup')" /> <el-checkbox v-model="deleteReq.deleteBackup" :label="$t('app.deleteBackup')" />
<span class="input-help"> <span class="input-help">
{{ $t('app.deleteBackupHelper') }} {{ $t('database.deleteBackupHelper') }}
</span> </span>
</el-form-item> </el-form-item>
<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> </template>
</LayoutContent> </LayoutContent>
<AppResources ref="checkRef"></AppResources>
<OperateDialog ref="dialogRef" @search="search" /> <OperateDialog ref="dialogRef" @search="search" />
<DeleteDialog ref="deleteRef" @search="search" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { dateFormat } from '@/utils/util'; import { dateFormat } from '@/utils/util';
import { onMounted, reactive, ref } from 'vue'; 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 OperateDialog from '@/views/database/mysql/remote/operate/index.vue';
import DeleteDialog from '@/views/database/mysql/remote/delete/index.vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
import useClipboard from 'vue-clipboard3';
import { Database } from '@/api/interface/database'; import { Database } from '@/api/interface/database';
import { useDeleteData } from '@/hooks/use-delete-data'; import useClipboard from 'vue-clipboard3';
const { toClipboard } = useClipboard(); const { toClipboard } = useClipboard();
const loading = ref(false); const loading = ref(false);
const dialogRef = ref(); const dialogRef = ref();
const checkRef = ref();
const deleteRef = ref();
const data = ref(); const data = ref();
const paginationConfig = reactive({ const paginationConfig = reactive({
@ -161,8 +166,15 @@ const onCopy = async (row: any) => {
}; };
const onDelete = async (row: Database.DatabaseInfo) => { const onDelete = async (row: Database.DatabaseInfo) => {
await useDeleteData(deleteDatabase, row.id, 'commons.msg.delete'); const res = await deleteCheckDatabase(row.id);
search(); 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 = [ const buttons = [