1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 14:08:06 +08:00

feat: 概览页支持调整应用显示 (#6892)

Refs #4862 #3819
This commit is contained in:
ssongliu 2024-10-30 15:58:40 +08:00 committed by GitHub
parent ef793f278a
commit f79adc9bc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 808 additions and 121 deletions

View File

@ -4,6 +4,8 @@ import (
"errors"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/gin-gonic/gin"
)
@ -23,6 +25,43 @@ func (b *BaseApi) LoadDashboardOsInfo(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Dashboard
// @Summary Load app launcher
// @Description 获取应用展示列表
// @Accept json
// @Success 200 {Array} dto.dto.AppLauncher
// @Security ApiKeyAuth
// @Router /dashboard/app/launcher [get]
func (b *BaseApi) LoadAppLauncher(c *gin.Context) {
data, err := dashboardService.LoadAppLauncher()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Dashboard
// @Summary Load app launcher options
// @Description 获取应用展示选项
// @Accept json
// @Param request body dto.SearchByFilter true "request"
// @Success 200 {Array} dto.LauncherOption
// @Security ApiKeyAuth
// @Router /dashboard/app/launcher/option [post]
func (b *BaseApi) LoadAppLauncherOption(c *gin.Context) {
var req dto.SearchByFilter
if err := helper.CheckBind(&req, c); err != nil {
return
}
data, err := dashboardService.ListLauncherOption(req.Filter)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Dashboard
// @Summary Load dashboard base info
// @Description 获取首页基础数据

View File

@ -27,6 +27,10 @@ type Operate struct {
Operation string `json:"operation" validate:"required"`
}
type SearchByFilter struct {
Filter string `json:"filter"`
}
type BatchDeleteReq struct {
Ids []uint `json:"ids" validate:"required"`
}

View File

@ -126,3 +126,33 @@ type GPUInfo struct {
MemTotal string `json:"memTotal"`
FanSpeed string `json:"fanSpeed"`
}
type AppLauncher struct {
Key string `json:"key"`
Type string `json:"type"`
Name string `json:"name"`
Icon string `json:"icon"`
Limit int `json:"limit"`
ShortDescZh string `json:"shortDescZh"`
ShortDescEn string `json:"shortDescEn"`
Recommend int `json:"recomend"`
IsInstall bool `json:"isInstall"`
IsRecommend bool `json:"isRecommend"`
Detail []InstallDetail `json:"detail"`
}
type InstallDetail struct {
InstallID uint `json:"installID"`
DetailID uint `json:"detailID"`
Name string `json:"name"`
Version string `json:"version"`
Path string `json:"path"`
Status string `json:"status"`
WebUI string `json:"webUI"`
HttpPort int `json:"httpPort"`
HttpsPort int `json:"httpsPort"`
}
type LauncherOption struct {
Key string `json:"key"`
IsShow bool `json:"isShow"`
}

View File

@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
"sync"
@ -12,6 +13,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/copier"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/shirou/gopsutil/v3/cpu"
@ -30,6 +32,8 @@ type IDashboardService interface {
LoadCurrentInfoForNode() *dto.NodeCurrent
LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent
LoadAppLauncher() ([]dto.AppLauncher, error)
ListLauncherOption(filter string) ([]dto.LauncherOption, error)
Restart(operation string) error
}
@ -240,6 +244,109 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
return &currentInfo
}
func (u *DashboardService) LoadAppLauncher() ([]dto.AppLauncher, error) {
var (
data []dto.AppLauncher
recommendList []dto.AppLauncher
)
appInstalls, err := appInstallRepo.ListBy()
if err != nil {
return data, err
}
apps, err := appRepo.GetBy()
if err != nil {
return data, err
}
showList := loadShowList()
defaultList := []string{"openresty", "mysql", "halo", "redis", "maxkb", "wordpress"}
allList := common.RemoveRepeatStr(append(defaultList, showList...))
for _, showItem := range allList {
var itemData dto.AppLauncher
for _, app := range apps {
if showItem == app.Key {
itemData.Key = app.Key
itemData.Type = app.Type
itemData.Name = app.Name
itemData.Icon = app.Icon
itemData.Limit = app.Limit
itemData.ShortDescEn = app.ShortDescEn
itemData.ShortDescZh = app.ShortDescZh
itemData.Recommend = app.Recommend
break
}
}
if len(itemData.Icon) == 0 {
continue
}
for _, install := range appInstalls {
if install.App.Key == showItem {
itemData.IsInstall = true
itemData.Detail = append(itemData.Detail, dto.InstallDetail{
InstallID: install.ID,
DetailID: install.AppDetailId,
Name: install.Name,
Version: install.Version,
Status: install.Status,
Path: install.GetPath(),
WebUI: install.WebUI,
HttpPort: install.HttpPort,
HttpsPort: install.HttpsPort,
})
}
}
if ArryContains(defaultList, showItem) && len(itemData.Detail) == 0 {
itemData.IsRecommend = true
recommendList = append(recommendList, itemData)
continue
}
if !ArryContains(showList, showItem) && len(itemData.Detail) != 0 {
continue
}
data = append(data, itemData)
}
sort.Slice(recommendList, func(i, j int) bool {
return recommendList[i].Recommend < recommendList[j].Recommend
})
sort.Slice(data, func(i, j int) bool {
return data[i].Name < data[j].Name
})
data = append(data, recommendList...)
return data, nil
}
func (u *DashboardService) ListLauncherOption(filter string) ([]dto.LauncherOption, error) {
showList := loadShowList()
var data []dto.LauncherOption
optionMap := make(map[string]bool)
appInstalls, err := appInstallRepo.ListBy()
if err != nil {
return data, err
}
for _, install := range appInstalls {
isShow := false
for _, item := range showList {
if install.App.Key == item {
isShow = true
break
}
}
optionMap[install.App.Key] = isShow
}
for key, val := range optionMap {
if len(filter) != 0 && !strings.Contains(key, filter) {
continue
}
data = append(data, dto.LauncherOption{Key: key, IsShow: val})
}
sort.Slice(data, func(i, j int) bool {
return data[i].Key < data[j].Key
})
return data, nil
}
type diskInfo struct {
Type string
Mount string
@ -355,3 +462,42 @@ func loadGPUInfo() []dto.GPUInfo {
}
return data
}
func loadShowList() []string {
var data []string
if global.IsMaster {
var list []AppLauncher
if err := global.CoreDB.Model(AppLauncher{}).Where("1 == 1").Find(&list).Error; err != nil {
return []string{}
}
for _, item := range list {
data = append(data, item.Key)
}
return data
}
res, err := xpack.RequestToMaster("/api/v2/core/launcher/search", http.MethodGet, nil)
if err != nil {
return data
}
item, err := json.Marshal(res)
if err != nil {
return data
}
if err := json.Unmarshal(item, &data); err != nil {
return data
}
return data
}
type AppLauncher struct {
Key string `json:"key"`
}
func ArryContains(arr []string, element string) bool {
for _, v := range arr {
if v == element {
return true
}
}
return false
}

View File

@ -12,6 +12,8 @@ func (s *DashboardRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v2.ApiGroupApp.BaseApi
{
cmdRouter.GET("/base/os", baseApi.LoadDashboardOsInfo)
cmdRouter.GET("/app/launcher", baseApi.LoadAppLauncher)
cmdRouter.POST("/app/launcher/option", baseApi.LoadAppLauncherOption)
cmdRouter.GET("/base/:ioOption/:netOption", baseApi.LoadDashboardBaseInfo)
cmdRouter.GET("/current/node", baseApi.LoadCurrentInfoForNode)
cmdRouter.GET("/current/:ioOption/:netOption", baseApi.LoadDashboardCurrentInfo)

View File

@ -229,6 +229,17 @@ func RemoveRepeatElement(a interface{}) (ret []interface{}) {
return ret
}
func RemoveRepeatStr(list []string) (ret []string) {
mapItem := make(map[string]struct{})
for _, item := range list {
mapItem[item] = struct{}{}
}
for key := range mapItem {
ret = append(ret, key)
}
return ret
}
func LoadSizeUnit(value float64) string {
val := int64(value)
if val%1024 != 0 {

View File

@ -0,0 +1,39 @@
package v2
import (
"github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) SearchAppLauncher(c *gin.Context) {
data, err := appLauncherService.Search()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags App Launcher
// @Summary Update app Launcher
// @Description 更新首页显示应用
// @Accept json
// @Param request body dto.ChangeShow true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /core/app/launcher/show [post]
// @x-panel-log {"bodyKeys":["key", "value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"首页应用 [key] => 显示:[value]","formatEN":"app launcher [key] => show: [value]"}
func (b *BaseApi) UpdateAppLauncher(c *gin.Context) {
var req dto.SettingUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := appLauncherService.ChangeShow(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -9,12 +9,13 @@ type ApiGroup struct {
var ApiGroupApp = new(ApiGroup)
var (
hostService = service.NewIHostService()
authService = service.NewIAuthService()
backupService = service.NewIBackupService()
settingService = service.NewISettingService()
logService = service.NewILogService()
upgradeService = service.NewIUpgradeService()
groupService = service.NewIGroupService()
commandService = service.NewICommandService()
hostService = service.NewIHostService()
authService = service.NewIAuthService()
backupService = service.NewIBackupService()
settingService = service.NewISettingService()
logService = service.NewILogService()
upgradeService = service.NewIUpgradeService()
groupService = service.NewIGroupService()
commandService = service.NewICommandService()
appLauncherService = service.NewIAppLauncher()
)

View File

@ -0,0 +1,6 @@
package model
type AppLauncher struct {
BaseModel
Key string `json:"key"`
}

View File

@ -0,0 +1,50 @@
package repo
import (
"github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/global"
)
type LauncherRepo struct{}
type ILauncherRepo interface {
Get(opts ...DBOption) (model.AppLauncher, error)
List(opts ...DBOption) ([]model.AppLauncher, error)
Create(launcher *model.AppLauncher) error
Delete(opts ...DBOption) error
}
func NewILauncherRepo() ILauncherRepo {
return &LauncherRepo{}
}
func (u *LauncherRepo) Get(opts ...DBOption) (model.AppLauncher, error) {
var launcher model.AppLauncher
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&launcher).Error
return launcher, err
}
func (u *LauncherRepo) List(opts ...DBOption) ([]model.AppLauncher, error) {
var ops []model.AppLauncher
db := global.DB.Model(&model.AppLauncher{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&ops).Error
return ops, err
}
func (u *LauncherRepo) Create(launcher *model.AppLauncher) error {
return global.DB.Create(launcher).Error
}
func (u *LauncherRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.AppLauncher{}).Error
}

View File

@ -15,6 +15,7 @@ type ICommonRepo interface {
WithByName(name string) DBOption
WithLikeName(name string) DBOption
WithByType(ty string) DBOption
WithByKey(key string) DBOption
WithOrderBy(orderStr string) DBOption
WithOrderRuleBy(orderBy, order string) DBOption
@ -60,6 +61,11 @@ func (c *CommonRepo) WithByType(ty string) DBOption {
return g.Where("`type` = ?", ty)
}
}
func (c *CommonRepo) WithByKey(key string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("key = ?", key)
}
}
func (c *CommonRepo) WithOrderBy(orderStr string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Order(orderStr)

View File

@ -3,7 +3,6 @@ package repo
import (
"github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/global"
"gorm.io/gorm"
)
type SettingRepo struct{}
@ -13,7 +12,6 @@ type ISettingRepo interface {
Get(opts ...DBOption) (model.Setting, error)
Create(key, value string) error
Update(key, value string) error
WithByKey(key string) DBOption
}
func NewISettingRepo() ISettingRepo {
@ -48,12 +46,6 @@ func (u *SettingRepo) Get(opts ...DBOption) (model.Setting, error) {
return settings, err
}
func (c *SettingRepo) WithByKey(key string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("key = ?", key)
}
}
func (u *SettingRepo) Update(key, value string) error {
return global.DB.Model(&model.Setting{}).Where("key = ?", key).Updates(map[string]interface{}{"value": value}).Error
}

View File

@ -0,0 +1,45 @@
package service
import (
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/constant"
)
type LauncherService struct{}
type IAppLauncher interface {
Search() ([]string, error)
ChangeShow(req dto.SettingUpdate) error
}
func NewIAppLauncher() IAppLauncher {
return &LauncherService{}
}
func (u *LauncherService) Search() ([]string, error) {
launchers, err := launcherRepo.List(commonRepo.WithOrderBy("created_at"))
if err != nil {
return nil, err
}
var data []string
for _, launcher := range launchers {
data = append(data, launcher.Key)
}
return data, nil
}
func (u *LauncherService) ChangeShow(req dto.SettingUpdate) error {
launcher, _ := launcherRepo.Get(commonRepo.WithByKey(req.Key))
if req.Value == constant.StatusEnable {
if launcher.ID != 0 {
return nil
}
launcher.Key = req.Key
return launcherRepo.Create(&launcher)
}
if launcher.ID == 0 {
return nil
}
return launcherRepo.Delete(commonRepo.WithByKey(req.Key))
}

View File

@ -32,11 +32,11 @@ func NewIAuthService() IAuthService {
}
func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo, error) {
nameSetting, err := settingRepo.Get(settingRepo.WithByKey("UserName"))
nameSetting, err := settingRepo.Get(commonRepo.WithByKey("UserName"))
if err != nil {
return nil, errors.WithMessage(constant.ErrRecordNotFound, err.Error())
}
passwordSetting, err := settingRepo.Get(settingRepo.WithByKey("Password"))
passwordSetting, err := settingRepo.Get(commonRepo.WithByKey("Password"))
if err != nil {
return nil, errors.WithMessage(constant.ErrRecordNotFound, err.Error())
}
@ -47,14 +47,14 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*d
if !hmac.Equal([]byte(info.Password), []byte(pass)) || nameSetting.Value != info.Name {
return nil, constant.ErrAuth
}
entranceSetting, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntrance"))
entranceSetting, err := settingRepo.Get(commonRepo.WithByKey("SecurityEntrance"))
if err != nil {
return nil, err
}
if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance {
return nil, buserr.New(constant.ErrEntrance)
}
mfa, err := settingRepo.Get(settingRepo.WithByKey("MFAStatus"))
mfa, err := settingRepo.Get(commonRepo.WithByKey("MFAStatus"))
if err != nil {
return nil, err
}
@ -68,11 +68,11 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*d
}
func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance string) (*dto.UserLoginInfo, error) {
nameSetting, err := settingRepo.Get(settingRepo.WithByKey("UserName"))
nameSetting, err := settingRepo.Get(commonRepo.WithByKey("UserName"))
if err != nil {
return nil, errors.WithMessage(constant.ErrRecordNotFound, err.Error())
}
passwordSetting, err := settingRepo.Get(settingRepo.WithByKey("Password"))
passwordSetting, err := settingRepo.Get(commonRepo.WithByKey("Password"))
if err != nil {
return nil, errors.WithMessage(constant.ErrRecordNotFound, err.Error())
}
@ -83,18 +83,18 @@ func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance strin
if !hmac.Equal([]byte(info.Password), []byte(pass)) || nameSetting.Value != info.Name {
return nil, constant.ErrAuth
}
entranceSetting, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntrance"))
entranceSetting, err := settingRepo.Get(commonRepo.WithByKey("SecurityEntrance"))
if err != nil {
return nil, err
}
if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance {
return nil, buserr.New(constant.ErrEntrance)
}
mfaSecret, err := settingRepo.Get(settingRepo.WithByKey("MFASecret"))
mfaSecret, err := settingRepo.Get(commonRepo.WithByKey("MFASecret"))
if err != nil {
return nil, err
}
mfaInterval, err := settingRepo.Get(settingRepo.WithByKey("MFAInterval"))
mfaInterval, err := settingRepo.Get(commonRepo.WithByKey("MFAInterval"))
if err != nil {
return nil, err
}
@ -107,11 +107,11 @@ func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance strin
}
func (u *AuthService) generateSession(c *gin.Context, name, authMethod string) (*dto.UserLoginInfo, error) {
setting, err := settingRepo.Get(settingRepo.WithByKey("SessionTimeout"))
setting, err := settingRepo.Get(commonRepo.WithByKey("SessionTimeout"))
if err != nil {
return nil, err
}
httpsSetting, err := settingRepo.Get(settingRepo.WithByKey("SSL"))
httpsSetting, err := settingRepo.Get(commonRepo.WithByKey("SSL"))
if err != nil {
return nil, err
}
@ -151,7 +151,7 @@ func (u *AuthService) generateSession(c *gin.Context, name, authMethod string) (
}
func (u *AuthService) LogOut(c *gin.Context) error {
httpsSetting, err := settingRepo.Get(settingRepo.WithByKey("SSL"))
httpsSetting, err := settingRepo.Get(commonRepo.WithByKey("SSL"))
if err != nil {
return err
}
@ -167,7 +167,7 @@ func (u *AuthService) LogOut(c *gin.Context) error {
}
func (u *AuthService) VerifyCode(code string) (bool, error) {
setting, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntrance"))
setting, err := settingRepo.Get(commonRepo.WithByKey("SecurityEntrance"))
if err != nil {
return false, err
}
@ -175,7 +175,7 @@ func (u *AuthService) VerifyCode(code string) (bool, error) {
}
func (u *AuthService) CheckIsSafety(code string) (string, error) {
status, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntrance"))
status, err := settingRepo.Get(commonRepo.WithByKey("SecurityEntrance"))
if err != nil {
return "", err
}
@ -189,7 +189,7 @@ func (u *AuthService) CheckIsSafety(code string) (string, error) {
}
func (u *AuthService) GetResponsePage() (string, error) {
pageCode, err := settingRepo.Get(settingRepo.WithByKey("NoAuthSetting"))
pageCode, err := settingRepo.Get(commonRepo.WithByKey("NoAuthSetting"))
if err != nil {
return "", err
}

View File

@ -173,7 +173,7 @@ func (u *BackupService) SearchWithPage(req dto.SearchPageWithType) (int64, inter
func (u *BackupService) LoadOneDriveInfo() (dto.OneDriveInfo, error) {
var data dto.OneDriveInfo
data.RedirectUri = constant.OneDriveRedirectURI
clientID, err := settingRepo.Get(settingRepo.WithByKey("OneDriveID"))
clientID, err := settingRepo.Get(commonRepo.WithByKey("OneDriveID"))
if err != nil {
return data, err
}
@ -182,7 +182,7 @@ func (u *BackupService) LoadOneDriveInfo() (dto.OneDriveInfo, error) {
return data, err
}
data.ClientID = string(idItem)
clientSecret, err := settingRepo.Get(settingRepo.WithByKey("OneDriveSc"))
clientSecret, err := settingRepo.Get(commonRepo.WithByKey("OneDriveSc"))
if err != nil {
return data, err
}

View File

@ -3,11 +3,12 @@ package service
import "github.com/1Panel-dev/1Panel/core/app/repo"
var (
hostRepo = repo.NewIHostRepo()
commandRepo = repo.NewICommandRepo()
commonRepo = repo.NewICommonRepo()
settingRepo = repo.NewISettingRepo()
backupRepo = repo.NewIBackupRepo()
logRepo = repo.NewILogRepo()
groupRepo = repo.NewIGroupRepo()
hostRepo = repo.NewIHostRepo()
commandRepo = repo.NewICommandRepo()
commonRepo = repo.NewICommonRepo()
settingRepo = repo.NewISettingRepo()
backupRepo = repo.NewIBackupRepo()
logRepo = repo.NewILogRepo()
groupRepo = repo.NewIGroupRepo()
launcherRepo = repo.NewILauncherRepo()
)

View File

@ -74,7 +74,7 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
func (u *SettingService) Update(key, value string) error {
switch key {
case "AppStoreLastModified":
exist, _ := settingRepo.Get(settingRepo.WithByKey("AppStoreLastModified"))
exist, _ := settingRepo.Get(commonRepo.WithByKey("AppStoreLastModified"))
if exist.ID == 0 {
_ = settingRepo.Create("AppStoreLastModified", value)
return nil
@ -270,14 +270,14 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
}
func (u *SettingService) LoadFromCert() (*dto.SSLInfo, error) {
ssl, err := settingRepo.Get(settingRepo.WithByKey("SSL"))
ssl, err := settingRepo.Get(commonRepo.WithByKey("SSL"))
if err != nil {
return nil, err
}
if ssl.Value == "disable" {
return &dto.SSLInfo{}, nil
}
sslType, err := settingRepo.Get(settingRepo.WithByKey("SSLType"))
sslType, err := settingRepo.Get(commonRepo.WithByKey("SSLType"))
if err != nil {
return nil, err
}
@ -311,7 +311,7 @@ func (u *SettingService) LoadFromCert() (*dto.SSLInfo, error) {
}
func (u *SettingService) HandlePasswordExpired(c *gin.Context, old, new string) error {
setting, err := settingRepo.Get(settingRepo.WithByKey("Password"))
setting, err := settingRepo.Get(commonRepo.WithByKey("Password"))
if err != nil {
return err
}
@ -328,7 +328,7 @@ func (u *SettingService) HandlePasswordExpired(c *gin.Context, old, new string)
return err
}
expiredSetting, err := settingRepo.Get(settingRepo.WithByKey("ExpirationDays"))
expiredSetting, err := settingRepo.Get(commonRepo.WithByKey("ExpirationDays"))
if err != nil {
return err
}

View File

@ -33,11 +33,11 @@ func NewIUpgradeService() IUpgradeService {
func (u *UpgradeService) SearchUpgrade() (*dto.UpgradeInfo, error) {
var upgrade dto.UpgradeInfo
currentVersion, err := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
currentVersion, err := settingRepo.Get(commonRepo.WithByKey("SystemVersion"))
if err != nil {
return nil, err
}
DeveloperMode, err := settingRepo.Get(settingRepo.WithByKey("DeveloperMode"))
DeveloperMode, err := settingRepo.Get(commonRepo.WithByKey("DeveloperMode"))
if err != nil {
return nil, err
}

View File

@ -14,33 +14,34 @@ import (
func Init() {
settingRepo := repo.NewISettingRepo()
masterSetting, err := settingRepo.Get(settingRepo.WithByKey("MasterAddr"))
commonRepo := repo.NewICommonRepo()
masterSetting, err := settingRepo.Get(commonRepo.WithByKey("MasterAddr"))
if err != nil {
global.LOG.Errorf("load master addr from setting failed, err: %v", err)
}
global.CONF.System.MasterAddr = masterSetting.Value
portSetting, err := settingRepo.Get(settingRepo.WithByKey("ServerPort"))
portSetting, err := settingRepo.Get(commonRepo.WithByKey("ServerPort"))
if err != nil {
global.LOG.Errorf("load service port from setting failed, err: %v", err)
}
global.CONF.System.Port = portSetting.Value
ipv6Setting, err := settingRepo.Get(settingRepo.WithByKey("Ipv6"))
ipv6Setting, err := settingRepo.Get(commonRepo.WithByKey("Ipv6"))
if err != nil {
global.LOG.Errorf("load ipv6 status from setting failed, err: %v", err)
}
global.CONF.System.Ipv6 = ipv6Setting.Value
bindAddressSetting, err := settingRepo.Get(settingRepo.WithByKey("BindAddress"))
bindAddressSetting, err := settingRepo.Get(commonRepo.WithByKey("BindAddress"))
if err != nil {
global.LOG.Errorf("load bind address from setting failed, err: %v", err)
}
global.CONF.System.BindAddress = bindAddressSetting.Value
sslSetting, err := settingRepo.Get(settingRepo.WithByKey("SSL"))
sslSetting, err := settingRepo.Get(commonRepo.WithByKey("SSL"))
if err != nil {
global.LOG.Errorf("load service ssl from setting failed, err: %v", err)
}
global.CONF.System.SSL = sslSetting.Value
if _, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus")); err != nil {
if _, err := settingRepo.Get(commonRepo.WithByKey("SystemStatus")); err != nil {
_ = settingRepo.Create("SystemStatus", "Free")
}
if err := settingRepo.Update("SystemStatus", "Free"); err != nil {

View File

@ -15,6 +15,7 @@ func Init() {
migrations.InitMasterAddr,
migrations.InitHost,
migrations.InitTerminalSetting,
migrations.InitAppLauncher,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -233,3 +233,10 @@ var InitTerminalSetting = &gormigrate.Migration{
return nil
},
}
var InitAppLauncher = &gormigrate.Migration{
ID: "20241029-init-app-launcher",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.AppLauncher{})
},
}

View File

@ -13,7 +13,8 @@ import (
func BindDomain() gin.HandlerFunc {
return func(c *gin.Context) {
settingRepo := repo.NewISettingRepo()
status, err := settingRepo.Get(settingRepo.WithByKey("BindDomain"))
commonRepo := repo.NewICommonRepo()
status, err := settingRepo.Get(commonRepo.WithByKey("BindDomain"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return

View File

@ -8,7 +8,8 @@ import (
func LoadErrCode(errInfo string) int {
settingRepo := repo.NewISettingRepo()
codeVal, err := settingRepo.Get(settingRepo.WithByKey("NoAuthSetting"))
commonRepo := repo.NewICommonRepo()
codeVal, err := settingRepo.Get(commonRepo.WithByKey("NoAuthSetting"))
if err != nil {
return 500
}

View File

@ -15,7 +15,8 @@ import (
func WhiteAllow() gin.HandlerFunc {
return func(c *gin.Context) {
settingRepo := repo.NewISettingRepo()
status, err := settingRepo.Get(settingRepo.WithByKey("AllowIPs"))
commonRepo := repo.NewICommonRepo()
status, err := settingRepo.Get(commonRepo.WithByKey("AllowIPs"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return

View File

@ -10,7 +10,8 @@ import (
func GlobalLoading() gin.HandlerFunc {
return func(c *gin.Context) {
settingRepo := repo.NewISettingRepo()
status, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus"))
commonRepo := repo.NewICommonRepo()
status, err := settingRepo.Get(commonRepo.WithByKey("SystemStatus"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return

View File

@ -21,7 +21,8 @@ func PasswordExpired() gin.HandlerFunc {
return
}
settingRepo := repo.NewISettingRepo()
setting, err := settingRepo.Get(settingRepo.WithByKey("ExpirationDays"))
commonRepo := repo.NewICommonRepo()
setting, err := settingRepo.Get(commonRepo.WithByKey("ExpirationDays"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypePasswordExpired, err)
return
@ -32,7 +33,7 @@ func PasswordExpired() gin.HandlerFunc {
return
}
extime, err := settingRepo.Get(settingRepo.WithByKey("ExpirationTime"))
extime, err := settingRepo.Get(commonRepo.WithByKey("ExpirationTime"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypePasswordExpired, err)
return

View File

@ -32,7 +32,8 @@ func SessionAuth() gin.HandlerFunc {
return
}
settingRepo := repo.NewISettingRepo()
setting, err := settingRepo.Get(settingRepo.WithByKey("SessionTimeout"))
commonRepo := repo.NewICommonRepo()
setting, err := settingRepo.Get(commonRepo.WithByKey("SessionTimeout"))
if err != nil {
global.LOG.Errorf("create operation record failed, err: %v", err)
}

View File

@ -0,0 +1,17 @@
package router
import (
v2 "github.com/1Panel-dev/1Panel/core/app/api/v2"
"github.com/gin-gonic/gin"
)
type AppLauncherRouter struct{}
func (s *AppLauncherRouter) InitRouter(Router *gin.RouterGroup) {
launcherRouter := Router.Group("launcher")
baseApi := v2.ApiGroupApp.BaseApi
{
launcherRouter.GET("/search", baseApi.SearchAppLauncher)
launcherRouter.POST("/change/show", baseApi.UpdateAppLauncher)
}
}

View File

@ -9,5 +9,6 @@ func commonGroups() []CommonRouter {
&CommandRouter{},
&HostRouter{},
&GroupRouter{},
&AppLauncherRouter{},
}
}

View File

@ -27,7 +27,8 @@ type BaseClaims struct {
func NewJWT() *JWT {
settingRepo := repo.NewISettingRepo()
jwtSign, _ := settingRepo.Get(settingRepo.WithByKey("JWTSigningKey"))
commonRepo := repo.NewICommonRepo()
jwtSign, _ := settingRepo.Get(commonRepo.WithByKey("JWTSigningKey"))
return &JWT{
[]byte(jwtSign.Value),
}

View File

@ -8,6 +8,34 @@ export namespace Dashboard {
diskSize: number;
}
export interface AppLauncher {
key: string;
icon: string;
limit: number;
shortDescEn: string;
shortDescZh: string;
currentRow: InstallDetail;
isInstall: boolean;
isRecommend: boolean;
detail: Array<InstallDetail>;
}
export interface AppLauncherOption {
key: string;
isShow: boolean;
}
export interface InstallDetail {
installID: number;
detailID: string;
name: string;
version: string;
path: string;
status: string;
appType: string;
webUI: string;
httpPort: string;
httpsPort: string;
}
export interface BaseInfo {
websiteNumber: number;
databaseNumber: number;

View File

@ -5,6 +5,19 @@ export const loadOsInfo = () => {
return http.get<Dashboard.OsInfo>(`/dashboard/base/os`);
};
export const loadAppLauncher = () => {
return http.get<Array<Dashboard.AppLauncher>>(`/dashboard/app/launcher`);
};
export const loadAppLauncherOption = (filter: string) => {
return http.post<Array<Dashboard.AppLauncherOption>>(`/dashboard/app/launcher/option`, { filter: filter });
};
export const changeLauncherStatus = (key: string, val: string) => {
return http.post(`/core/launcher/change/show`, { key: key, value: val });
};
export const resetLauncherStatus = () => {
return http.post(`/core/launcher/reset`);
};
export const loadBaseInfo = (ioOption: string, netOption: string) => {
return http.get<Dashboard.BaseInfo>(`/dashboard/base/${ioOption}/${netOption}`);
};

View File

@ -336,6 +336,8 @@ const message = {
name: 'Tamper Proof',
},
home: {
recommend: 'recommend',
dir: 'dir',
restart_1panel: 'Restart Panel',
restart_system: 'Restart Server',
operationSuccess: 'Operation succeeded, rebooting, please refresh the browser manually later!',

View File

@ -330,6 +330,8 @@ const message = {
tamper: '防篡改',
},
home: {
recommend: '推薦',
dir: '目錄',
restart_1panel: '重啟面板',
restart_system: '重啟服務器',
operationSuccess: '操作成功正在重啟請稍後手動刷新瀏覽器',

View File

@ -330,6 +330,8 @@ const message = {
tamper: '防篡改',
},
home: {
recommend: '推荐',
dir: '目录',
restart_1panel: '重启面板',
restart_system: '重启服务器',
operationSuccess: '操作成功正在重启请稍后手动刷新浏览器',

View File

@ -1,67 +1,212 @@
<template>
<div>
<el-scrollbar height="525px" class="moz-height">
<div class="h-app-card" v-for="(app, index) in apps" :key="index">
<el-row :gutter="10">
<el-col :span="5">
<div>
<el-avatar shape="square" :size="55" :src="'data:image/png;base64,' + app.icon" />
</div>
</el-col>
<el-col :span="15">
<div class="h-app-content">
<div>
<span class="h-app-title">{{ app.name }}</span>
</div>
<div class="h-app-desc">
<span>
{{ language == 'zh' || language == 'tw' ? app.shortDescZh : app.shortDescEn }}
</span>
</div>
</div>
</el-col>
<el-col :span="2">
<el-button
class="h-app-button"
type="primary"
plain
round
size="small"
:disabled="app.limit == 1 && app.installed"
@click="goInstall(app.key, app.type)"
>
{{ $t('app.install') }}
</el-button>
</el-col>
</el-row>
<div class="h-app-divider" />
</div>
</el-scrollbar>
<CardWithHeader :header="$t('home.app')" class="mt-5" :loading="loading">
<template #header-r>
<el-popover placement="left" :width="226" trigger="click">
<el-input size="small" v-model="filter" clearable @input="loadOption()" />
<el-table :show-header="false" :data="options" stripe max-height="500px">
<el-table-column prop="key" width="120" show-overflow-tooltip />
<el-table-column prop="name">
<template #default="{ row }">
<el-switch
@change="onChangeStatus(row)"
class="float-right"
size="small"
v-model="row.isShow"
/>
</template>
</el-table-column>
</el-table>
<template #reference>
<el-button class="h-button-setting" link icon="Setting"></el-button>
</template>
</el-popover>
</template>
<template #body>
<el-scrollbar height="525px" class="moz-height">
<div class="h-app-card" v-for="(app, index) in apps" :key="index">
<el-row :gutter="5">
<el-col :span="5">
<div>
<el-badge
badge-style="background-color: transparent; color: #646a73; font-size: 12px; border: none"
:value="$t('home.recommend')"
v-if="app.isRecommend"
:offset="[-60, 0]"
>
<el-avatar
shape="square"
:size="55"
:src="'data:image/png;base64,' + app.icon"
/>
</el-badge>
<el-avatar
v-else
shape="square"
:size="55"
:src="'data:image/png;base64,' + app.icon"
/>
</div>
</el-col>
<el-col :span="16">
<div class="h-app-content" v-if="!app.currentRow">
<div>
<span class="h-app-title">{{ app.name }}</span>
</div>
<div class="h-app-desc">
<span>
{{
language == 'zh' || language == 'tw' ? app.shortDescZh : app.shortDescEn
}}
</span>
</div>
</div>
<div class="h-app-content" v-else>
<div>
<el-dropdown trigger="hover">
<el-button plain size="small" link class="h-app-dropdown">
{{ app.currentRow.name }}
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu
v-for="(detailItem, index2) in app.detail"
:key="index2"
>
<el-dropdown-item @click="app.currentRow = detailItem">
{{ detailItem.name + ' - ' + detailItem.version }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="h-app-margin">
<el-button plain size="small" link class="h-app-desc">
{{ $t('app.version') + ': ' + app.currentRow.version }}
</el-button>
</div>
<div class="h-app-margin">
<el-button
size="small"
type="primary"
link
@click="onOperate('stop', app.currentRow)"
>
{{ $t('commons.operate.up') }}
</el-button>
<el-button
:style="mobile ? 'margin-left: -1px' : ''"
size="small"
type="primary"
link
@click="onOperate('stop', app.currentRow)"
>
{{ $t('commons.operate.down') }}
</el-button>
<el-button
:style="mobile ? 'margin-left: -1px' : ''"
size="small"
type="primary"
link
@click="toFolder(app.currentRow.path)"
>
{{ $t('home.dir') }}
</el-button>
<el-popover
placement="left"
trigger="hover"
v-if="app.currentRow.appType == 'website'"
:width="260"
>
<template #reference>
<el-button
link
size="small"
type="primary"
:style="mobile ? 'margin-left: -1px' : ''"
>
{{ $t('app.toLink') }}
</el-button>
</template>
<span v-if="defaultLink == '' && app.currentRow.webUI == ''">
{{ $t('app.webUIConfig') }}
</span>
<div v-else>
<div>
<el-button
v-if="defaultLink != ''"
type="primary"
link
@click="toLink(defaultLink + ':' + app.currentRow.httpPort)"
>
{{ defaultLink + ':' + app.currentRow.httpPort }}
</el-button>
</div>
<div>
<el-button
v-if="app.currentRow.webUI != ''"
type="primary"
link
@click="toLink(app.currentRow.webUI)"
>
{{ app.currentRow.webUI }}
</el-button>
</div>
</div>
</el-popover>
</div>
</div>
</el-col>
<el-col :span="1">
<el-button
class="h-app-button"
type="primary"
plain
round
size="small"
:disabled="app.limit == 1 && app.detail && app.detail.length !== 0"
@click="goInstall(app.key, app.appType)"
>
{{ $t('app.install') }}
</el-button>
</el-col>
</el-row>
<div class="h-app-divider" />
</div>
</el-scrollbar>
</template>
</CardWithHeader>
</div>
</template>
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { SearchApp } from '@/api/modules/app';
import { GetAppStoreConfig, InstalledOp } from '@/api/modules/app';
import { changeLauncherStatus, loadAppLauncher, loadAppLauncherOption } from '@/api/modules/dashboard';
import i18n from '@/lang';
import { GlobalStore } from '@/store';
import { MsgSuccess } from '@/utils/message';
import { getLanguage } from '@/utils/util';
import { reactive, ref } from 'vue';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { toFolder } from '@/global/business';
const router = useRouter();
const language = getLanguage();
let req = reactive({
name: '',
tags: [],
page: 1,
pageSize: 50,
recommend: true,
});
const globalStore = GlobalStore();
let loading = ref(false);
let apps = ref<App.AppDTO[]>([]);
let apps = ref([]);
const options = ref([]);
const filter = ref();
const mobile = computed(() => {
return globalStore.isMobile();
});
const defaultLink = ref('');
const acceptParams = (): void => {
search(req);
search();
loadOption();
getAppStoreConfig();
};
const goInstall = (key: string, type: string) => {
@ -77,17 +222,82 @@ const goInstall = (key: string, type: string) => {
}
};
const search = async (req: App.AppReq) => {
const search = async () => {
loading.value = true;
await SearchApp(req)
await loadAppLauncher()
.then((res) => {
apps.value = res.data.items;
loading.value = false;
apps.value = res.data;
for (const item of apps.value) {
if (item.detail && item.detail.length !== 0) {
item.currentRow = item.detail[0];
}
}
})
.finally(() => {
loading.value = false;
});
};
const onChangeStatus = async (row: any) => {
loading.value = true;
await changeLauncherStatus(row.key, row.isShow ? 'Enable' : 'Disable')
.then(() => {
loading.value = false;
search();
})
.catch(() => {
loading.value = false;
});
};
const toLink = (link: string) => {
window.open(link, '_blank');
};
const getAppStoreConfig = async () => {
try {
const res = await GetAppStoreConfig();
if (res.data.defaultDomain != '') {
defaultLink.value = res.data.defaultDomain;
}
} catch (error) {}
};
const onOperate = async (operation: string, row: any) => {
ElMessageBox.confirm(
i18n.global.t('app.operatorHelper', [i18n.global.t('app.' + operation)]),
i18n.global.t('app.' + operation),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
loading.value = true;
let params = {
installId: row.installId,
operate: operation,
detailId: row.detailId,
};
await InstalledOp(params)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
});
};
const loadOption = async () => {
const res = await loadAppLauncherOption(filter.value || '');
options.value = res.data || [];
console.log(options.value);
};
defineExpose({
acceptParams,
});
@ -125,11 +335,39 @@ defineExpose({
}
.h-app-divider {
margin-top: 13px;
margin-top: 10px;
border: 0;
border-top: var(--panel-border);
}
.h-app-desc {
font-weight: 400;
font-size: 12px;
color: var(--el-text-color-regular);
}
.h-button-setting {
float: right;
margin-left: 5px;
}
.h-app-dropdown {
font-weight: 600;
font-size: 16px;
color: #1f2329;
}
.h-app-margin {
margin-top: 2px;
}
.h-app-option {
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: var(--el-text-color-regular);
}
/* FOR MOZILLA */
@-moz-document url-prefix() {
.moz-height {

View File

@ -230,11 +230,7 @@
</template>
</CardWithHeader>
<CardWithHeader :header="$t('home.app')" style="margin-top: 20px">
<template #body>
<App ref="appRef" />
</template>
</CardWithHeader>
<AppLauncher ref="appRef" style="margin-top: 20px" />
</el-col>
</el-row>
@ -245,7 +241,7 @@
<script lang="ts" setup>
import { onMounted, onBeforeUnmount, ref, reactive } from 'vue';
import Status from '@/views/home/status/index.vue';
import App from '@/views/home/app/index.vue';
import AppLauncher from '@/views/home/app/index.vue';
import VCharts from '@/components/v-charts/index.vue';
import LicenseImport from '@/components/license-import/index.vue';
import CardWithHeader from '@/components/card-with-header/index.vue';