diff --git a/backend/app/model/database.go b/backend/app/model/database.go new file mode 100644 index 000000000..58bd8accb --- /dev/null +++ b/backend/app/model/database.go @@ -0,0 +1,13 @@ +package model + +type Database struct { + BaseModel + Name string `json:"name" gorm:"type:varchar(64);not null"` + Type string `json:"type" gorm:"type:varchar(64);not null"` + Address string `json:"address" gorm:"type:varchar(64);not null"` + Port uint `json:"port" gorm:"type:decimal;not null"` + Username string `json:"username" gorm:"type:varchar(64)"` + Password string `json:"password" gorm:"type:varchar(64)"` + Format string `json:"format" gorm:"type:varchar(64)"` + Description string `json:"description" gorm:"type:varchar(256);"` +} diff --git a/backend/app/service/database_mysql.go b/backend/app/service/database_mysql.go index 0a362443b..dde139887 100644 --- a/backend/app/service/database_mysql.go +++ b/backend/app/service/database_mysql.go @@ -585,7 +585,7 @@ func excSQL(containerName, password, command string) error { cmd := exec.CommandContext(ctx, "docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command) stdout, err := cmd.CombinedOutput() if ctx.Err() == context.DeadlineExceeded { - return buserr.WithDetail(constant.ErrExecTimeOut, containerName, nil) + return buserr.New(constant.ErrExecTimeOut) } stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml index 9e5611a7d..ac9caf314 100644 --- a/backend/i18n/lang/en.yaml +++ b/backend/i18n/lang/en.yaml @@ -76,7 +76,7 @@ ErrSSLCertificateFormat: 'Certificate file format error, please use pem format' #mysql ErrUserIsExist: "The current user already exists. Please enter a new user" ErrDatabaseIsExist: "The current database already exists. Please enter a new database" -ErrExecTimeOut: "SQL execution timed out, please check the {{ .detail }} container" +ErrExecTimeOut: "SQL execution timed out, please check the database" #redis ErrTypeOfRedis: "The recovery file type does not match the current persistence mode. Modify the file type and try again" diff --git a/backend/i18n/lang/zh-Hant.yaml b/backend/i18n/lang/zh-Hant.yaml index 36011683c..08216862a 100644 --- a/backend/i18n/lang/zh-Hant.yaml +++ b/backend/i18n/lang/zh-Hant.yaml @@ -76,7 +76,7 @@ ErrSSLCertificateFormat: '證書文件格式錯誤,請使用 pem 格式' #mysql ErrUserIsExist: "當前用戶已存在,請重新輸入" ErrDatabaseIsExist: "當前資料庫已存在,請重新輸入" -ErrExecTimeOut: "SQL 執行超時,請檢查{{ .detail }}容器" +ErrExecTimeOut: "SQL 執行超時,請檢查數據庫" #redis ErrTypeOfRedis: "恢復文件類型與當前持久化方式不符,請修改後重試" diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml index 9072be463..3ffb03d6c 100644 --- a/backend/i18n/lang/zh.yaml +++ b/backend/i18n/lang/zh.yaml @@ -76,7 +76,7 @@ ErrSSLCertificateFormat: '证书文件格式错误,请使用 pem 格式' #mysql ErrUserIsExist: "当前用户已存在,请重新输入" ErrDatabaseIsExist: "当前数据库已存在,请重新输入" -ErrExecTimeOut: "SQL 执行超时,请检查{{ .detail }}容器" +ErrExecTimeOut: "SQL 执行超时,请检查数据库" #redis ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符,请修改后重试" diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index 76abd9f34..06d97d1e5 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -35,6 +35,7 @@ func Init() { migrations.AddMfaInterval, migrations.UpdateAppDetail, migrations.EncryptHostPassword, + migrations.AddRemoteDB, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index e8a1a1228..9bd5c3ec1 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -479,3 +479,13 @@ var EncryptHostPassword = &gormigrate.Migration{ return nil }, } + +var AddRemoteDB = &gormigrate.Migration{ + ID: "20230718-add-remote-db", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.Database{}); err != nil { + return err + } + return nil + }, +} diff --git a/backend/utils/mysql/client.go b/backend/utils/mysql/client.go new file mode 100644 index 000000000..9d064fb95 --- /dev/null +++ b/backend/utils/mysql/client.go @@ -0,0 +1,41 @@ +package mysql + +import ( + "database/sql" + "errors" + "fmt" + + "github.com/1Panel-dev/1Panel/backend/buserr" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/utils/cmd" + "github.com/1Panel-dev/1Panel/backend/utils/mysql/client" +) + +type MysqlClient interface { + Create(info client.CreateInfo) error + Delete(info client.DeleteInfo) error + + ChangePassword(info client.PasswordChangeInfo) error + ChangeAccess(info client.AccessChangeInfo) error + + Close() +} + +func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) { + if conn.Type == "remote" { + connArgs := fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8", conn.UserName, conn.Password, conn.Address, conn.Port) + db, err := sql.Open("mysql", connArgs) + if err != nil { + return nil, err + } + return client.NewRemote(db), nil + } + if conn.Type == "local" { + if cmd.CheckIllegal(conn.Address, conn.UserName, conn.Password) { + return nil, buserr.New(constant.ErrCmdIllegal) + } + connArgs := []string{"exec", conn.Address, "mysql", "-u" + conn.UserName, "-p" + conn.Password + "-e"} + return client.NewLocal(connArgs, conn.Address), nil + } + return nil, errors.New("no such type") +} diff --git a/backend/utils/mysql/client/info.go b/backend/utils/mysql/client/info.go new file mode 100644 index 000000000..8d478d32c --- /dev/null +++ b/backend/utils/mysql/client/info.go @@ -0,0 +1,60 @@ +package client + +type DBInfo struct { + Type string `json:"type"` // local remote + Address string `json:"address"` + Port uint `json:"port"` + UserName string `json:"userName"` + Password string `json:"password"` + Format string `json:"format"` + + Timeout uint `json:"timeout"` // second +} + +type CreateInfo struct { + Name string `json:"name"` + Format string `json:"format"` + Version string `json:"version"` + UserName string `json:"userName"` + Password string `json:"password"` + Permission string `json:"permission"` + + Timeout uint `json:"timeout"` // second +} + +type DeleteInfo struct { + Name string `json:"name"` + Version string `json:"version"` + UserName string `json:"userName"` + Permission string `json:"permission"` + + ForceDelete bool `json:"forceDelete"` + Timeout uint `json:"timeout"` // second +} + +type PasswordChangeInfo struct { + Name string `json:"name"` + Version string `json:"version"` + UserName string `json:"userName"` + Password string `json:"password"` + Permission string `json:"permission"` + + Timeout uint `json:"timeout"` // second +} + +type AccessChangeInfo struct { + Name string `json:"name"` + Version string `json:"version"` + UserName string `json:"userName"` + OldPermission string `json:"oldPermission"` + Permission string `json:"permission"` + + Timeout uint `json:"timeout"` // second +} + +var formatMap = map[string]string{ + "utf8": "utf8_general_ci", + "utf8mb4": "utf8mb4_general_ci", + "gbk": "gbk_chinese_ci", + "big5": "big5_chinese_ci", +} diff --git a/backend/utils/mysql/client/local.go b/backend/utils/mysql/client/local.go new file mode 100644 index 000000000..0c61e0a23 --- /dev/null +++ b/backend/utils/mysql/client/local.go @@ -0,0 +1,228 @@ +package client + +import ( + "context" + "errors" + "fmt" + "os/exec" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/backend/buserr" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" +) + +type Local struct { + PrefixCommand []string + ContainerName string +} + +func NewLocal(command []string, containerName string) *Local { + return &Local{PrefixCommand: command, ContainerName: containerName} +} + +func (r *Local) Create(info CreateInfo) error { + createSql := fmt.Sprintf("create database `%s` default character set %s collate %s", info.Name, info.Format, formatMap[info.Format]) + if err := r.ExecSQL(createSql, info.Timeout); err != nil { + if strings.Contains(err.Error(), "ERROR 1007") { + return buserr.New(constant.ErrDatabaseIsExist) + } + return err + } + + if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, UserName: info.UserName, Permission: info.Permission, Timeout: info.Timeout}); err != nil { + return err + } + + return nil +} + +func (r *Local) CreateUser(info CreateInfo) error { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission)) + } + + for _, user := range userlist { + if err := r.ExecSQL(fmt.Sprintf("create user %s identified by '%s';", user, info.Password), info.Timeout); err != nil { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Version: info.Version, + UserName: info.UserName, + Permission: info.Permission, + ForceDelete: true, + Timeout: 300}) + if strings.Contains(err.Error(), "ERROR 1396") { + return buserr.New(constant.ErrUserIsExist) + } + return err + } + grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", info.Name, user) + if info.Name == "*" { + grantStr = fmt.Sprintf("grant all privileges on *.* to %s", user) + } + if strings.HasPrefix(info.Version, "5.7") || strings.HasPrefix(info.Version, "5.6") { + grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, info.Password) + } + if err := r.ExecSQL(grantStr, info.Timeout); err != nil { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Version: info.Version, + UserName: info.UserName, + Permission: info.Permission, + ForceDelete: true, + Timeout: 300}) + return err + } + } + return nil +} + +func (r *Local) Delete(info DeleteInfo) error { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission)) + } + + for _, user := range userlist { + if strings.HasPrefix(info.Version, "5.6") { + if err := r.ExecSQL(fmt.Sprintf("drop user %s", user), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } else { + if err := r.ExecSQL(fmt.Sprintf("drop user if exists %s", user), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } + } + if len(info.Name) != 0 { + if err := r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } + if !info.ForceDelete { + global.LOG.Info("execute delete database sql successful, now start to drop uploads and records") + } + + return nil +} + +func (r *Local) ChangePassword(info PasswordChangeInfo) error { + if info.UserName != "root" { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission)) + } + + for _, user := range userlist { + passwordChangeSql := fmt.Sprintf("set password for %s = password('%s')", user, info.Password) + if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { + passwordChangeSql = fmt.Sprintf("ALTER USER %s IDENTIFIED WITH mysql_native_password BY '%s';", user, info.Password) + } + if err := r.ExecSQL(passwordChangeSql, info.Timeout); err != nil { + return err + } + } + return nil + } + + hosts, err := r.ExecSQLForRows("select host from mysql.user where user='root';", info.Timeout) + if err != nil { + return err + } + for _, host := range hosts { + if host == "%" || host == "localhost" { + passwordRootChangeCMD := fmt.Sprintf("set password for 'root'@'%s' = password('%s')", host, info.Password) + if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { + passwordRootChangeCMD = fmt.Sprintf("alter user 'root'@'%s' identified with mysql_native_password BY '%s';", host, info.Password) + } + if err := r.ExecSQL(passwordRootChangeCMD, info.Timeout); err != nil { + return err + } + } + } + + return nil +} + +func (r *Local) ChangeAccess(info AccessChangeInfo) error { + if info.UserName == "root" { + info.OldPermission = "%" + info.Name = "*" + } + if info.Permission != info.OldPermission { + if err := r.Delete(DeleteInfo{ + Version: info.Version, + UserName: info.UserName, + Permission: info.OldPermission, + ForceDelete: true, + Timeout: 300}); err != nil { + return err + } + if info.UserName == "root" { + return nil + } + } + if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, UserName: info.UserName, Permission: info.Permission, Timeout: info.Timeout}); err != nil { + return err + } + return nil +} + +func (r *Local) Close() {} + +func (r *Local) ExecSQL(command string, timeout uint) error { + itemCommand := r.PrefixCommand[:] + itemCommand = append(itemCommand, command) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "docker", itemCommand...) + stdout, err := cmd.CombinedOutput() + if ctx.Err() == context.DeadlineExceeded { + return buserr.New(constant.ErrExecTimeOut) + } + stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") + if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { + return errors.New(stdStr) + } + return nil +} + +func (r *Local) ExecSQLForRows(command string, timeout uint) ([]string, error) { + itemCommand := r.PrefixCommand[:] + itemCommand = append(itemCommand, command) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "docker", itemCommand...) + stdout, err := cmd.CombinedOutput() + if ctx.Err() == context.DeadlineExceeded { + return nil, buserr.New(constant.ErrExecTimeOut) + } + stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") + if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { + return nil, errors.New(stdStr) + } + return strings.Split(stdStr, "\n"), nil +} diff --git a/backend/utils/mysql/client/remote.go b/backend/utils/mysql/client/remote.go new file mode 100644 index 000000000..80a2158fe --- /dev/null +++ b/backend/utils/mysql/client/remote.go @@ -0,0 +1,230 @@ +package client + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/backend/buserr" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" +) + +type Remote struct { + Client *sql.DB +} + +func NewRemote(client *sql.DB) *Remote { + return &Remote{Client: client} +} + +func (r *Remote) Create(info CreateInfo) error { + createSql := fmt.Sprintf("create database `%s` default character set %s collate %s", info.Name, info.Format, formatMap[info.Format]) + if err := r.ExecSQL(createSql, info.Timeout); err != nil { + if strings.Contains(err.Error(), "ERROR 1007") { + return buserr.New(constant.ErrDatabaseIsExist) + } + return err + } + + if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, UserName: info.UserName, Permission: info.Permission, Timeout: info.Timeout}); err != nil { + return err + } + + return nil +} + +func (r *Remote) CreateUser(info CreateInfo) error { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission)) + } + + for _, user := range userlist { + if err := r.ExecSQL(fmt.Sprintf("create user %s identified by '%s';", user, info.Password), info.Timeout); err != nil { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Version: info.Version, + UserName: info.UserName, + Permission: info.Permission, + ForceDelete: true, + Timeout: 300}) + if strings.Contains(err.Error(), "ERROR 1396") { + return buserr.New(constant.ErrUserIsExist) + } + return err + } + grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", info.Name, user) + if info.Name == "*" { + grantStr = fmt.Sprintf("grant all privileges on *.* to %s", user) + } + if strings.HasPrefix(info.Version, "5.7") || strings.HasPrefix(info.Version, "5.6") { + grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, info.Password) + } + if err := r.ExecSQL(grantStr, info.Timeout); err != nil { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Version: info.Version, + UserName: info.UserName, + Permission: info.Permission, + ForceDelete: true, + Timeout: 300}) + return err + } + } + return nil +} + +func (r *Remote) Delete(info DeleteInfo) error { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission)) + } + + for _, user := range userlist { + if strings.HasPrefix(info.Version, "5.6") { + if err := r.ExecSQL(fmt.Sprintf("drop user %s", user), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } else { + if err := r.ExecSQL(fmt.Sprintf("drop user if exists %s", user), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } + } + if len(info.Name) != 0 { + if err := r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } + if !info.ForceDelete { + global.LOG.Info("execute delete database sql successful, now start to drop uploads and records") + } + + return nil +} + +func (r *Remote) ChangePassword(info PasswordChangeInfo) error { + if info.UserName != "root" { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission)) + } + + for _, user := range userlist { + passwordChangeSql := fmt.Sprintf("set password for %s = password('%s')", user, info.Password) + if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { + passwordChangeSql = fmt.Sprintf("ALTER USER %s IDENTIFIED WITH mysql_native_password BY '%s';", user, info.Password) + } + if err := r.ExecSQL(passwordChangeSql, info.Timeout); err != nil { + return err + } + } + return nil + } + + hosts, err := r.ExecSQLForHosts(info.Timeout) + if err != nil { + return err + } + for _, host := range hosts { + if host == "%" || host == "localhost" { + passwordRootChangeCMD := fmt.Sprintf("set password for 'root'@'%s' = password('%s')", host, info.Password) + if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { + passwordRootChangeCMD = fmt.Sprintf("alter user 'root'@'%s' identified with mysql_native_password BY '%s';", host, info.Password) + } + if err := r.ExecSQL(passwordRootChangeCMD, info.Timeout); err != nil { + return err + } + } + } + + return nil +} + +func (r *Remote) ChangeAccess(info AccessChangeInfo) error { + if info.UserName == "root" { + info.OldPermission = "%" + info.Name = "*" + } + if info.Permission != info.OldPermission { + if err := r.Delete(DeleteInfo{ + Version: info.Version, + UserName: info.UserName, + Permission: info.OldPermission, + ForceDelete: true, + Timeout: 300}); err != nil { + return err + } + if info.UserName == "root" { + return nil + } + } + if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, UserName: info.UserName, Permission: info.Permission, Timeout: info.Timeout}); err != nil { + return err + } + return nil +} + +func (r *Remote) Close() { + _ = r.Client.Close() +} + +func (r *Remote) ExecSQL(command string, timeout uint) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + + if _, err := r.Client.ExecContext(ctx, command); err != nil { + return err + } + if ctx.Err() == context.DeadlineExceeded { + return buserr.New(constant.ErrExecTimeOut) + } + + return nil +} + +func (r *Remote) ExecSQLForHosts(timeout uint) ([]string, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + + results, err := r.Client.QueryContext(ctx, "select host from mysql.user where user='root';") + if err != nil { + return nil, err + } + if ctx.Err() == context.DeadlineExceeded { + return nil, buserr.New(constant.ErrExecTimeOut) + } + var rows []string + for results.Next() { + var host string + if err := results.Scan(&host); err != nil { + continue + } + rows = append(rows, host) + } + return rows, nil +} diff --git a/frontend/src/api/interface/database.ts b/frontend/src/api/interface/database.ts index 7d91224a0..757f130a4 100644 --- a/frontend/src/api/interface/database.ts +++ b/frontend/src/api/interface/database.ts @@ -160,4 +160,17 @@ export namespace Database { fileName: string; fileDir: string; } + + // remote + export interface RemoteDBInfo { + id: number; + createdAt: Date; + name: string; + type: string; + address: string; + port: number; + username: string; + password: string; + description: string; + } } diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts index aec58fedb..6039668fb 100644 --- a/frontend/src/api/modules/database.ts +++ b/frontend/src/api/modules/database.ts @@ -83,3 +83,17 @@ export const updateRedisConf = (params: Database.RedisConfUpdate) => { export const updateRedisConfByFile = (params: Database.RedisConfUpdateByFile) => { return http.post(`/databases/redis/conffile/update`, params); }; + +// remote +export const searchRemoteDBs = (params: SearchWithPage) => { + return http.post>(`/databases/remote/search`, params); +}; +export const addRemoteDB = (params: Database.RemoteDBInfo) => { + return http.post(`/databases/remote`, params); +}; +export const editRemoteDB = (params: Database.RemoteDBInfo) => { + return http.post(`/databases/remote/update`, params); +}; +export const deleteRemoteDB = (id: number) => { + return http.post(`/databases/remote/del`, { id: id }); +}; diff --git a/frontend/src/views/database/remote-db/index.vue b/frontend/src/views/database/remote-db/index.vue new file mode 100644 index 000000000..77c3a6c6a --- /dev/null +++ b/frontend/src/views/database/remote-db/index.vue @@ -0,0 +1,152 @@ + + + diff --git a/frontend/src/views/database/remote-db/operate/index.vue b/frontend/src/views/database/remote-db/operate/index.vue new file mode 100644 index 000000000..b5b8db0a2 --- /dev/null +++ b/frontend/src/views/database/remote-db/operate/index.vue @@ -0,0 +1,112 @@ + + +