feat: 添加clickhouse和mongodb的管理

This commit is contained in:
耗子
2026-03-27 15:09:06 +08:00
parent 4df9400fb2
commit 8ebe63ac3d
12 changed files with 728 additions and 101 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ const (
DatabaseTypePostgresql DatabaseType = "postgresql"
DatabaseTypeMongoDB DatabaseType = "mongodb"
DatabaseTypeClickHouse DatabaseType = "clickhouse"
DatabaseSQLite DatabaseType = "sqlite"
DatabaseTypeSQLite DatabaseType = "sqlite"
DatabaseTypeRedis DatabaseType = "redis"
)
+104 -25
View File
@@ -45,25 +45,58 @@ func (r *databaseRepo) List(page, limit uint, typ string) ([]*biz.Database, int6
database := make([]*biz.Database, 0)
for _, server := range databaseServer {
operator, err := r.getOperator(server)
if err != nil {
continue
}
if databases, err := operator.Databases(); err == nil {
for item := range slices.Values(databases) {
database = append(database, &biz.Database{
Type: server.Type,
Name: item.Name,
Server: server.Name,
ServerID: server.ID,
Encoding: item.CharSet,
Comment: item.Comment,
})
switch server.Type {
case biz.DatabaseTypeMongoDB:
mongo, err := db.NewMongoDB(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
if err != nil {
continue
}
if databases, err := mongo.Databases(); err == nil {
for item := range slices.Values(databases) {
database = append(database, &biz.Database{
Type: server.Type,
Name: item.Name,
Server: server.Name,
ServerID: server.ID,
})
}
}
mongo.Close()
case biz.DatabaseTypeSQLite:
sqlite, err := db.NewSQLite(server.Host)
if err != nil {
continue
}
if tables, err := sqlite.Tables(); err == nil {
for table := range slices.Values(tables) {
database = append(database, &biz.Database{
Type: server.Type,
Name: table,
Server: server.Name,
ServerID: server.ID,
})
}
}
sqlite.Close()
default:
operator, err := r.getOperator(server)
if err != nil {
continue
}
if databases, err := operator.Databases(); err == nil {
for item := range slices.Values(databases) {
database = append(database, &biz.Database{
Type: server.Type,
Name: item.Name,
Server: server.Name,
ServerID: server.ID,
Encoding: item.CharSet,
Comment: item.Comment,
})
}
}
operator.Close()
}
operator.Close()
}
if len(database) < int((page-1)*limit) {
@@ -127,6 +160,33 @@ func (r *databaseRepo) Create(ctx context.Context, req *request.DatabaseCreate)
if err = operator.(*db.Postgres).DatabaseComment(req.Name, req.Comment); err != nil {
return err
}
case biz.DatabaseTypeClickHouse:
if req.CreateUser {
if err = r.user.Create(ctx, &request.DatabaseUserCreate{
ServerID: req.ServerID,
Username: req.Username,
Password: req.Password,
}); err != nil {
return err
}
}
if err = operator.DatabaseCreate(req.Name); err != nil {
return err
}
if req.Username != "" {
if err = operator.PrivilegesGrant(req.Username, req.Name); err != nil {
return err
}
}
case biz.DatabaseTypeMongoDB:
mongo, mongoErr := db.NewMongoDB(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
if mongoErr != nil {
return mongoErr
}
defer mongo.Close()
if mongoErr = mongo.DatabaseCreate(req.Name); mongoErr != nil {
return mongoErr
}
}
// 记录日志
@@ -141,14 +201,27 @@ func (r *databaseRepo) Delete(ctx context.Context, serverID uint, name string) e
return err
}
operator, err := r.getOperator(server)
if err != nil {
return err
}
defer operator.Close()
if err = operator.DatabaseDrop(name); err != nil {
return err
switch server.Type {
case biz.DatabaseTypeMongoDB:
mongo, mongoErr := db.NewMongoDB(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
if mongoErr != nil {
return mongoErr
}
defer mongo.Close()
if mongoErr = mongo.DatabaseDrop(name); mongoErr != nil {
return mongoErr
}
case biz.DatabaseTypeSQLite:
return errors.New(r.t.Get("sqlite does not support dropping tables from here"))
default:
operator, opErr := r.getOperator(server)
if opErr != nil {
return opErr
}
defer operator.Close()
if opErr = operator.DatabaseDrop(name); opErr != nil {
return opErr
}
}
// 记录日志
@@ -193,6 +266,12 @@ func (r *databaseRepo) getOperator(server *biz.DatabaseServer) (db.Operator, err
return nil, err
}
return postgres, nil
case biz.DatabaseTypeClickHouse:
clickhouse, err := db.NewClickHouse(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
if err != nil {
return nil, err
}
return clickhouse, nil
default:
return nil, fmt.Errorf("unsupported database type: %s", server.Type)
}
+40 -1
View File
@@ -191,6 +191,25 @@ func (r *databaseServerRepo) Sync(id uint) error {
}
}
}
case biz.DatabaseTypeClickHouse:
allUsers, err := operator.Users()
if err != nil {
return err
}
for user := range slices.Values(allUsers) {
if !slices.ContainsFunc(users, func(a *biz.DatabaseUser) bool {
return a.Username == user.User
}) && !slices.Contains([]string{"default"}, user.User) {
newUser := &biz.DatabaseUser{
ServerID: id,
Username: user.User,
Remark: r.t.Get("sync from server %s", server.Name),
}
if err = r.db.Create(newUser).Error; err != nil {
r.log.Warn("sync clickhouse database user failed", slog.String("type", biz.OperationTypeDatabaseServer), slog.Uint64("operator_id", 0), slog.Any("err", err))
}
}
}
}
return nil
@@ -199,7 +218,7 @@ func (r *databaseServerRepo) Sync(id uint) error {
// checkServer 检查服务器连接
func (r *databaseServerRepo) checkServer(server *biz.DatabaseServer) bool {
switch server.Type {
case biz.DatabaseTypeMysql, biz.DatabaseTypePostgresql:
case biz.DatabaseTypeMysql, biz.DatabaseTypePostgresql, biz.DatabaseTypeClickHouse:
operator, err := r.getOperator(server)
if err == nil {
operator.Close()
@@ -213,6 +232,20 @@ func (r *databaseServerRepo) checkServer(server *biz.DatabaseServer) bool {
server.Status = biz.DatabaseServerStatusValid
return true
}
case biz.DatabaseTypeMongoDB:
mongo, err := db.NewMongoDB(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
if err == nil {
mongo.Close()
server.Status = biz.DatabaseServerStatusValid
return true
}
case biz.DatabaseTypeSQLite:
sqlite, err := db.NewSQLite(server.Host)
if err == nil {
sqlite.Close()
server.Status = biz.DatabaseServerStatusValid
return true
}
}
server.Status = biz.DatabaseServerStatusInvalid
@@ -233,6 +266,12 @@ func (r *databaseServerRepo) getOperator(server *biz.DatabaseServer) (db.Operato
return nil, err
}
return postgres, nil
case biz.DatabaseTypeClickHouse:
clickhouse, err := db.NewClickHouse(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
if err != nil {
return nil, err
}
return clickhouse, nil
default:
return nil, fmt.Errorf("unsupported database type: %s", server.Type)
}
+16 -1
View File
@@ -223,7 +223,7 @@ func (r *databaseUserRepo) DeleteByNames(serverID uint, names []string) error {
}
_ = operator.UserDrop(name, host)
}
case biz.DatabaseTypePostgresql:
case biz.DatabaseTypePostgresql, biz.DatabaseTypeClickHouse:
for name := range slices.Values(names) {
_ = operator.UserDrop(name)
}
@@ -257,6 +257,15 @@ func (r *databaseUserRepo) fillUser(user *biz.DatabaseUser) {
} else {
user.Status = biz.DatabaseUserStatusInvalid
}
case biz.DatabaseTypeClickHouse:
privileges, _ := operator.UserPrivileges(user.Username)
user.Privileges = privileges
if ch2, err := db.NewClickHouse(user.Username, user.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)); err == nil {
ch2.Close()
user.Status = biz.DatabaseUserStatusValid
} else {
user.Status = biz.DatabaseUserStatusInvalid
}
}
} else {
user.Status = biz.DatabaseUserStatusInvalid
@@ -282,6 +291,12 @@ func (r *databaseUserRepo) getOperator(server *biz.DatabaseServer) (db.Operator,
return nil, err
}
return postgres, nil
case biz.DatabaseTypeClickHouse:
clickhouse, err := db.NewClickHouse(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
if err != nil {
return nil, err
}
return clickhouse, nil
default:
return nil, fmt.Errorf("unsupported database type: %s", server.Type)
}
+1 -1
View File
@@ -2,7 +2,7 @@ package request
type DatabaseServerCreate struct {
Name string `form:"name" json:"name" validate:"required|notExists:database_servers,name|regex:^[a-zA-Z0-9_-]+$"`
Type string `form:"type" json:"type" validate:"required|in:mysql,postgresql,redis"`
Type string `form:"type" json:"type" validate:"required|in:mysql,postgresql,redis,clickhouse,mongodb,sqlite"`
Host string `form:"host" json:"host" validate:"required"`
Port uint `form:"port" json:"port" validate:"required|min:1|max:65535"`
Username string `form:"username" json:"username"`
+200
View File
@@ -0,0 +1,200 @@
package db
import (
"database/sql"
"fmt"
"net/url"
"strings"
"resty.dev/v3"
)
// ClickHouse 通过 HTTP API 操作 ClickHouse
type ClickHouse struct {
client *resty.Client
address string
username string
password string
}
// NewClickHouse 创建 ClickHouse 连接(HTTP API
func NewClickHouse(username, password, address string) (*ClickHouse, error) {
client := resty.New()
client.SetBaseURL(fmt.Sprintf("http://%s", address))
client.SetTimeout(10 * 1000 * 1000 * 1000) // 10s
ch := &ClickHouse{
client: client,
address: address,
username: username,
password: password,
}
// 测试连接
if err := ch.Ping(); err != nil {
_ = client.Close()
return nil, fmt.Errorf("connect to clickhouse failed: %w", err)
}
return ch, nil
}
func (r *ClickHouse) Close() {
_ = r.client.Close()
}
func (r *ClickHouse) Ping() error {
_, err := r.exec("SELECT 1")
return err
}
func (r *ClickHouse) Query(query string, args ...any) (*sql.Rows, error) {
return nil, fmt.Errorf("clickhouse HTTP API does not support sql.Rows")
}
func (r *ClickHouse) QueryRow(query string, args ...any) *sql.Row {
return nil
}
func (r *ClickHouse) Exec(query string, args ...any) (sql.Result, error) {
_, err := r.exec(query)
return nil, err
}
func (r *ClickHouse) Prepare(query string) (*sql.Stmt, error) {
return nil, fmt.Errorf("clickhouse HTTP API does not support Prepare")
}
func (r *ClickHouse) DatabaseCreate(name string) error {
_, err := r.exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", name))
return err
}
func (r *ClickHouse) DatabaseDrop(name string) error {
_, err := r.exec(fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", name))
return err
}
func (r *ClickHouse) DatabaseExists(name string) (bool, error) {
result, err := r.exec(fmt.Sprintf("SELECT count() FROM system.databases WHERE name = '%s'", name))
if err != nil {
return false, err
}
return strings.TrimSpace(result) != "0", nil
}
func (r *ClickHouse) DatabaseSize(name string) (int64, error) {
result, err := r.exec(fmt.Sprintf("SELECT COALESCE(sum(bytes_on_disk), 0) FROM system.parts WHERE database = '%s'", name))
if err != nil {
return 0, err
}
var size int64
if _, err = fmt.Sscanf(strings.TrimSpace(result), "%d", &size); err != nil {
return 0, nil
}
return size, nil
}
func (r *ClickHouse) UserCreate(user, password string, host ...string) error {
_, err := r.exec(fmt.Sprintf("CREATE USER IF NOT EXISTS `%s` IDENTIFIED BY '%s'", user, password))
return err
}
func (r *ClickHouse) UserDrop(user string, host ...string) error {
_, err := r.exec(fmt.Sprintf("DROP USER IF EXISTS `%s`", user))
return err
}
func (r *ClickHouse) UserPassword(user, password string, host ...string) error {
_, err := r.exec(fmt.Sprintf("ALTER USER `%s` IDENTIFIED BY '%s'", user, password))
return err
}
func (r *ClickHouse) UserPrivileges(user string, host ...string) ([]string, error) {
result, err := r.exec(fmt.Sprintf("SHOW GRANTS FOR `%s`", user))
if err != nil {
return nil, err
}
var databases []string
for line := range strings.SplitSeq(result, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// 解析 GRANT ALL ON database.* TO user 格式
if idx := strings.Index(line, " ON "); idx != -1 {
rest := line[idx+4:]
if dotIdx := strings.Index(rest, "."); dotIdx != -1 {
dbName := strings.Trim(rest[:dotIdx], "`")
if dbName != "" && dbName != "*" {
databases = append(databases, dbName)
}
}
}
}
return databases, nil
}
func (r *ClickHouse) PrivilegesGrant(user, database string, host ...string) error {
_, err := r.exec(fmt.Sprintf("GRANT ALL ON `%s`.* TO `%s`", database, user))
return err
}
func (r *ClickHouse) PrivilegesRevoke(user, database string, host ...string) error {
_, err := r.exec(fmt.Sprintf("REVOKE ALL ON `%s`.* FROM `%s`", database, user))
return err
}
func (r *ClickHouse) Users() ([]User, error) {
result, err := r.exec("SELECT name FROM system.users FORMAT TabSeparated")
if err != nil {
return nil, err
}
var users []User
for line := range strings.SplitSeq(result, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
users = append(users, User{User: line})
}
return users, nil
}
func (r *ClickHouse) Databases() ([]Database, error) {
result, err := r.exec("SELECT name FROM system.databases WHERE name NOT IN ('system', 'INFORMATION_SCHEMA', 'information_schema') FORMAT TabSeparated")
if err != nil {
return nil, err
}
var databases []Database
for line := range strings.SplitSeq(result, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
databases = append(databases, Database{Name: line})
}
return databases, nil
}
// exec 通过 HTTP API 执行 SQL
func (r *ClickHouse) exec(query string) (string, error) {
resp, err := r.client.R().
SetQueryParam("query", query).
SetQueryParam("user", r.username).
SetQueryParam("password", url.QueryEscape(r.password)).
Get("/")
if err != nil {
return "", fmt.Errorf("clickhouse query failed: %w", err)
}
if resp.StatusCode() != 200 {
return "", fmt.Errorf("clickhouse query error: %s", strings.TrimSpace(resp.String()))
}
return resp.String(), nil
}
+160
View File
@@ -0,0 +1,160 @@
package db
import (
"encoding/json"
"fmt"
"strings"
"github.com/acepanel/panel/v3/pkg/shell"
)
// MongoDB 通过 mongosh CLI 操作 MongoDB
type MongoDB struct {
username string
password string
address string // host:port
}
// NewMongoDB 创建 MongoDB 连接
func NewMongoDB(username, password, address string) (*MongoDB, error) {
m := &MongoDB{
username: username,
password: password,
address: address,
}
if err := m.Ping(); err != nil {
return nil, fmt.Errorf("connect to mongodb failed: %w", err)
}
return m, nil
}
func (r *MongoDB) Close() {}
func (r *MongoDB) Ping() error {
_, err := r.mongosh(`db.runCommand({ping:1})`)
return err
}
// DatabaseCreate 创建数据库(MongoDB 通过创建集合来显式创建数据库)
func (r *MongoDB) DatabaseCreate(name string) error {
_, err := r.mongosh(fmt.Sprintf(`db.getSiblingDB('%s').createCollection('_init')`, name))
return err
}
// DatabaseDrop 删除数据库
func (r *MongoDB) DatabaseDrop(name string) error {
_, err := r.mongosh(fmt.Sprintf(`db.getSiblingDB('%s').dropDatabase()`, name))
return err
}
// Databases 获取数据库列表
func (r *MongoDB) Databases() ([]MongoDatabase, error) {
raw, err := r.mongosh(`JSON.stringify(db.adminCommand({listDatabases:1,nameOnly:false}))`)
if err != nil {
return nil, err
}
var result struct {
Databases []struct {
Name string `json:"name"`
SizeOnDisk int64 `json:"sizeOnDisk"`
} `json:"databases"`
}
if err = json.Unmarshal([]byte(raw), &result); err != nil {
return nil, fmt.Errorf("failed to parse databases: %w", err)
}
var databases []MongoDatabase
for _, db := range result.Databases {
if db.Name == "admin" || db.Name == "config" || db.Name == "local" {
continue
}
databases = append(databases, MongoDatabase{
Name: db.Name,
SizeOnDisk: db.SizeOnDisk,
})
}
return databases, nil
}
// UserCreate 创建用户
func (r *MongoDB) UserCreate(user, password, database string) error {
_, err := r.mongosh(fmt.Sprintf(`db.getSiblingDB('%s').createUser({user:'%s',pwd:'%s',roles:[{role:'readWrite',db:'%s'}]})`, database, user, password, database))
return err
}
// UserDrop 删除用户
func (r *MongoDB) UserDrop(user, database string) error {
_, err := r.mongosh(fmt.Sprintf(`db.getSiblingDB('%s').dropUser('%s')`, database, user))
return err
}
// UserPassword 修改用户密码
func (r *MongoDB) UserPassword(user, password string) error {
_, err := r.mongosh(fmt.Sprintf(`db.getSiblingDB('admin').changeUserPassword('%s','%s')`, user, password))
return err
}
// Users 获取用户列表
func (r *MongoDB) Users() ([]MongoUser, error) {
raw, err := r.mongosh(`JSON.stringify(db.getSiblingDB('admin').system.users.find({},{user:1,db:1,roles:1}).toArray())`)
if err != nil {
return nil, err
}
var result []struct {
User string `json:"user"`
DB string `json:"db"`
Roles []struct {
Role string `json:"role"`
DB string `json:"db"`
} `json:"roles"`
}
if err = json.Unmarshal([]byte(raw), &result); err != nil {
return nil, fmt.Errorf("failed to parse users: %w", err)
}
var users []MongoUser
for _, u := range result {
var roles []string
for _, role := range u.Roles {
roles = append(roles, fmt.Sprintf("%s@%s", role.Role, role.DB))
}
users = append(users, MongoUser{
User: u.User,
DB: u.DB,
Roles: roles,
})
}
return users, nil
}
// mongosh 执行 mongosh 命令
func (r *MongoDB) mongosh(eval string) (string, error) {
cmd := fmt.Sprintf(`mongosh --quiet --eval "%s" mongodb://%s:%s@%s/admin 2>/dev/null`,
strings.ReplaceAll(eval, `"`, `\"`),
r.username, r.password, r.address,
)
raw, err := shell.Execf(cmd)
if err != nil {
return "", fmt.Errorf("mongosh error: %w", err)
}
return strings.TrimSpace(raw), nil
}
// MongoDatabase MongoDB 数据库信息
type MongoDatabase struct {
Name string `json:"name"`
SizeOnDisk int64 `json:"size_on_disk"`
}
// MongoUser MongoDB 用户信息
type MongoUser struct {
User string `json:"user"`
DB string `json:"db"`
Roles []string `json:"roles"`
}
+84
View File
@@ -0,0 +1,84 @@
package db
import (
"database/sql"
"fmt"
_ "modernc.org/sqlite"
)
// SQLite 通过 database/sql 操作 SQLite 文件
type SQLite struct {
db *sql.DB
path string
}
// NewSQLite 打开 SQLite 数据库文件
func NewSQLite(path string) (*SQLite, error) {
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("open sqlite failed: %w", err)
}
if err = db.Ping(); err != nil {
return nil, fmt.Errorf("connect to sqlite failed: %w", err)
}
return &SQLite{db: db, path: path}, nil
}
func (r *SQLite) Close() {
_ = r.db.Close()
}
func (r *SQLite) Ping() error {
return r.db.Ping()
}
// Tables 获取所有表
func (r *SQLite) Tables() ([]string, error) {
rows, err := r.db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
if err != nil {
return nil, err
}
defer rows.Close()
var tables []string
for rows.Next() {
var name string
if err = rows.Scan(&name); err != nil {
continue
}
tables = append(tables, name)
}
return tables, rows.Err()
}
// SQLiteColumn 表列信息
type SQLiteColumn struct {
CID int `json:"cid"`
Name string `json:"name"`
Type string `json:"type"`
NotNull bool `json:"not_null"`
Default string `json:"default"`
PrimaryKey bool `json:"primary_key"`
}
// TableInfo 获取表结构
func (r *SQLite) TableInfo(name string) ([]SQLiteColumn, error) {
rows, err := r.db.Query(fmt.Sprintf("PRAGMA table_info('%s')", name))
if err != nil {
return nil, err
}
defer rows.Close()
var columns []SQLiteColumn
for rows.Next() {
var col SQLiteColumn
var dflt sql.NullString
if err = rows.Scan(&col.CID, &col.Name, &col.Type, &col.NotNull, &dflt, &col.PrimaryKey); err != nil {
continue
}
col.Default = dflt.String
columns = append(columns, col)
}
return columns, rows.Err()
}
+59 -47
View File
@@ -12,6 +12,9 @@ const show = defineModel<boolean>('show', { type: Boolean, required: true })
const defaultPort = (type: string) => {
if (type === 'postgresql') return 5432
if (type === 'redis') return 6379
if (type === 'clickhouse') return 8123
if (type === 'mongodb') return 27017
if (type === 'sqlite') return 0
return 3306
}
@@ -28,6 +31,9 @@ const createModel = ref({
const typeOptions = [
{ label: 'MySQL', value: 'mysql' },
{ label: 'PostgreSQL', value: 'postgresql' },
{ label: 'ClickHouse', value: 'clickhouse' },
{ label: 'MongoDB', value: 'mongodb' },
{ label: 'SQLite', value: 'sqlite' },
{ label: 'Redis', value: 'redis' }
]
@@ -35,13 +41,7 @@ const typeOptions = [
watch(
() => createModel.value.type,
(val) => {
if (val === 'postgresql') {
createModel.value.port = 5432
} else if (val === 'redis') {
createModel.value.port = 6379
} else {
createModel.value.port = 3306
}
createModel.value.port = defaultPort(val)
}
)
@@ -100,46 +100,58 @@ const handleCreate = () => {
:placeholder="$gettext('Enter database server name')"
/>
</n-form-item>
<n-row :gutter="[0, 24]">
<n-col :span="15">
<n-form-item path="host" :label="$gettext('Host')">
<n-input
v-model:value="createModel.host"
type="text"
@keydown.enter.prevent
:placeholder="$gettext('Enter database server host')"
/>
</n-form-item>
</n-col>
<n-col :span="2"></n-col>
<n-col :span="7">
<n-form-item path="port" :label="$gettext('Port')">
<n-input-number
w-full
v-model:value="createModel.port"
@keydown.enter.prevent
:placeholder="$gettext('Enter database server port')"
/>
</n-form-item>
</n-col>
</n-row>
<n-form-item v-if="createModel.type !== 'redis'" path="username" :label="$gettext('Username')">
<n-input
v-model:value="createModel.username"
type="text"
@keydown.enter.prevent
:placeholder="$gettext('Enter database server username')"
/>
</n-form-item>
<n-form-item path="password" :label="$gettext('Password')">
<n-input
v-model:value="createModel.password"
type="password"
show-password-on="click"
@keydown.enter.prevent
:placeholder="$gettext('Enter database server password')"
/>
</n-form-item>
<template v-if="createModel.type === 'sqlite'">
<n-form-item path="host" :label="$gettext('File Path')">
<n-input
v-model:value="createModel.host"
type="text"
@keydown.enter.prevent
:placeholder="$gettext('Enter SQLite database file path, e.g. /data/app.db')"
/>
</n-form-item>
</template>
<template v-else>
<n-row :gutter="[0, 24]">
<n-col :span="15">
<n-form-item path="host" :label="$gettext('Host')">
<n-input
v-model:value="createModel.host"
type="text"
@keydown.enter.prevent
:placeholder="$gettext('Enter database server host')"
/>
</n-form-item>
</n-col>
<n-col :span="2"></n-col>
<n-col :span="7">
<n-form-item path="port" :label="$gettext('Port')">
<n-input-number
w-full
v-model:value="createModel.port"
@keydown.enter.prevent
:placeholder="$gettext('Enter database server port')"
/>
</n-form-item>
</n-col>
</n-row>
<n-form-item v-if="!['redis', 'sqlite'].includes(createModel.type)" path="username" :label="$gettext('Username')">
<n-input
v-model:value="createModel.username"
type="text"
@keydown.enter.prevent
:placeholder="$gettext('Enter database server username')"
/>
</n-form-item>
<n-form-item v-if="createModel.type !== 'sqlite'" path="password" :label="$gettext('Password')">
<n-input
v-model:value="createModel.password"
type="password"
show-password-on="click"
@keydown.enter.prevent
:placeholder="$gettext('Enter database server password')"
/>
</n-form-item>
</template>
<n-form-item path="remark" :label="$gettext('Comment')">
<n-input
v-model:value="createModel.remark"
+7 -1
View File
@@ -27,6 +27,9 @@ const createServerModalShow = ref(false)
<n-tabs v-model:value="currentTab" animated>
<n-tab name="mysql" tab="MySQL" />
<n-tab name="postgresql" tab="PostgreSQL" />
<n-tab name="clickhouse" tab="ClickHouse" />
<n-tab name="mongodb" tab="MongoDB" />
<n-tab name="sqlite" tab="SQLite" />
<n-tab name="redis" tab="Redis" />
<n-tab name="user" :tab="$gettext('User')" />
<n-tab name="server" :tab="$gettext('Server')" />
@@ -35,7 +38,7 @@ const createServerModalShow = ref(false)
<n-flex vertical>
<n-flex v-if="currentTab !== 'redis'">
<n-button
v-if="currentTab === 'mysql' || currentTab === 'postgresql'"
v-if="['mysql', 'postgresql', 'clickhouse', 'mongodb'].includes(currentTab)"
type="primary"
@click="createDatabaseModalShow = true"
>
@@ -54,6 +57,9 @@ const createServerModalShow = ref(false)
</n-flex>
<database-list v-if="currentTab === 'mysql'" type="mysql" />
<database-list v-if="currentTab === 'postgresql'" type="postgresql" />
<database-list v-if="currentTab === 'clickhouse'" type="clickhouse" />
<database-list v-if="currentTab === 'mongodb'" type="mongodb" />
<database-list v-if="currentTab === 'sqlite'" type="sqlite" />
<redis-data-view v-if="currentTab === 'redis'" />
<user-list v-if="currentTab === 'user'" />
<server-list v-if="currentTab === 'server'" />
+11 -2
View File
@@ -40,6 +40,15 @@ const openTerminal = (row: any) => {
} else {
terminalCommand.value = `redis-cli -h '${row.host}' -p '${row.port}'`
}
} else if (row.type === 'clickhouse') {
terminalTitle.value = `ClickHouse - ${row.name}`
terminalCommand.value = `clickhouse-client --host '${row.host}' --port 9000 --user '${row.username}' --password '${row.password}'`
} else if (row.type === 'mongodb') {
terminalTitle.value = `MongoDB - ${row.name}`
terminalCommand.value = `mongosh mongodb://${row.username}:${row.password}@${row.host}:${row.port}/admin`
} else if (row.type === 'sqlite') {
terminalTitle.value = `SQLite - ${row.name}`
terminalCommand.value = `sqlite3 '${row.host}'`
} else {
window.$message.error($gettext('Unsupported database type'))
return
@@ -161,7 +170,7 @@ const columns: any = [
default: () => $gettext('Terminal')
}
),
row.type !== 'redis'
!['redis', 'mongodb', 'sqlite'].includes(row.type)
? h(
NPopconfirm,
{
@@ -214,7 +223,7 @@ const columns: any = [
{
onPositiveClick: () => {
// 防手贱
if (['local_mysql', 'local_postgresql', 'local_redis'].includes(row.name)) {
if (['local_mysql', 'local_postgresql', 'local_redis', 'local_clickhouse', 'local_mongodb'].includes(row.name)) {
window.$message.error(
$gettext(
'Built-in servers cannot be deleted. If you need to delete them, please uninstall the corresponding app'
+45 -22
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { DataTableSortState, DropdownOption } from 'naive-ui'
import { NTag } from 'naive-ui'
import { NButton, NDropdown, NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import process, { type ProcessListParams } from '@/api/panel/process'
@@ -54,24 +54,21 @@ const statusOptions = [
{ label: $gettext('Locked'), value: 'L' }
]
// 右键菜单选项
const dropdownOptions = computed<DropdownOption[]>(() => {
if (!selectedRow.value) return []
return [
{ label: $gettext('View Details'), key: 'detail' },
{ type: 'divider', key: 'd1' },
{ label: $gettext('Terminate (SIGTERM)'), key: 'sigterm' },
{ label: $gettext('Kill (SIGKILL)'), key: 'sigkill' },
{ type: 'divider', key: 'd2' },
{ label: $gettext('Stop (SIGSTOP)'), key: 'sigstop' },
{ label: $gettext('Continue (SIGCONT)'), key: 'sigcont' },
{ type: 'divider', key: 'd3' },
{ label: $gettext('Interrupt (SIGINT)'), key: 'sigint' },
{ label: $gettext('Hang Up (SIGHUP)'), key: 'sighup' },
{ label: $gettext('User Signal 1 (SIGUSR1)'), key: 'sigusr1' },
{ label: $gettext('User Signal 2 (SIGUSR2)'), key: 'sigusr2' }
]
})
// 操作菜单选项
const actionOptions: DropdownOption[] = [
{ label: $gettext('View Details'), key: 'detail' },
{ type: 'divider', key: 'd1' },
{ label: $gettext('Terminate (SIGTERM)'), key: 'sigterm' },
{ label: $gettext('Kill (SIGKILL)'), key: 'sigkill' },
{ type: 'divider', key: 'd2' },
{ label: $gettext('Stop (SIGSTOP)'), key: 'sigstop' },
{ label: $gettext('Continue (SIGCONT)'), key: 'sigcont' },
{ type: 'divider', key: 'd3' },
{ label: $gettext('Interrupt (SIGINT)'), key: 'sigint' },
{ label: $gettext('Hang Up (SIGHUP)'), key: 'sighup' },
{ label: $gettext('User Signal 1 (SIGUSR1)'), key: 'sigusr1' },
{ label: $gettext('User Signal 2 (SIGUSR2)'), key: 'sigusr2' }
]
// 渲染状态标签
const renderStatus = (status: string) => {
@@ -167,10 +164,33 @@ const columns = computed<any[]>(() => [
width: 240,
sortOrder: sortKeyMapOrder.value.start_time || false,
sorter: true
},
{
title: $gettext('Actions'),
key: 'actions',
width: 80,
fixed: 'right' as const,
render(row: any) {
return h(
NDropdown,
{
options: actionOptions,
trigger: 'click',
onSelect: (key: string) => {
selectedRow.value = row
handleDropdownSelect(key)
}
},
{
default: () =>
h(NButton, { size: 'small', ghost: true }, { default: () => $gettext('Actions') })
}
)
}
}
])
// 行属性 - 右键菜单
// 行属性 - 右键菜单 & 双击查看详情
const rowProps = (row: any) => {
return {
onContextmenu: (e: MouseEvent) => {
@@ -182,6 +202,9 @@ const rowProps = (row: any) => {
dropdownX.value = e.clientX
dropdownY.value = e.clientY
})
},
onDblclick: () => {
handleShowDetail(row.pid)
}
}
}
@@ -334,7 +357,7 @@ const { loading, data, page, total, pageSize, pageCount, reload } = usePaginatio
striped
remote
virtual-scroll
:scroll-x="1400"
:scroll-x="1480"
:loading="loading"
:columns="columns"
:data="data"
@@ -361,7 +384,7 @@ const { loading, data, page, total, pageSize, pageCount, reload } = usePaginatio
trigger="manual"
:x="dropdownX"
:y="dropdownY"
:options="dropdownOptions"
:options="actionOptions"
:show="showDropdown"
:on-clickoutside="onCloseDropdown"
@select="handleDropdownSelect"