feat: add cluster status page (#9514)

This commit is contained in:
CityFun 2025-07-14 19:08:57 +08:00 committed by GitHub
parent ea1a34f57a
commit ab2cf0d0c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 92 additions and 55 deletions

View file

@ -371,12 +371,12 @@ func (b *BaseApi) Backup(c *gin.Context) {
helper.InternalServer(c, err)
return
}
case "mysql", "mariadb":
case "mysql", "mariadb", constant.AppMysqlCluster:
if err := backupService.MysqlBackup(req); err != nil {
helper.InternalServer(c, err)
return
}
case constant.AppPostgresql:
case constant.AppPostgresql, constant.AppPostgresqlCluster:
if err := backupService.PostgresqlBackup(req); err != nil {
helper.InternalServer(c, err)
return
@ -386,7 +386,7 @@ func (b *BaseApi) Backup(c *gin.Context) {
helper.InternalServer(c, err)
return
}
case "redis":
case "redis", constant.AppRedisCluster:
if err := backupService.RedisBackup(req); err != nil {
helper.InternalServer(c, err)
return

View file

@ -54,7 +54,7 @@ type BackupOption struct {
}
type CommonBackup struct {
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql"`
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster"`
Name string `json:"name"`
DetailName string `json:"detailName"`
Secret string `json:"secret"`
@ -65,7 +65,7 @@ type CommonBackup struct {
}
type CommonRecover struct {
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql"`
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster"`
Name string `json:"name"`
DetailName string `json:"detailName"`
File string `json:"file"`

View file

@ -4,14 +4,14 @@ import "time"
// common
type DBConfUpdateByFile struct {
Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql redis"`
Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql redis mysql-cluster postgresql-cluster"`
Database string `json:"database" validate:"required"`
File string `json:"file"`
}
type ChangeDBInfo struct {
ID uint `json:"id"`
From string `json:"from" validate:"required,oneof=local remote"`
Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql"`
Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql mysql-cluster postgresql-cluster"`
Database string `json:"database" validate:"required"`
Value string `json:"value" validate:"required"`
}
@ -74,19 +74,19 @@ type BindUser struct {
type MysqlLoadDB struct {
From string `json:"from" validate:"required,oneof=local remote"`
Type string `json:"type" validate:"required,oneof=mysql mariadb"`
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
Database string `json:"database" validate:"required"`
}
type MysqlDBDeleteCheck struct {
ID uint `json:"id" validate:"required"`
Type string `json:"type" validate:"required,oneof=mysql mariadb"`
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
Database string `json:"database" validate:"required"`
}
type MysqlDBDelete struct {
ID uint `json:"id" validate:"required"`
Type string `json:"type" validate:"required,oneof=mysql mariadb"`
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
Database string `json:"database" validate:"required"`
ForceDelete bool `json:"forceDelete"`
DeleteBackup bool `json:"deleteBackup"`
@ -153,7 +153,7 @@ type MysqlVariables struct {
}
type MysqlVariablesUpdate struct {
Type string `json:"type" validate:"required,oneof=mysql mariadb"`
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
Database string `json:"database" validate:"required"`
Variables []MysqlVariablesUpdateHelper `json:"variables"`
}

View file

@ -476,7 +476,11 @@ func (a *AppInstallService) GetServices(key string) ([]response.AppService, erro
if key == constant.AppPostgres {
key = constant.AppPostgresql
}
dbs, _ := databaseRepo.GetList(repo.WithByType(key))
types := []string{key}
if key == constant.AppMysql {
types = []string{constant.AppMysql, constant.AppMysqlCluster}
}
dbs, _ := databaseRepo.GetList(repo.WithTypes(types))
if len(dbs) == 0 {
return res, nil
}
@ -659,7 +663,7 @@ func (a *AppInstallService) GetDefaultConfigByKey(key, name string) (string, err
return "", buserr.New("ErrPathNotFound")
}
if key == constant.AppMysql || key == constant.AppMariaDB {
if key == constant.AppMysql || key == constant.AppMariaDB || key == constant.AppMysqlCluster {
filePath = path.Join(filePath, "my.cnf")
}
if key == constant.AppRedis {

View file

@ -254,7 +254,7 @@ func createLink(ctx context.Context, installTask *task.Task, app model.App, appI
var resourceId uint
if dbConfig.DbName != "" && dbConfig.DbUser != "" && dbConfig.Password != "" {
switch database.Type {
case constant.AppPostgresql, constant.AppPostgres:
case constant.AppPostgresql, constant.AppPostgres, constant.AppPostgresqlCluster:
oldPostgresqlDb, _ := postgresqlRepo.Get(repo.WithByName(dbConfig.DbName), repo.WithByFrom(constant.ResourceLocal))
resourceId = oldPostgresqlDb.ID
if oldPostgresqlDb.ID > 0 {
@ -276,7 +276,7 @@ func createLink(ctx context.Context, installTask *task.Task, app model.App, appI
}
resourceId = pgdb.ID
}
case constant.AppMysql, constant.AppMariaDB:
case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster:
oldMysqlDb, _ := mysqlRepo.Get(repo.WithByName(dbConfig.DbName), repo.WithByFrom(constant.ResourceLocal))
resourceId = oldMysqlDb.ID
if oldMysqlDb.ID > 0 {
@ -407,9 +407,9 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
}
switch install.App.Key {
case constant.AppMysql, constant.AppMariaDB:
case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster:
_ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name))
case constant.AppPostgresql:
case constant.AppPostgresql, constant.AppPostgresqlCluster:
_ = postgresqlRepo.Delete(ctx, postgresqlRepo.WithByPostgresqlName(install.Name))
}
@ -1494,7 +1494,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated bool, sync bool)
continue
}
lastVersion := versions[0]
if app.Key == constant.AppMysql {
if app.Key == constant.AppMysql || app.Key == constant.AppMysqlCluster {
for _, version := range versions {
majorVersion := getMajorVersion(installed.Version)
if !strings.HasPrefix(version, majorVersion) {

View file

@ -34,7 +34,7 @@ func (u *DBCommonService) LoadBaseInfo(req dto.OperationWithNameAndType) (*dto.D
}
data.ContainerName = app.ContainerName
data.Name = app.Name
data.Port = int64(app.Port)
data.Port = app.Port
return &data, nil
}
@ -42,6 +42,8 @@ func (u *DBCommonService) LoadBaseInfo(req dto.OperationWithNameAndType) (*dto.D
func (u *DBCommonService) LoadDatabaseFile(req dto.OperationWithNameAndType) (string, error) {
filePath := ""
switch req.Type {
case "mysql-cluster-conf":
filePath = path.Join(global.Dir.DataDir, fmt.Sprintf("apps/mysql-cluster/%s/conf/my.cnf", req.Name))
case "mysql-conf":
filePath = path.Join(global.Dir.DataDir, fmt.Sprintf("apps/mysql/%s/conf/my.cnf", req.Name))
case "mariadb-conf":

View file

@ -534,7 +534,7 @@ func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlS
info.File = "OFF"
info.Position = "OFF"
masterStatus := "show master status;"
if common.CompareAppVersion(app.Version, "8.4.0") && req.Type == constant.AppMysql {
if common.CompareAppVersion(app.Version, "8.4.0") && (req.Type == constant.AppMysql || req.Type == constant.AppMysqlCluster) {
masterStatus = "show binary log status;"
}
rows, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, masterStatus)
@ -553,10 +553,13 @@ func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlS
}
func executeSqlForMaps(containerName, dbType, password, command string) (map[string]string, error) {
if dbType == "mysql-cluster" {
dbType = "mysql"
}
cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command)
stdout, err := cmd.CombinedOutput()
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 ") {
if err != nil || strings.HasPrefix(stdStr, "ERROR ") {
return nil, errors.New(stdStr)
}
@ -572,10 +575,13 @@ func executeSqlForMaps(containerName, dbType, password, command string) (map[str
}
func executeSqlForRows(containerName, dbType, password, command string) ([]string, error) {
if dbType == "mysql-cluster" {
dbType = "mysql"
}
cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command)
stdout, err := cmd.CombinedOutput()
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 ") {
if err != nil || strings.HasPrefix(stdStr, "ERROR ") {
return nil, errors.New(stdStr)
}
return strings.Split(stdStr, "\n"), nil

View file

@ -30,7 +30,11 @@ type MysqlClient interface {
func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) {
if conn.From == "local" {
connArgs := []string{"exec", conn.Address, conn.Type, "-u" + conn.Username, "-p" + conn.Password, "-e"}
mysqlCli := conn.Type
if mysqlCli == "mysql-cluster" {
mysqlCli = "mysql"
}
connArgs := []string{"exec", conn.Address, mysqlCli, "-u" + conn.Username, "-p" + conn.Password, "-e"}
return client.NewLocal(connArgs, conn.Type, conn.Address, conn.Password, conn.Database), nil
}

View file

@ -1,9 +1,11 @@
package helper
import (
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/core/cmd/server/res"
"net/http"
"strconv"
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/global"
@ -70,11 +72,6 @@ func CheckBindAndValidate(req interface{}, c *gin.Context) error {
return nil
}
func ErrResponse(ctx *gin.Context, code int) {
ctx.JSON(code, nil)
ctx.Abort()
}
func ErrWithHtml(ctx *gin.Context, code int, scope string) {
if code == 444 {
ctx.String(444, "")
@ -94,3 +91,12 @@ func ErrWithHtml(ctx *gin.Context, code int, scope string) {
ctx.Data(code, "text/html; charset=utf-8", data)
ctx.Abort()
}
func GetParamID(c *gin.Context) (uint, error) {
idParam, ok := c.Params.Get("id")
if !ok {
return 0, errors.New("error id in path")
}
intNum, _ := strconv.Atoi(idParam)
return uint(intNum), nil
}

View file

@ -206,7 +206,6 @@ const onClean = async () => {
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
console.log(logSearch);
await cleanContainerLog(logSearch.container);
searchLogs();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));

View file

@ -3647,6 +3647,7 @@ const message = {
installNode: 'Install Node',
master: 'Master Node',
slave: 'Slave Node',
replicaStatus: 'Master-Slave Status',
},
},
};

View file

@ -3510,6 +3510,7 @@ const message = {
installNode: 'ノードをインストール',
master: 'マスターノード',
slave: 'スレーブノード',
replicaStatus: 'マスタースレーブステータス',
},
},
};

View file

@ -3448,6 +3448,7 @@ const message = {
installNode: '노드 설치',
master: '마스터 노드',
slave: '슬레이브 노드',
replicaStatus: '마스터-슬레이브 상태',
},
},
};

View file

@ -3591,6 +3591,7 @@ const message = {
installNode: 'Pasang Node',
master: 'Node Utama',
slave: 'Node Hamba',
replicaStatus: 'Utama-Hamba Status',
},
},
};

View file

@ -3598,6 +3598,7 @@ const message = {
installNode: 'Instalar ',
master: ' Mestre',
slave: ' Escravo',
replicaStatus: 'Status Mestre-Escravo',
},
},
};

View file

@ -3589,6 +3589,7 @@ const message = {
installNode: 'Установить узел',
master: 'Главный узел',
slave: 'Подчиненный узел',
replicaStatus: 'Состояние мастер-слейв',
},
},
};

View file

@ -3687,6 +3687,7 @@ const message = {
installNode: 'Установить узел',
master: 'Главный узел',
slave: 'Подчиненный узел',
replicaStatus: 'Ana-Çalışan Durumu',
},
},
};

View file

@ -3392,6 +3392,7 @@ const message = {
installNode: '安裝節點',
master: '主節點',
slave: '從節點',
replicaStatus: '主從狀態',
},
},
};

View file

@ -3372,6 +3372,7 @@ const message = {
installNode: '安装节点',
master: '主节点',
slave: '从节点',
replicaStatus: '主从状态',
},
},
};

View file

@ -14,7 +14,6 @@ export const jumpToInstall = (type: string, key: string) => {
}
switch (key) {
case 'mysql-cluster':
console.log('jumpToInstall mysql-cluster');
jumpToPath(router, '/xpack/cluster/mysql');
return true;
case 'redis-cluster':

View file

@ -56,8 +56,8 @@
:label="service.label"
></el-option>
</el-select>
<el-row :gutter="10" v-if="p.type == 'apps'">
<el-col :span="12">
<div v-if="p.type == 'apps'" class="flex space-x-4">
<div class="flex-1">
<el-form-item :prop="p.prop">
<el-select
v-model="form[p.envKey]"
@ -72,14 +72,14 @@
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
</div>
<div class="flex-2">
<el-form-item :prop="p.childProp">
<el-select
v-model="form[p.child.envKey]"
v-if="p.child.type == 'service'"
@change="changeService(form[p.child.envKey], p.services)"
class="p-w-300"
class="p-w-400"
>
<el-option
v-for="service in p.services"
@ -89,33 +89,33 @@
:disabled="service.status != 'Running'"
>
<el-row :gutter="5">
<el-col :span="10">
<el-col :span="18">
<span>{{ service.label }}</span>
</el-col>
<el-col :span="8">
<el-col :span="6">
<span v-if="service.from != ''">
<el-tag v-if="service.from === 'local'">
{{ $t('commons.table.local') }}
</el-tag>
<el-tag v-else type="success">{{ $t('database.remote') }}</el-tag>
<Status
class="ml-2"
:key="service.status"
:status="service.status"
></Status>
</span>
</el-col>
<el-col :span="6">
<Status :key="service.status" :status="service.status"></Status>
</el-col>
</el-row>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col>
<span v-if="p.child.type === 'service' && p.services.length === 0">
<el-link type="primary" underline="never" @click="toPage(form[p.envKey])">
{{ $t('app.toInstall') }}
</el-link>
</span>
</el-col>
</el-row>
</div>
<span v-if="p.child.type === 'service' && p.services.length === 0">
<el-link type="primary" underline="never" @click="toPage(form[p.envKey])">
{{ $t('app.toInstall') }}
</el-link>
</span>
</div>
<span class="input-help" v-if="p.description">{{ getDescription(p) }}</span>
</el-form-item>
</div>

View file

@ -234,7 +234,6 @@ const get = async () => {
if (d.type === 'number') {
value = Number(value);
}
console.log('d', d);
params.value.push({
default: value,
labelEn: d.labelEn,

View file

@ -12,7 +12,7 @@
</div>
</el-card>
</div>
<LayoutContent :title="currentDB?.type === 'mysql' ? 'MySQL ' : 'MariaDB '">
<LayoutContent>
<template #app v-if="currentDB?.from === 'local'">
<AppStatus
:app-key="appKey"
@ -74,7 +74,7 @@
<span>{{ item.database.substring(0, 25) }}...</span>
</el-tooltip>
<el-tag class="tagClass">
{{ item.type === 'mysql' ? 'MySQL' : 'MariaDB' }}
{{ mysqlName(item.type) }}
</el-tag>
</el-option>
</div>
@ -90,7 +90,7 @@
<span>{{ item.database.substring(0, 25) }}...</span>
</el-tooltip>
<el-tag class="tagClass">
{{ item.type === 'mysql' ? 'MySQL' : 'MariaDB' }}
{{ mysqlName(item.type) }}
</el-tag>
</el-option>
</div>
@ -227,7 +227,7 @@
class="mask-prompt"
>
<span>
{{ $t('commons.service.serviceNotStarted', [currentDB.type === 'mysql' ? 'MySQL' : 'Mariadb']) }}
{{ $t('commons.service.serviceNotStarted', [mysqlName(currentDB.type)]) }}
</span>
</el-card>
@ -355,6 +355,14 @@ const onChangeConn = async () => {
});
};
const mysqlName = (appType: string) => {
if (appType === 'mysql' || appType === 'mysql-cluster') {
return 'MySQL';
} else {
return 'MariaDB';
}
};
const goRemoteDB = async () => {
if (currentDB.value) {
globalStore.setCurrentDB(currentDB.value.database);

View file

@ -40,6 +40,7 @@
type="primary"
:disabled="mysqlStatus !== 'Running'"
@click="changeTab('slowLog')"
v-if="type != 'mysql-cluster'"
:plain="activeName !== 'slowLog'"
>
{{ $t('database.slowLog') }}
@ -94,7 +95,7 @@
<SlowLog
@loading="changeLoading"
@refresh="loadBaseInfo"
v-if="activeName === 'slowLog'"
v-if="activeName === 'slowLog' && type != 'mysql-cluster'"
ref="slowLogRef"
/>
</template>