mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 14:08:06 +08:00
feat: 统一部分日志页面 (#3035)
This commit is contained in:
parent
2c612a5919
commit
038879819d
@ -685,25 +685,21 @@ func (b *BaseApi) Keys(c *gin.Context) {
|
|||||||
|
|
||||||
// @Tags File
|
// @Tags File
|
||||||
// @Summary Read file by Line
|
// @Summary Read file by Line
|
||||||
// @Description 按行读取文件
|
// @Description 按行读取日志文件
|
||||||
// @Param request body request.FileReadByLineReq true "request"
|
// @Param request body request.FileReadByLineReq true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Router /files/read [post]
|
// @Router /files/log/read [post]
|
||||||
func (b *BaseApi) ReadFileByLine(c *gin.Context) {
|
func (b *BaseApi) ReadFileByLine(c *gin.Context) {
|
||||||
var req request.FileReadByLineReq
|
var req request.FileReadByLineReq
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lines, end, err := files.ReadFileByLine(req.Path, req.Page, req.PageSize)
|
res, err := fileService.ReadLogByLine(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
res := response.FileLineContent{
|
|
||||||
End: end,
|
|
||||||
}
|
|
||||||
res.Path = req.Path
|
|
||||||
res.Content = strings.Join(lines, "\n")
|
|
||||||
helper.SuccessWithData(c, res)
|
helper.SuccessWithData(c, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,9 +118,11 @@ type FileRoleUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FileReadByLineReq struct {
|
type FileReadByLineReq struct {
|
||||||
Path string `json:"path" validate:"required"`
|
|
||||||
Page int `json:"page" validate:"required"`
|
Page int `json:"page" validate:"required"`
|
||||||
PageSize int `json:"pageSize" validate:"required"`
|
PageSize int `json:"pageSize" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required"`
|
||||||
|
ID uint `json:"ID"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileExistReq struct {
|
type FileExistReq struct {
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type WebsiteSSL struct {
|
type WebsiteSSL struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
@ -28,3 +33,7 @@ type WebsiteSSL struct {
|
|||||||
func (w WebsiteSSL) TableName() string {
|
func (w WebsiteSSL) TableName() string {
|
||||||
return "website_ssls"
|
return "website_ssls"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w WebsiteSSL) GetLogPath() string {
|
||||||
|
return path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", w.PrimaryDomain, w.ID))
|
||||||
|
}
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -699,18 +698,15 @@ func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error
|
|||||||
|
|
||||||
func (u *ContainerService) LoadContainerLogs(req dto.OperationWithNameAndType) string {
|
func (u *ContainerService) LoadContainerLogs(req dto.OperationWithNameAndType) string {
|
||||||
filePath := ""
|
filePath := ""
|
||||||
switch req.Type {
|
if req.Type == "compose-detail" {
|
||||||
case "image-pull", "image-push", "image-build", "compose-create":
|
cli, err := docker.NewDockerClient()
|
||||||
filePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name))
|
|
||||||
case "compose-detail":
|
|
||||||
client, err := docker.NewDockerClient()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
options := types.ContainerListOptions{All: true}
|
options := types.ContainerListOptions{All: true}
|
||||||
options.Filters = filters.NewArgs()
|
options.Filters = filters.NewArgs()
|
||||||
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.Name))
|
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.Name))
|
||||||
containers, err := client.ContainerList(context.Background(), options)
|
containers, err := cli.ContainerList(context.Background(), options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -725,9 +721,6 @@ func (u *ContainerService) LoadContainerLogs(req dto.OperationWithNameAndType) s
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.Type == "compose-create" {
|
|
||||||
filePath = path.Join(path.Dir(filePath), "compose.log")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(filePath); err != nil {
|
if _, err := os.Stat(filePath); err != nil {
|
||||||
return ""
|
return ""
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -41,6 +42,7 @@ type IFileService interface {
|
|||||||
ChangeOwner(req request.FileRoleUpdate) error
|
ChangeOwner(req request.FileRoleUpdate) error
|
||||||
ChangeMode(op request.FileCreate) error
|
ChangeMode(op request.FileCreate) error
|
||||||
BatchChangeModeAndOwner(op request.FileRoleReq) error
|
BatchChangeModeAndOwner(op request.FileRoleReq) error
|
||||||
|
ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIFileService() IFileService {
|
func NewIFileService() IFileService {
|
||||||
@ -297,3 +299,62 @@ func (f *FileService) DirSize(req request.DirSizeReq) (response.DirSizeRes, erro
|
|||||||
}
|
}
|
||||||
return response.DirSizeRes{Size: size}, nil
|
return response.DirSizeRes{Size: size}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) {
|
||||||
|
logFilePath := ""
|
||||||
|
switch req.Type {
|
||||||
|
case constant.TypeWebsite:
|
||||||
|
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nginx, err := getNginxFull(&website)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sitePath := path.Join(nginx.SiteDir, "sites", website.Alias)
|
||||||
|
logFilePath = path.Join(sitePath, "log", req.Name)
|
||||||
|
case constant.TypePhp:
|
||||||
|
php, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logFilePath = php.GetLogPath()
|
||||||
|
case constant.TypeSSL:
|
||||||
|
ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logFilePath = ssl.GetLogPath()
|
||||||
|
case constant.TypeSystem:
|
||||||
|
fileName := ""
|
||||||
|
if req.Name == time.Now().Format("2006-01-02") {
|
||||||
|
fileName = "1Panel.log"
|
||||||
|
} else {
|
||||||
|
fileName = "1Panel-" + req.Name + ".log"
|
||||||
|
}
|
||||||
|
logFilePath = path.Join(global.CONF.System.DataDir, "log", fileName)
|
||||||
|
if _, err := os.Stat(logFilePath); err != nil {
|
||||||
|
fileGzPath := path.Join(global.CONF.System.DataDir, "log", fileName+".gz")
|
||||||
|
if _, err := os.Stat(fileGzPath); err != nil {
|
||||||
|
return nil, buserr.New("ErrHttpReqNotFound")
|
||||||
|
}
|
||||||
|
if err := handleGunzip(fileGzPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("handle ungzip file %s failed, err: %v", fileGzPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "image-pull", "image-push", "image-build", "compose-create":
|
||||||
|
logFilePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
lines, isEndOfFile, err := files.ReadFileByLine(logFilePath, req.Page, req.PageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &response.FileLineContent{
|
||||||
|
Content: strings.Join(lines, "\n"),
|
||||||
|
End: isEndOfFile,
|
||||||
|
Path: logFilePath,
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
@ -51,7 +51,7 @@ func (u *LogService) ListSystemLogFile() ([]string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !info.IsDir() {
|
if !info.IsDir() && strings.HasPrefix(info.Name(), "1Panel-") {
|
||||||
if info.Name() == "1Panel.log" {
|
if info.Name() == "1Panel.log" {
|
||||||
files = append(files, time.Now().Format("2006-01-02"))
|
files = append(files, time.Now().Format("2006-01-02"))
|
||||||
return nil
|
return nil
|
||||||
|
@ -6,4 +6,9 @@ const (
|
|||||||
DB DBContext = "db"
|
DB DBContext = "db"
|
||||||
|
|
||||||
SystemRestart = "systemRestart"
|
SystemRestart = "systemRestart"
|
||||||
|
|
||||||
|
TypeWebsite = "website"
|
||||||
|
TypePhp = "php"
|
||||||
|
TypeSSL = "ssl"
|
||||||
|
TypeSystem = "system"
|
||||||
)
|
)
|
||||||
|
@ -78,6 +78,9 @@ func IsHidden(path string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReadFileByLine(filename string, page, pageSize int) ([]string, bool, error) {
|
func ReadFileByLine(filename string, page, pageSize int) ([]string, bool, error) {
|
||||||
|
if !NewFileOp().Stat(filename) {
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
|
@ -95,47 +95,47 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string) error {
|
|||||||
case DnsPod:
|
case DnsPod:
|
||||||
dnsPodConfig := dnspod.NewDefaultConfig()
|
dnsPodConfig := dnspod.NewDefaultConfig()
|
||||||
dnsPodConfig.LoginToken = param.ID + "," + param.Token
|
dnsPodConfig.LoginToken = param.ID + "," + param.Token
|
||||||
dnsPodConfig.PropagationTimeout = 30 * time.Minute
|
dnsPodConfig.PropagationTimeout = 60 * time.Minute
|
||||||
dnsPodConfig.PollingInterval = 30 * time.Second
|
dnsPodConfig.PollingInterval = 5 * time.Second
|
||||||
dnsPodConfig.TTL = 3600
|
dnsPodConfig.TTL = 3600
|
||||||
p, err = dnspod.NewDNSProviderConfig(dnsPodConfig)
|
p, err = dnspod.NewDNSProviderConfig(dnsPodConfig)
|
||||||
case AliYun:
|
case AliYun:
|
||||||
alidnsConfig := alidns.NewDefaultConfig()
|
alidnsConfig := alidns.NewDefaultConfig()
|
||||||
alidnsConfig.SecretKey = param.SecretKey
|
alidnsConfig.SecretKey = param.SecretKey
|
||||||
alidnsConfig.APIKey = param.AccessKey
|
alidnsConfig.APIKey = param.AccessKey
|
||||||
alidnsConfig.PropagationTimeout = 30 * time.Minute
|
alidnsConfig.PropagationTimeout = 60 * time.Minute
|
||||||
alidnsConfig.PollingInterval = 30 * time.Second
|
alidnsConfig.PollingInterval = 5 * time.Second
|
||||||
alidnsConfig.TTL = 3600
|
alidnsConfig.TTL = 3600
|
||||||
p, err = alidns.NewDNSProviderConfig(alidnsConfig)
|
p, err = alidns.NewDNSProviderConfig(alidnsConfig)
|
||||||
case CloudFlare:
|
case CloudFlare:
|
||||||
cloudflareConfig := cloudflare.NewDefaultConfig()
|
cloudflareConfig := cloudflare.NewDefaultConfig()
|
||||||
cloudflareConfig.AuthEmail = param.Email
|
cloudflareConfig.AuthEmail = param.Email
|
||||||
cloudflareConfig.AuthKey = param.APIkey
|
cloudflareConfig.AuthKey = param.APIkey
|
||||||
cloudflareConfig.PropagationTimeout = 30 * time.Minute
|
cloudflareConfig.PropagationTimeout = 60 * time.Minute
|
||||||
cloudflareConfig.PollingInterval = 30 * time.Second
|
cloudflareConfig.PollingInterval = 5 * time.Second
|
||||||
cloudflareConfig.TTL = 3600
|
cloudflareConfig.TTL = 3600
|
||||||
p, err = cloudflare.NewDNSProviderConfig(cloudflareConfig)
|
p, err = cloudflare.NewDNSProviderConfig(cloudflareConfig)
|
||||||
case NameCheap:
|
case NameCheap:
|
||||||
namecheapConfig := namecheap.NewDefaultConfig()
|
namecheapConfig := namecheap.NewDefaultConfig()
|
||||||
namecheapConfig.APIKey = param.APIkey
|
namecheapConfig.APIKey = param.APIkey
|
||||||
namecheapConfig.APIUser = param.APIUser
|
namecheapConfig.APIUser = param.APIUser
|
||||||
namecheapConfig.PropagationTimeout = 30 * time.Minute
|
namecheapConfig.PropagationTimeout = 60 * time.Minute
|
||||||
namecheapConfig.PollingInterval = 30 * time.Second
|
namecheapConfig.PollingInterval = 5 * time.Second
|
||||||
namecheapConfig.TTL = 3600
|
namecheapConfig.TTL = 3600
|
||||||
p, err = namecheap.NewDNSProviderConfig(namecheapConfig)
|
p, err = namecheap.NewDNSProviderConfig(namecheapConfig)
|
||||||
case NameSilo:
|
case NameSilo:
|
||||||
nameSiloConfig := namesilo.NewDefaultConfig()
|
nameSiloConfig := namesilo.NewDefaultConfig()
|
||||||
nameSiloConfig.APIKey = param.APIkey
|
nameSiloConfig.APIKey = param.APIkey
|
||||||
nameSiloConfig.PropagationTimeout = 30 * time.Minute
|
nameSiloConfig.PropagationTimeout = 60 * time.Minute
|
||||||
nameSiloConfig.PollingInterval = 30 * time.Second
|
nameSiloConfig.PollingInterval = 5 * time.Second
|
||||||
nameSiloConfig.TTL = 3600
|
nameSiloConfig.TTL = 3600
|
||||||
p, err = namesilo.NewDNSProviderConfig(nameSiloConfig)
|
p, err = namesilo.NewDNSProviderConfig(nameSiloConfig)
|
||||||
case Godaddy:
|
case Godaddy:
|
||||||
godaddyConfig := godaddy.NewDefaultConfig()
|
godaddyConfig := godaddy.NewDefaultConfig()
|
||||||
godaddyConfig.APIKey = param.APIkey
|
godaddyConfig.APIKey = param.APIkey
|
||||||
godaddyConfig.APISecret = param.APISecret
|
godaddyConfig.APISecret = param.APISecret
|
||||||
godaddyConfig.PropagationTimeout = 30 * time.Minute
|
godaddyConfig.PropagationTimeout = 60 * time.Minute
|
||||||
godaddyConfig.PollingInterval = 30 * time.Second
|
godaddyConfig.PollingInterval = 5 * time.Second
|
||||||
godaddyConfig.TTL = 3600
|
godaddyConfig.TTL = 3600
|
||||||
p, err = godaddy.NewDNSProviderConfig(godaddyConfig)
|
p, err = godaddy.NewDNSProviderConfig(godaddyConfig)
|
||||||
case NameCom:
|
case NameCom:
|
||||||
|
@ -5655,6 +5655,36 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/files/log/read": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "按行读取日志文件",
|
||||||
|
"tags": [
|
||||||
|
"File"
|
||||||
|
],
|
||||||
|
"summary": "Read file by Line",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/request.FileReadByLineReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/files/mode": {
|
"/files/mode": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -5785,36 +5815,6 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/files/read": {
|
|
||||||
"post": {
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"ApiKeyAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "按行读取文件",
|
|
||||||
"tags": [
|
|
||||||
"File"
|
|
||||||
],
|
|
||||||
"summary": "Read file by Line",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "request",
|
|
||||||
"name": "request",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/request.FileReadByLineReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/files/recycle/clear": {
|
"/files/recycle/clear": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -17777,16 +17777,22 @@ const docTemplate = `{
|
|||||||
"required": [
|
"required": [
|
||||||
"page",
|
"page",
|
||||||
"pageSize",
|
"pageSize",
|
||||||
"path"
|
"type"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"ID": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"pageSize": {
|
"pageSize": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"path": {
|
"type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5648,6 +5648,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/files/log/read": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "按行读取日志文件",
|
||||||
|
"tags": [
|
||||||
|
"File"
|
||||||
|
],
|
||||||
|
"summary": "Read file by Line",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/request.FileReadByLineReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/files/mode": {
|
"/files/mode": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -5778,36 +5808,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/files/read": {
|
|
||||||
"post": {
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"ApiKeyAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "按行读取文件",
|
|
||||||
"tags": [
|
|
||||||
"File"
|
|
||||||
],
|
|
||||||
"summary": "Read file by Line",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "request",
|
|
||||||
"name": "request",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/request.FileReadByLineReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/files/recycle/clear": {
|
"/files/recycle/clear": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -17770,16 +17770,22 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"page",
|
"page",
|
||||||
"pageSize",
|
"pageSize",
|
||||||
"path"
|
"type"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"ID": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"pageSize": {
|
"pageSize": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"path": {
|
"type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3137,16 +3137,20 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
request.FileReadByLineReq:
|
request.FileReadByLineReq:
|
||||||
properties:
|
properties:
|
||||||
|
ID:
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
page:
|
page:
|
||||||
type: integer
|
type: integer
|
||||||
pageSize:
|
pageSize:
|
||||||
type: integer
|
type: integer
|
||||||
path:
|
type:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- page
|
- page
|
||||||
- pageSize
|
- pageSize
|
||||||
- path
|
- type
|
||||||
type: object
|
type: object
|
||||||
request.FileRename:
|
request.FileRename:
|
||||||
properties:
|
properties:
|
||||||
@ -8145,6 +8149,24 @@ paths:
|
|||||||
summary: List favorites
|
summary: List favorites
|
||||||
tags:
|
tags:
|
||||||
- File
|
- File
|
||||||
|
/files/log/read:
|
||||||
|
post:
|
||||||
|
description: 按行读取日志文件
|
||||||
|
parameters:
|
||||||
|
- description: request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/request.FileReadByLineReq'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Read file by Line
|
||||||
|
tags:
|
||||||
|
- File
|
||||||
/files/mode:
|
/files/mode:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@ -8230,24 +8252,6 @@ paths:
|
|||||||
formatEN: Change owner [paths] => [user]/[group]
|
formatEN: Change owner [paths] => [user]/[group]
|
||||||
formatZH: 修改用户/组 [paths] => [user]/[group]
|
formatZH: 修改用户/组 [paths] => [user]/[group]
|
||||||
paramKeys: []
|
paramKeys: []
|
||||||
/files/read:
|
|
||||||
post:
|
|
||||||
description: 按行读取文件
|
|
||||||
parameters:
|
|
||||||
- description: request
|
|
||||||
in: body
|
|
||||||
name: request
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/request.FileReadByLineReq'
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
security:
|
|
||||||
- ApiKeyAuth: []
|
|
||||||
summary: Read file by Line
|
|
||||||
tags:
|
|
||||||
- File
|
|
||||||
/files/recycle/clear:
|
/files/recycle/clear:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
@ -165,7 +165,9 @@ export namespace File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FileReadByLine {
|
export interface FileReadByLine {
|
||||||
path: string;
|
id?: number;
|
||||||
|
type: string;
|
||||||
|
name?: string;
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
79
frontend/src/components/log-dialog/index.vue
Normal file
79
frontend/src/components/log-dialog/index.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
v-model="open"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:size="globalStore.isFullScreen ? '100%' : '50%'"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="$t('website.log')" :back="handleClose">
|
||||||
|
<template #extra v-if="!mobile">
|
||||||
|
<el-tooltip :content="loadTooltip()" placement="top">
|
||||||
|
<el-button @click="toggleFullscreen" class="fullScreen" icon="FullScreen" plain></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</DrawerHeader>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<LogFile :config="config"></LogFile>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import LogFile from '@/components/log-file/index.vue';
|
||||||
|
import { GlobalStore } from '@/store';
|
||||||
|
import screenfull from 'screenfull';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
|
||||||
|
const globalStore = GlobalStore();
|
||||||
|
|
||||||
|
const mobile = computed(() => {
|
||||||
|
return globalStore.isMobile();
|
||||||
|
});
|
||||||
|
|
||||||
|
interface LogProps {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
style: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
const config = ref();
|
||||||
|
const em = defineEmits(['close']);
|
||||||
|
|
||||||
|
const handleClose = (search: boolean) => {
|
||||||
|
open.value = false;
|
||||||
|
em('close', search);
|
||||||
|
};
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
if (screenfull.isEnabled) {
|
||||||
|
screenfull.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTooltip = () => {
|
||||||
|
return i18n.global.t('commons.button.' + (screenfull.isFullscreen ? 'quitFullscreen' : 'fullscreen'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const acceptParams = (props: LogProps) => {
|
||||||
|
config.value = props;
|
||||||
|
open.value = true;
|
||||||
|
|
||||||
|
if (!mobile.value) {
|
||||||
|
screenfull.on('change', () => {
|
||||||
|
globalStore.isFullScreen = screenfull.isFullscreen;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ acceptParams });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.fullScreen {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
250
frontend/src/components/log-file/index.vue
Normal file
250
frontend/src/components/log-file/index.vue
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="defaultButton">
|
||||||
|
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail(false)">
|
||||||
|
{{ $t('commons.button.watch') }}
|
||||||
|
</el-checkbox>
|
||||||
|
<el-button class="ml-2.5" @click="onDownload" icon="Download" :disabled="data.content === ''">
|
||||||
|
{{ $t('file.download') }}
|
||||||
|
</el-button>
|
||||||
|
<span v-if="$slots.button" class="ml-2.5">
|
||||||
|
<slot name="button"></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2.5">
|
||||||
|
<Codemirror
|
||||||
|
ref="logContainer"
|
||||||
|
:style="styleObject"
|
||||||
|
:autofocus="true"
|
||||||
|
:placeholder="$t('website.noLog')"
|
||||||
|
:indent-with-tab="true"
|
||||||
|
:tabSize="4"
|
||||||
|
:lineWrapping="true"
|
||||||
|
:matchBrackets="true"
|
||||||
|
theme="cobalt"
|
||||||
|
:styleActiveLine="true"
|
||||||
|
:extensions="extensions"
|
||||||
|
v-model="content"
|
||||||
|
:disabled="true"
|
||||||
|
@ready="handleReady"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Codemirror } from 'vue-codemirror';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef } from 'vue';
|
||||||
|
import { downloadFile } from '@/utils/util';
|
||||||
|
import { ReadByLine } from '@/api/modules/files';
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
const extensions = [javascript(), oneDark];
|
||||||
|
|
||||||
|
interface LogProps {
|
||||||
|
id?: number;
|
||||||
|
type: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
config: {
|
||||||
|
type: Object as () => LogProps | null,
|
||||||
|
default: () => ({
|
||||||
|
id: 0,
|
||||||
|
type: '',
|
||||||
|
name: '',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
type: String,
|
||||||
|
default: 'height: calc(100vh - 200px); width: 100%; min-height: 400px',
|
||||||
|
},
|
||||||
|
defaultButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
hasContent: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = ref({
|
||||||
|
enable: false,
|
||||||
|
content: '',
|
||||||
|
path: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
let timer: NodeJS.Timer | null = null;
|
||||||
|
const tailLog = ref(false);
|
||||||
|
const view = shallowRef();
|
||||||
|
const content = ref('');
|
||||||
|
const end = ref(false);
|
||||||
|
const lastContent = ref('');
|
||||||
|
const logContainer = ref();
|
||||||
|
const scrollerElement = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const readReq = reactive({
|
||||||
|
id: 0,
|
||||||
|
type: '',
|
||||||
|
name: '',
|
||||||
|
page: 0,
|
||||||
|
pageSize: 2000,
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['update:loading', 'update:hasContent']);
|
||||||
|
|
||||||
|
const handleReady = (payload) => {
|
||||||
|
view.value = payload.view;
|
||||||
|
const editorContainer = payload.container;
|
||||||
|
const editorElement = editorContainer.querySelector('.cm-editor');
|
||||||
|
scrollerElement.value = editorElement.querySelector('.cm-scroller') as HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loading = ref(props.loading);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.loading,
|
||||||
|
(newLoading) => {
|
||||||
|
loading.value = newLoading;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const changeLoading = () => {
|
||||||
|
loading.value = !loading.value;
|
||||||
|
emit('update:loading', loading.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styleObject = computed(() => {
|
||||||
|
const styles = {};
|
||||||
|
let style = 'height: calc(100vh - 200px); width: 100%; min-height: 400px';
|
||||||
|
if (props.style != null && props.style != '') {
|
||||||
|
style = props.style;
|
||||||
|
}
|
||||||
|
style.split(';').forEach((styleRule) => {
|
||||||
|
const [property, value] = styleRule.split(':');
|
||||||
|
if (property && value) {
|
||||||
|
const formattedProperty = property
|
||||||
|
.trim()
|
||||||
|
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
||||||
|
.toLowerCase();
|
||||||
|
styles[formattedProperty] = value.trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return styles;
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopSignals = [
|
||||||
|
'docker-compose up failed!',
|
||||||
|
'docker-compose up successful!',
|
||||||
|
'image build failed!',
|
||||||
|
'image build successful!',
|
||||||
|
'image pull failed!',
|
||||||
|
'image pull successful!',
|
||||||
|
'image push failed!',
|
||||||
|
'image push successful!',
|
||||||
|
];
|
||||||
|
|
||||||
|
const getContent = () => {
|
||||||
|
if (!end.value) {
|
||||||
|
readReq.page += 1;
|
||||||
|
}
|
||||||
|
readReq.id = props.config.id;
|
||||||
|
readReq.type = props.config.type;
|
||||||
|
readReq.name = props.config.name;
|
||||||
|
ReadByLine(readReq).then((res) => {
|
||||||
|
if (!end.value && res.data.end) {
|
||||||
|
lastContent.value = content.value;
|
||||||
|
}
|
||||||
|
data.value = res.data;
|
||||||
|
if (res.data.content != '') {
|
||||||
|
if (stopSignals.some((singal) => res.data.content.endsWith(singal))) {
|
||||||
|
onCloseLog();
|
||||||
|
}
|
||||||
|
if (end.value) {
|
||||||
|
if (lastContent.value == '') {
|
||||||
|
content.value = res.data.content;
|
||||||
|
} else {
|
||||||
|
content.value = lastContent.value + '\n' + res.data.content;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (content.value == '') {
|
||||||
|
content.value = res.data.content;
|
||||||
|
} else {
|
||||||
|
content.value = content.value + '\n' + res.data.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end.value = res.data.end;
|
||||||
|
emit('update:hasContent', content.value !== '');
|
||||||
|
nextTick(() => {
|
||||||
|
const state = view.value.state;
|
||||||
|
view.value.dispatch({
|
||||||
|
selection: { anchor: state.doc.length, head: state.doc.length },
|
||||||
|
});
|
||||||
|
view.value.focus();
|
||||||
|
const firstLine = view.value.state.doc.line(view.value.state.doc.lines);
|
||||||
|
const { top } = view.value.lineBlockAt(firstLine.from);
|
||||||
|
scrollerElement.value.scrollTo({ top, behavior: 'instant' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeTail = (fromOutSide: boolean) => {
|
||||||
|
if (fromOutSide) {
|
||||||
|
tailLog.value = !tailLog.value;
|
||||||
|
}
|
||||||
|
if (tailLog.value) {
|
||||||
|
timer = setInterval(() => {
|
||||||
|
getContent();
|
||||||
|
}, 1000 * 2);
|
||||||
|
} else {
|
||||||
|
onCloseLog();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDownload = async () => {
|
||||||
|
changeLoading();
|
||||||
|
downloadFile(data.value.path);
|
||||||
|
changeLoading();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCloseLog = async () => {
|
||||||
|
tailLog.value = false;
|
||||||
|
clearInterval(Number(timer));
|
||||||
|
timer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isScrolledToBottom(element: HTMLElement): boolean {
|
||||||
|
return element.scrollTop + element.clientHeight + 1 >= element.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
tailLog.value = false;
|
||||||
|
|
||||||
|
getContent();
|
||||||
|
nextTick(() => {
|
||||||
|
if (scrollerElement.value) {
|
||||||
|
scrollerElement.value.addEventListener('scroll', function () {
|
||||||
|
if (isScrolledToBottom(scrollerElement.value)) {
|
||||||
|
getContent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
onCloseLog();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ changeTail, onDownload });
|
||||||
|
</script>
|
@ -1,160 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-drawer v-model="open" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
|
||||||
<template #header>
|
|
||||||
<DrawerHeader :header="$t('website.log')" :back="handleClose" />
|
|
||||||
</template>
|
|
||||||
<div>
|
|
||||||
<div class="mt-2.5">
|
|
||||||
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail">
|
|
||||||
{{ $t('commons.button.watch') }}
|
|
||||||
</el-checkbox>
|
|
||||||
<el-button class="ml-5" @click="onDownload" icon="Download" :disabled="data.content === ''">
|
|
||||||
{{ $t('file.download') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<Codemirror
|
|
||||||
ref="logContainer"
|
|
||||||
style="height: calc(100vh - 200px); width: 100%; min-height: 400px"
|
|
||||||
:autofocus="true"
|
|
||||||
:placeholder="$t('website.noLog')"
|
|
||||||
:indent-with-tab="true"
|
|
||||||
:tabSize="4"
|
|
||||||
:lineWrapping="true"
|
|
||||||
:matchBrackets="true"
|
|
||||||
theme="cobalt"
|
|
||||||
:styleActiveLine="true"
|
|
||||||
:extensions="extensions"
|
|
||||||
v-model="content"
|
|
||||||
:disabled="true"
|
|
||||||
@ready="handleReady"
|
|
||||||
/>
|
|
||||||
</el-drawer>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Codemirror } from 'vue-codemirror';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
|
||||||
import { nextTick, onUnmounted, reactive, ref, shallowRef } from 'vue';
|
|
||||||
import { downloadFile } from '@/utils/util';
|
|
||||||
import { ReadByLine } from '@/api/modules/files';
|
|
||||||
|
|
||||||
const extensions = [javascript(), oneDark];
|
|
||||||
|
|
||||||
interface LogProps {
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = ref({
|
|
||||||
enable: false,
|
|
||||||
content: '',
|
|
||||||
path: '',
|
|
||||||
});
|
|
||||||
const tailLog = ref(false);
|
|
||||||
let timer: NodeJS.Timer | null = null;
|
|
||||||
|
|
||||||
const view = shallowRef();
|
|
||||||
const editorContainer = ref<HTMLDivElement | null>(null);
|
|
||||||
const handleReady = (payload) => {
|
|
||||||
view.value = payload.view;
|
|
||||||
editorContainer.value = payload.container;
|
|
||||||
};
|
|
||||||
const content = ref('');
|
|
||||||
const end = ref(false);
|
|
||||||
const lastContent = ref('');
|
|
||||||
const open = ref(false);
|
|
||||||
const logContainer = ref();
|
|
||||||
|
|
||||||
const readReq = reactive({
|
|
||||||
path: '',
|
|
||||||
page: 0,
|
|
||||||
pageSize: 100,
|
|
||||||
});
|
|
||||||
const em = defineEmits(['close']);
|
|
||||||
|
|
||||||
const handleClose = (search: boolean) => {
|
|
||||||
open.value = false;
|
|
||||||
em('close', search);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getContent = () => {
|
|
||||||
if (!end.value) {
|
|
||||||
readReq.page += 1;
|
|
||||||
}
|
|
||||||
ReadByLine(readReq).then((res) => {
|
|
||||||
if (!end.value && res.data.end) {
|
|
||||||
lastContent.value = content.value;
|
|
||||||
}
|
|
||||||
data.value = res.data;
|
|
||||||
if (res.data.content != '') {
|
|
||||||
if (end.value) {
|
|
||||||
content.value = lastContent.value + '\n' + res.data.content;
|
|
||||||
} else {
|
|
||||||
if (content.value == '') {
|
|
||||||
content.value = res.data.content;
|
|
||||||
} else {
|
|
||||||
content.value = content.value + '\n' + res.data.content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end.value = res.data.end;
|
|
||||||
nextTick(() => {
|
|
||||||
const state = view.value.state;
|
|
||||||
view.value.dispatch({
|
|
||||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
|
||||||
});
|
|
||||||
view.value.focus();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeTail = () => {
|
|
||||||
if (tailLog.value) {
|
|
||||||
timer = setInterval(() => {
|
|
||||||
getContent();
|
|
||||||
}, 1000 * 1);
|
|
||||||
} else {
|
|
||||||
onCloseLog();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDownload = async () => {
|
|
||||||
downloadFile(data.value.path);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCloseLog = async () => {
|
|
||||||
tailLog.value = false;
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isScrolledToBottom(element: HTMLElement): boolean {
|
|
||||||
return element.scrollTop + element.clientHeight === element.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
const acceptParams = (props: LogProps) => {
|
|
||||||
readReq.path = props.path;
|
|
||||||
open.value = true;
|
|
||||||
tailLog.value = false;
|
|
||||||
|
|
||||||
getContent();
|
|
||||||
nextTick(() => {
|
|
||||||
let editorElement = editorContainer.value.querySelector('.cm-editor');
|
|
||||||
let scrollerElement = editorElement.querySelector('.cm-scroller') as HTMLElement;
|
|
||||||
if (scrollerElement) {
|
|
||||||
scrollerElement.addEventListener('scroll', function () {
|
|
||||||
if (isScrolledToBottom(scrollerElement)) {
|
|
||||||
getContent();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
onCloseLog();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({ acceptParams });
|
|
||||||
</script>
|
|
@ -78,21 +78,12 @@
|
|||||||
v-model="form.file"
|
v-model="form.file"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<codemirror
|
<LogFile
|
||||||
v-if="mode === 'log'"
|
ref="logRef"
|
||||||
:autofocus="true"
|
:config="logConfig"
|
||||||
placeholder="Waiting for docker-compose up output..."
|
:default-button="false"
|
||||||
:indent-with-tab="true"
|
v-if="mode === 'log' && showLog"
|
||||||
:tabSize="4"
|
:style="'height: calc(100vh - 370px);min-height: 200px'"
|
||||||
style="width: 100%; height: calc(100vh - 375px)"
|
|
||||||
:lineWrapping="true"
|
|
||||||
:matchBrackets="true"
|
|
||||||
theme="cobalt"
|
|
||||||
:styleActiveLine="true"
|
|
||||||
:extensions="extensions"
|
|
||||||
@ready="handleReady"
|
|
||||||
v-model="logInfo"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -113,40 +104,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
|
import { nextTick, onBeforeUnmount, reactive, ref } from 'vue';
|
||||||
import FileList from '@/components/file-list/index.vue';
|
import FileList from '@/components/file-list/index.vue';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
|
||||||
import { Rules } from '@/global/form-rules';
|
import { Rules } from '@/global/form-rules';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { ElForm, ElMessageBox } from 'element-plus';
|
import { ElForm, ElMessageBox } from 'element-plus';
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
import { listComposeTemplate, loadContainerLog, testCompose, upCompose } from '@/api/modules/container';
|
import { listComposeTemplate, testCompose, upCompose } from '@/api/modules/container';
|
||||||
import { loadBaseDir } from '@/api/modules/setting';
|
import { loadBaseDir } from '@/api/modules/setting';
|
||||||
import { formatImageStdout } from '@/utils/docker';
|
|
||||||
import { MsgError } from '@/utils/message';
|
import { MsgError } from '@/utils/message';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
|
||||||
|
const extensions = [javascript(), oneDark];
|
||||||
|
|
||||||
|
const showLog = ref(false);
|
||||||
const loading = ref();
|
const loading = ref();
|
||||||
|
|
||||||
const mode = ref('edit');
|
const mode = ref('edit');
|
||||||
const onCreating = ref();
|
const onCreating = ref();
|
||||||
const oldFrom = ref('edit');
|
const oldFrom = ref('edit');
|
||||||
|
|
||||||
const extensions = [javascript(), oneDark];
|
|
||||||
const view = shallowRef();
|
|
||||||
const handleReady = (payload) => {
|
|
||||||
view.value = payload.view;
|
|
||||||
};
|
|
||||||
const logInfo = ref();
|
|
||||||
|
|
||||||
const drawerVisible = ref(false);
|
const drawerVisible = ref(false);
|
||||||
const templateOptions = ref();
|
const templateOptions = ref();
|
||||||
|
|
||||||
const baseDir = ref();
|
const baseDir = ref();
|
||||||
const composeFile = ref();
|
const composeFile = ref();
|
||||||
|
|
||||||
let timer: NodeJS.Timer | null = null;
|
let timer: NodeJS.Timer | null = null;
|
||||||
|
const logRef = ref();
|
||||||
|
|
||||||
|
const logConfig = reactive({
|
||||||
|
type: 'compose-create',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@ -174,7 +162,6 @@ const acceptParams = (): void => {
|
|||||||
form.path = '';
|
form.path = '';
|
||||||
form.file = '';
|
form.file = '';
|
||||||
form.template = null;
|
form.template = null;
|
||||||
logInfo.value = '';
|
|
||||||
loadTemplates();
|
loadTemplates();
|
||||||
loadPath();
|
loadPath();
|
||||||
};
|
};
|
||||||
@ -243,7 +230,6 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
logInfo.value = '';
|
|
||||||
await testCompose(form)
|
await testCompose(form)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -252,8 +238,8 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
|||||||
mode.value = 'log';
|
mode.value = 'log';
|
||||||
await upCompose(form)
|
await upCompose(form)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
logInfo.value = '';
|
logConfig.name = res.data;
|
||||||
loadLogs(res.data);
|
loadLogs();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -267,26 +253,14 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadLogs = async (name: string) => {
|
const loadLogs = () => {
|
||||||
timer = setInterval(async () => {
|
showLog.value = false;
|
||||||
const res = await loadContainerLog('compose-create', name);
|
nextTick(() => {
|
||||||
logInfo.value = formatImageStdout(res.data);
|
showLog.value = true;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const state = view.value.state;
|
logRef.value.changeTail(true);
|
||||||
view.value.dispatch({
|
|
||||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
|
||||||
scrollIntoView: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
if (
|
});
|
||||||
logInfo.value.endsWith('docker-compose up failed!') ||
|
|
||||||
logInfo.value.endsWith('docker-compose up successful!')
|
|
||||||
) {
|
|
||||||
onCreating.value = false;
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
}, 1000 * 3);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadDir = async (path: string) => {
|
const loadDir = async (path: string) => {
|
||||||
|
@ -54,21 +54,12 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<codemirror
|
<LogFile
|
||||||
|
ref="logRef"
|
||||||
|
:config="logConfig"
|
||||||
|
:default-button="false"
|
||||||
v-if="logVisiable"
|
v-if="logVisiable"
|
||||||
:autofocus="true"
|
:style="'height: calc(100vh - 370px);min-height: 200px'"
|
||||||
placeholder="Waiting for build output..."
|
|
||||||
:indent-with-tab="true"
|
|
||||||
:tabSize="4"
|
|
||||||
style="max-height: 300px"
|
|
||||||
:lineWrapping="true"
|
|
||||||
:matchBrackets="true"
|
|
||||||
theme="cobalt"
|
|
||||||
:styleActiveLine="true"
|
|
||||||
:extensions="extensions"
|
|
||||||
@ready="handleReady"
|
|
||||||
v-model="logInfo"
|
|
||||||
:readOnly="true"
|
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@ -89,26 +80,23 @@ import FileList from '@/components/file-list/index.vue';
|
|||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
import { nextTick, onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
|
import { nextTick, reactive, ref } from 'vue';
|
||||||
import { Rules } from '@/global/form-rules';
|
import { Rules } from '@/global/form-rules';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { ElForm, ElMessage } from 'element-plus';
|
import { ElForm, ElMessage } from 'element-plus';
|
||||||
import { imageBuild, loadContainerLog } from '@/api/modules/container';
|
import { imageBuild } from '@/api/modules/container';
|
||||||
import { formatImageStdout } from '@/utils/docker';
|
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
|
|
||||||
const logVisiable = ref<boolean>(false);
|
const logVisiable = ref<boolean>(false);
|
||||||
const logInfo = ref();
|
|
||||||
const view = shallowRef();
|
|
||||||
const handleReady = (payload) => {
|
|
||||||
view.value = payload.view;
|
|
||||||
};
|
|
||||||
const extensions = [javascript(), oneDark];
|
const extensions = [javascript(), oneDark];
|
||||||
let timer: NodeJS.Timer | null = null;
|
|
||||||
|
|
||||||
const buttonDisabled = ref(false);
|
const buttonDisabled = ref(false);
|
||||||
|
|
||||||
const drawerVisiable = ref(false);
|
const drawerVisiable = ref(false);
|
||||||
|
const logRef = ref();
|
||||||
|
|
||||||
|
const logConfig = reactive({
|
||||||
|
type: 'image-build',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
from: 'path',
|
from: 'path',
|
||||||
dockerfile: '',
|
dockerfile: '',
|
||||||
@ -129,7 +117,6 @@ const acceptParams = async () => {
|
|||||||
form.dockerfile = '';
|
form.dockerfile = '';
|
||||||
form.tagStr = '';
|
form.tagStr = '';
|
||||||
form.name = '';
|
form.name = '';
|
||||||
logInfo.value = '';
|
|
||||||
buttonDisabled.value = false;
|
buttonDisabled.value = false;
|
||||||
};
|
};
|
||||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||||
@ -137,8 +124,6 @@ const emit = defineEmits<{ (e: 'search'): void }>();
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
drawerVisiable.value = false;
|
drawerVisiable.value = false;
|
||||||
emit('search');
|
emit('search');
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormInstance = InstanceType<typeof ElForm>;
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
@ -153,38 +138,22 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
|||||||
}
|
}
|
||||||
const res = await imageBuild(form);
|
const res = await imageBuild(form);
|
||||||
buttonDisabled.value = true;
|
buttonDisabled.value = true;
|
||||||
logVisiable.value = true;
|
logConfig.name = res.data;
|
||||||
loadLogs(res.data);
|
loadLogs();
|
||||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadLogs = async (path: string) => {
|
const loadLogs = () => {
|
||||||
timer = setInterval(async () => {
|
logVisiable.value = false;
|
||||||
if (logVisiable.value) {
|
nextTick(() => {
|
||||||
const res = await loadContainerLog('image-build', path);
|
logVisiable.value = true;
|
||||||
logInfo.value = formatImageStdout(res.data);
|
nextTick(() => {
|
||||||
nextTick(() => {
|
logRef.value.changeTail(true);
|
||||||
const state = view.value.state;
|
});
|
||||||
view.value.dispatch({
|
});
|
||||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
|
||||||
scrollIntoView: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (logInfo.value.endsWith('image build failed!') || logInfo.value.endsWith('image build successful!')) {
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
buttonDisabled.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 1000 * 3);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadBuildDir = async (path: string) => {
|
const loadBuildDir = async (path: string) => {
|
||||||
form.dockerfile = path;
|
form.dockerfile = path;
|
||||||
};
|
};
|
||||||
|
@ -31,22 +31,12 @@
|
|||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
<LogFile
|
||||||
<codemirror
|
ref="logRef"
|
||||||
v-if="logVisible"
|
:config="logConfig"
|
||||||
:autofocus="true"
|
:default-button="false"
|
||||||
placeholder="Waiting for pull output..."
|
v-if="showLog"
|
||||||
:indent-with-tab="true"
|
:style="'height: calc(100vh - 370px);min-height: 200px'"
|
||||||
:tabSize="4"
|
|
||||||
style="height: calc(100vh - 415px)"
|
|
||||||
:lineWrapping="true"
|
|
||||||
:matchBrackets="true"
|
|
||||||
theme="cobalt"
|
|
||||||
:styleActiveLine="true"
|
|
||||||
:extensions="extensions"
|
|
||||||
@ready="handleReady"
|
|
||||||
v-model="logInfo"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@ -64,18 +54,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
|
import { nextTick, reactive, ref } from 'vue';
|
||||||
import { Rules } from '@/global/form-rules';
|
import { Rules } from '@/global/form-rules';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { ElForm } from 'element-plus';
|
import { ElForm } from 'element-plus';
|
||||||
import { imagePull, loadContainerLog } from '@/api/modules/container';
|
import { imagePull } from '@/api/modules/container';
|
||||||
import { Container } from '@/api/interface/container';
|
import { Container } from '@/api/interface/container';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
import { formatImageStdout } from '@/utils/docker';
|
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
import LogFile from '@/components/log-file/index.vue';
|
||||||
|
|
||||||
const drawerVisible = ref(false);
|
const drawerVisible = ref(false);
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
@ -83,17 +70,15 @@ const form = reactive({
|
|||||||
repoID: null as number,
|
repoID: null as number,
|
||||||
imageName: '',
|
imageName: '',
|
||||||
});
|
});
|
||||||
|
const logConfig = reactive({
|
||||||
|
type: 'image-pull',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
const showLog = ref(false);
|
||||||
|
const logRef = ref();
|
||||||
const buttonDisabled = ref(false);
|
const buttonDisabled = ref(false);
|
||||||
|
|
||||||
const logVisible = ref(false);
|
const logVisible = ref(false);
|
||||||
const logInfo = ref();
|
const logInfo = ref();
|
||||||
const view = shallowRef();
|
|
||||||
const handleReady = (payload) => {
|
|
||||||
view.value = payload.view;
|
|
||||||
};
|
|
||||||
const extensions = [javascript(), oneDark];
|
|
||||||
let timer: NodeJS.Timer | null = null;
|
|
||||||
|
|
||||||
interface DialogProps {
|
interface DialogProps {
|
||||||
repos: Array<Container.RepoOptions>;
|
repos: Array<Container.RepoOptions>;
|
||||||
@ -108,6 +93,7 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
|||||||
repos.value = params.repos;
|
repos.value = params.repos;
|
||||||
buttonDisabled.value = false;
|
buttonDisabled.value = false;
|
||||||
logInfo.value = '';
|
logInfo.value = '';
|
||||||
|
showLog.value = false;
|
||||||
};
|
};
|
||||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||||
|
|
||||||
@ -124,35 +110,24 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
|||||||
const res = await imagePull(form);
|
const res = await imagePull(form);
|
||||||
logVisible.value = true;
|
logVisible.value = true;
|
||||||
buttonDisabled.value = true;
|
buttonDisabled.value = true;
|
||||||
loadLogs(res.data);
|
logConfig.name = res.data;
|
||||||
|
search();
|
||||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadLogs = async (path: string) => {
|
const search = () => {
|
||||||
timer = setInterval(async () => {
|
showLog.value = false;
|
||||||
if (logVisible.value) {
|
nextTick(() => {
|
||||||
const res = await loadContainerLog('image-pull', path);
|
showLog.value = true;
|
||||||
logInfo.value = formatImageStdout(res.data);
|
nextTick(() => {
|
||||||
nextTick(() => {
|
logRef.value.changeTail(true);
|
||||||
const state = view.value.state;
|
});
|
||||||
view.value.dispatch({
|
});
|
||||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
|
||||||
scrollIntoView: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (logInfo.value.endsWith('image pull failed!') || logInfo.value.endsWith('image pull successful!')) {
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
buttonDisabled.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 1000 * 3);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCloseLog = async () => {
|
const onCloseLog = async () => {
|
||||||
emit('search');
|
emit('search');
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
drawerVisible.value = false;
|
drawerVisible.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -165,11 +140,6 @@ function loadDetailInfo(id: number) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
acceptParams,
|
acceptParams,
|
||||||
});
|
});
|
||||||
|
@ -34,21 +34,13 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<codemirror
|
<LogFile
|
||||||
v-if="logVisible"
|
ref="logRef"
|
||||||
:autofocus="true"
|
:config="logConfig"
|
||||||
placeholder="Waiting for push output..."
|
:default-button="false"
|
||||||
:indent-with-tab="true"
|
v-if="logVisiable"
|
||||||
:tabSize="4"
|
:style="'height: calc(100vh - 370px);min-height: 200px'"
|
||||||
style="height: calc(100vh - 415px)"
|
v-model:loading="loading"
|
||||||
:lineWrapping="true"
|
|
||||||
:matchBrackets="true"
|
|
||||||
theme="cobalt"
|
|
||||||
:styleActiveLine="true"
|
|
||||||
:extensions="extensions"
|
|
||||||
@ready="handleReady"
|
|
||||||
v-model="logInfo"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@ -58,7 +50,7 @@
|
|||||||
<el-button @click="drawerVisible = false">
|
<el-button @click="drawerVisible = false">
|
||||||
{{ $t('commons.button.cancel') }}
|
{{ $t('commons.button.cancel') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button :disabled="buttonDisabled" type="primary" @click="onSubmit(formRef)">
|
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
|
||||||
{{ $t('container.push') }}
|
{{ $t('container.push') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</span>
|
</span>
|
||||||
@ -67,17 +59,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
|
import { nextTick, reactive, ref } from 'vue';
|
||||||
import { Rules } from '@/global/form-rules';
|
import { Rules } from '@/global/form-rules';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { ElForm } from 'element-plus';
|
import { ElForm } from 'element-plus';
|
||||||
import { imagePush, loadContainerLog } from '@/api/modules/container';
|
import { imagePush } from '@/api/modules/container';
|
||||||
import { Container } from '@/api/interface/container';
|
import { Container } from '@/api/interface/container';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
import { formatImageStdout } from '@/utils/docker';
|
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
|
||||||
const drawerVisible = ref(false);
|
const drawerVisible = ref(false);
|
||||||
@ -88,16 +76,14 @@ const form = reactive({
|
|||||||
name: '',
|
name: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttonDisabled = ref(false);
|
const logVisiable = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
const logVisible = ref(false);
|
const logRef = ref();
|
||||||
const logInfo = ref();
|
const logConfig = reactive({
|
||||||
const view = shallowRef();
|
type: 'image-push',
|
||||||
const handleReady = (payload) => {
|
name: '',
|
||||||
view.value = payload.view;
|
});
|
||||||
};
|
|
||||||
const extensions = [javascript(), oneDark];
|
|
||||||
let timer: NodeJS.Timer | null = null;
|
|
||||||
|
|
||||||
interface DialogProps {
|
interface DialogProps {
|
||||||
repos: Array<Container.RepoOptions>;
|
repos: Array<Container.RepoOptions>;
|
||||||
@ -109,7 +95,8 @@ const dialogData = ref<DialogProps>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||||
logVisible.value = false;
|
logVisiable.value = false;
|
||||||
|
loading.value = false;
|
||||||
drawerVisible.value = true;
|
drawerVisible.value = true;
|
||||||
form.tags = params.tags;
|
form.tags = params.tags;
|
||||||
form.repoID = 1;
|
form.repoID = 1;
|
||||||
@ -127,37 +114,25 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
|||||||
formEl.validate(async (valid) => {
|
formEl.validate(async (valid) => {
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
const res = await imagePush(form);
|
const res = await imagePush(form);
|
||||||
logVisible.value = true;
|
logVisiable.value = true;
|
||||||
buttonDisabled.value = true;
|
logConfig.name = res.data;
|
||||||
loadLogs(res.data);
|
loadLogs();
|
||||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadLogs = async (path: string) => {
|
const loadLogs = () => {
|
||||||
timer = setInterval(async () => {
|
logVisiable.value = false;
|
||||||
if (logVisible.value) {
|
nextTick(() => {
|
||||||
const res = await loadContainerLog('image-push', path);
|
logVisiable.value = true;
|
||||||
logInfo.value = formatImageStdout(res.data);
|
nextTick(() => {
|
||||||
nextTick(() => {
|
logRef.value.changeTail(true);
|
||||||
const state = view.value.state;
|
});
|
||||||
view.value.dispatch({
|
});
|
||||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
|
||||||
scrollIntoView: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (logInfo.value.endsWith('image push failed!') || logInfo.value.endsWith('image push successful!')) {
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
buttonDisabled.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 1000 * 3);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCloseLog = async () => {
|
const onCloseLog = async () => {
|
||||||
emit('search');
|
emit('search');
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
drawerVisible.value = false;
|
drawerVisible.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -170,11 +145,6 @@ function loadDetailInfo(id: number) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
acceptParams,
|
acceptParams,
|
||||||
});
|
});
|
||||||
|
@ -17,31 +17,25 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
<template #search>
|
<template #search>
|
||||||
<el-select class="float-left" v-model="currentFile" @change="search()">
|
<el-select class="float-left" v-model="logConfig.name" @change="search()">
|
||||||
<template #prefix>{{ $t('commons.button.log') }}</template>
|
<template #prefix>{{ $t('commons.button.log') }}</template>
|
||||||
<el-option v-for="(item, index) in fileList" :key="index" :label="item" :value="item" />
|
<el-option v-for="(item, index) in fileList" :key="index" :label="item" :value="item" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<div class="watchCheckbox">
|
<div class="watchCheckbox">
|
||||||
<el-checkbox border @change="changeWatch" v-model="isWatch">
|
<el-checkbox border @change="changeTail" v-model="isWatch">
|
||||||
{{ $t('commons.button.watch') }}
|
{{ $t('commons.button.watch') }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<codemirror
|
<LogFile
|
||||||
:autofocus="true"
|
ref="logRef"
|
||||||
:placeholder="$t('commons.msg.noneData')"
|
:config="logConfig"
|
||||||
:indent-with-tab="true"
|
:default-button="false"
|
||||||
:tabSize="4"
|
v-if="showLog"
|
||||||
style="height: calc(100vh - 370px)"
|
v-model:loading="loading"
|
||||||
:lineWrapping="true"
|
v-model:hasContent="hasContent"
|
||||||
:matchBrackets="true"
|
:style="'height: calc(100vh - 370px);min-height: 200px'"
|
||||||
theme="cobalt"
|
|
||||||
:styleActiveLine="true"
|
|
||||||
:extensions="extensions"
|
|
||||||
@ready="handleReady"
|
|
||||||
v-model="logs"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
@ -49,77 +43,48 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
|
||||||
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { getSystemFiles, getSystemLogs } from '@/api/modules/log';
|
import { getSystemFiles } from '@/api/modules/log';
|
||||||
const router = useRouter();
|
import LogFile from '@/components/log-file/index.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const loading = ref();
|
const loading = ref();
|
||||||
const isWatch = ref();
|
const isWatch = ref();
|
||||||
const currentFile = ref();
|
|
||||||
const fileList = ref();
|
const fileList = ref();
|
||||||
|
const logRef = ref();
|
||||||
|
|
||||||
const extensions = [javascript(), oneDark];
|
const hasContent = ref(false);
|
||||||
const logs = ref();
|
const logConfig = reactive({
|
||||||
const view = shallowRef();
|
type: 'system',
|
||||||
const handleReady = (payload) => {
|
name: '',
|
||||||
view.value = payload.view;
|
});
|
||||||
};
|
const showLog = ref(false);
|
||||||
|
|
||||||
let timer: NodeJS.Timer | null = null;
|
const changeTail = () => {
|
||||||
|
logRef.value.changeTail(true);
|
||||||
const changeWatch = async () => {
|
|
||||||
if (isWatch.value) {
|
|
||||||
timer = setInterval(() => {
|
|
||||||
search();
|
|
||||||
}, 1000 * 3);
|
|
||||||
} else {
|
|
||||||
if (timer) {
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFiles = async () => {
|
const loadFiles = async () => {
|
||||||
const res = await getSystemFiles();
|
const res = await getSystemFiles();
|
||||||
fileList.value = res.data || [];
|
fileList.value = res.data || [];
|
||||||
if (fileList.value) {
|
if (fileList.value) {
|
||||||
currentFile.value = fileList.value[0];
|
logConfig.name = fileList.value[0];
|
||||||
search();
|
search();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const search = async () => {
|
const search = () => {
|
||||||
await getSystemLogs(currentFile.value)
|
showLog.value = false;
|
||||||
.then((res) => {
|
nextTick(() => {
|
||||||
loading.value = false;
|
showLog.value = true;
|
||||||
logs.value = res.data.replace(/\u0000/g, '');
|
});
|
||||||
nextTick(() => {
|
|
||||||
const state = view.value.state;
|
|
||||||
view.value.dispatch({
|
|
||||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
|
||||||
scrollIntoView: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeRoute = async (addr: string) => {
|
const onChangeRoute = async (addr: string) => {
|
||||||
router.push({ name: addr });
|
router.push({ name: addr });
|
||||||
};
|
};
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadFiles();
|
loadFiles();
|
||||||
});
|
});
|
||||||
|
@ -6,16 +6,16 @@
|
|||||||
<el-col :span="16">
|
<el-col :span="16">
|
||||||
<el-button
|
<el-button
|
||||||
class="tag-button"
|
class="tag-button"
|
||||||
:class="logReq.logType === 'access.log' ? '' : 'no-active'"
|
:class="logConfig.name === 'access.log' ? '' : 'no-active'"
|
||||||
:type="logReq.logType === 'access.log' ? 'primary' : ''"
|
:type="logConfig.name === 'access.log' ? 'primary' : ''"
|
||||||
@click="changeType('access.log')"
|
@click="changeType('access.log')"
|
||||||
>
|
>
|
||||||
{{ $t('logs.runLog') }}
|
{{ $t('logs.runLog') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
class="tag-button"
|
class="tag-button"
|
||||||
:class="logReq.logType === 'error.log' ? '' : 'no-active'"
|
:class="logConfig.name === 'error.log' ? '' : 'no-active'"
|
||||||
:type="logReq.logType === 'error.log' ? 'primary' : ''"
|
:type="logConfig.name === 'error.log' ? 'primary' : ''"
|
||||||
@click="changeType('error.log')"
|
@click="changeType('error.log')"
|
||||||
>
|
>
|
||||||
{{ $t('logs.errLog') }}
|
{{ $t('logs.errLog') }}
|
||||||
@ -25,7 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #search>
|
<template #search>
|
||||||
<div>
|
<div>
|
||||||
<el-select v-model="logReq.id" @change="changeWebsite()">
|
<el-select v-model="logConfig.id" @change="changeWebsite()">
|
||||||
<template #prefix>{{ $t('website.website') }}</template>
|
<template #prefix>{{ $t('website.website') }}</template>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="(website, index) in websites"
|
v-for="(website, index) in websites"
|
||||||
@ -39,35 +39,23 @@
|
|||||||
{{ $t('commons.button.watch') }}
|
{{ $t('commons.button.watch') }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button class="left-button" @click="onDownload" icon="Download" :disabled="data.content === ''">
|
<el-button class="left-button" @click="onDownload" icon="Download" :disabled="!hasContent">
|
||||||
{{ $t('file.download') }}
|
{{ $t('file.download') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button type="primary" plain @click="onClean()" class="left-button" :disabled="!hasContent">
|
||||||
type="primary"
|
|
||||||
plain
|
|
||||||
@click="onClean()"
|
|
||||||
class="left-button"
|
|
||||||
:disabled="data.content.length === 0"
|
|
||||||
>
|
|
||||||
{{ $t('logs.deleteLogs') }}
|
{{ $t('logs.deleteLogs') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<Codemirror
|
<LogFile
|
||||||
style="height: calc(100vh - 368px); width: 100%"
|
ref="logRef"
|
||||||
:autofocus="true"
|
:config="logConfig"
|
||||||
:placeholder="$t('website.noLog')"
|
:default-button="false"
|
||||||
:indent-with-tab="true"
|
v-if="showLog"
|
||||||
:tabSize="4"
|
v-model:loading="loading"
|
||||||
:lineWrapping="true"
|
v-model:hasContent="hasContent"
|
||||||
:matchBrackets="true"
|
:style="'height: calc(100vh - 370px)'"
|
||||||
theme="cobalt"
|
|
||||||
:styleActiveLine="true"
|
|
||||||
:extensions="extensions"
|
|
||||||
v-model="content"
|
|
||||||
:disabled="true"
|
|
||||||
@ready="handleReady"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
@ -76,40 +64,32 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ListWebsites, OpWebsiteLog } from '@/api/modules/website';
|
import { ListWebsites, OpWebsiteLog } from '@/api/modules/website';
|
||||||
import { nextTick, reactive, shallowRef } from 'vue';
|
import { reactive } from 'vue';
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { ref } from 'vue';
|
import { ref, nextTick } from 'vue';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
import { dateFormatForName, downloadWithContent } from '@/utils/util';
|
import LogFile from '@/components/log-file/index.vue';
|
||||||
|
|
||||||
const extensions = [javascript(), oneDark];
|
|
||||||
|
|
||||||
|
const logConfig = reactive({
|
||||||
|
type: 'website',
|
||||||
|
id: 0,
|
||||||
|
name: 'access.log',
|
||||||
|
});
|
||||||
|
const showLog = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const websites = ref();
|
const websites = ref();
|
||||||
const data = ref({
|
|
||||||
enable: false,
|
|
||||||
content: '',
|
|
||||||
});
|
|
||||||
const confirmDialogRef = ref();
|
const confirmDialogRef = ref();
|
||||||
const tailLog = ref(false);
|
const tailLog = ref(false);
|
||||||
let timer: NodeJS.Timer | null = null;
|
const logRef = ref();
|
||||||
|
const hasContent = ref(false);
|
||||||
|
|
||||||
const content = ref('');
|
const searchLog = () => {
|
||||||
const end = ref(false);
|
showLog.value = false;
|
||||||
const lastContent = ref('');
|
nextTick(() => {
|
||||||
const editorContainer = ref<HTMLDivElement | null>(null);
|
showLog.value = true;
|
||||||
|
});
|
||||||
const logReq = reactive({
|
};
|
||||||
id: undefined,
|
|
||||||
operate: 'get',
|
|
||||||
logType: 'access.log',
|
|
||||||
page: 0,
|
|
||||||
pageSize: 500,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getWebsites = async () => {
|
const getWebsites = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@ -117,19 +97,8 @@ const getWebsites = async () => {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
websites.value = res.data || [];
|
websites.value = res.data || [];
|
||||||
if (websites.value.length > 0) {
|
if (websites.value.length > 0) {
|
||||||
logReq.id = websites.value[0].id;
|
logConfig.id = websites.value[0].id;
|
||||||
search();
|
showLog.value = true;
|
||||||
nextTick(() => {
|
|
||||||
let editorElement = editorContainer.value.querySelector('.cm-editor');
|
|
||||||
let scrollerElement = editorElement.querySelector('.cm-scroller') as HTMLElement;
|
|
||||||
if (scrollerElement) {
|
|
||||||
scrollerElement.addEventListener('scroll', function () {
|
|
||||||
if (isScrolledToBottom(scrollerElement)) {
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -137,60 +106,15 @@ const getWebsites = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const view = shallowRef();
|
|
||||||
const handleReady = (payload) => {
|
|
||||||
view.value = payload.view;
|
|
||||||
editorContainer.value = payload.container;
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeType = (type: string) => {
|
const changeType = (type: string) => {
|
||||||
logReq.logType = type;
|
logConfig.name = type;
|
||||||
if (logReq.id != undefined) {
|
if (logConfig.id != undefined) {
|
||||||
logReq.page = 0;
|
searchLog();
|
||||||
logReq.pageSize = 500;
|
|
||||||
search();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeWebsite = () => {
|
const changeWebsite = () => {
|
||||||
logReq.page = 0;
|
searchLog();
|
||||||
logReq.pageSize = 500;
|
|
||||||
end.value = false;
|
|
||||||
content.value = '';
|
|
||||||
search();
|
|
||||||
};
|
|
||||||
|
|
||||||
const search = () => {
|
|
||||||
if (!end.value) {
|
|
||||||
logReq.page += 1;
|
|
||||||
}
|
|
||||||
OpWebsiteLog(logReq).then((res) => {
|
|
||||||
if (!end.value && res.data.end) {
|
|
||||||
lastContent.value = content.value;
|
|
||||||
}
|
|
||||||
data.value = res.data;
|
|
||||||
if (res.data.content != '') {
|
|
||||||
if (end.value) {
|
|
||||||
content.value = lastContent.value + '\n' + res.data.content;
|
|
||||||
} else {
|
|
||||||
if (content.value == '') {
|
|
||||||
content.value = res.data.content;
|
|
||||||
} else {
|
|
||||||
content.value = content.value + '\n' + res.data.content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content.value = '';
|
|
||||||
}
|
|
||||||
end.value = res.data.end;
|
|
||||||
nextTick(() => {
|
|
||||||
const state = view.value.state;
|
|
||||||
view.value.dispatch({
|
|
||||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
|
||||||
});
|
|
||||||
view.value.focus();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClean = async () => {
|
const onClean = async () => {
|
||||||
@ -200,52 +124,41 @@ const onClean = async () => {
|
|||||||
submitInputInfo: i18n.global.t('logs.deleteLogs'),
|
submitInputInfo: i18n.global.t('logs.deleteLogs'),
|
||||||
};
|
};
|
||||||
confirmDialogRef.value!.acceptParams(params);
|
confirmDialogRef.value!.acceptParams(params);
|
||||||
|
searchLog();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDownload = async () => {
|
const onDownload = async () => {
|
||||||
downloadWithContent(data.value.content, logReq.logType + '-' + dateFormatForName(new Date()) + '.log');
|
logRef.value.onDownload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeTail = () => {
|
const changeTail = () => {
|
||||||
if (tailLog.value) {
|
logRef.value.changeTail(true);
|
||||||
timer = setInterval(() => {
|
|
||||||
search();
|
|
||||||
}, 1000 * 5);
|
|
||||||
} else {
|
|
||||||
onCloseLog();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCloseLog = async () => {
|
// const onCloseLog = async () => {
|
||||||
tailLog.value = false;
|
// tailLog.value = false;
|
||||||
clearInterval(Number(timer));
|
// clearInterval(Number(timer));
|
||||||
timer = null;
|
// timer = null;
|
||||||
};
|
// };
|
||||||
|
|
||||||
const onSubmitClean = async () => {
|
const onSubmitClean = async () => {
|
||||||
search();
|
|
||||||
const req = {
|
const req = {
|
||||||
id: logReq.id,
|
id: logConfig.id,
|
||||||
operate: 'delete',
|
operate: 'delete',
|
||||||
logType: logReq.logType,
|
logType: logConfig.name,
|
||||||
};
|
};
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
OpWebsiteLog(req)
|
OpWebsiteLog(req)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
search();
|
searchLog();
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function isScrolledToBottom(element: HTMLElement): boolean {
|
|
||||||
return element.scrollTop + element.clientHeight === element.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
logReq.logType = 'access.log';
|
|
||||||
getWebsites();
|
getWebsites();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -87,7 +87,7 @@ import CreateRuntime from '@/views/website/runtime/php/create/index.vue';
|
|||||||
import Status from '@/components/status/index.vue';
|
import Status from '@/components/status/index.vue';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import RouterMenu from '../index.vue';
|
import RouterMenu from '../index.vue';
|
||||||
import Log from '@/components/log/index.vue';
|
import Log from '@/components/log-dialog/index.vue';
|
||||||
|
|
||||||
const paginationConfig = reactive({
|
const paginationConfig = reactive({
|
||||||
cacheSizeKey: 'runtime-page-size',
|
cacheSizeKey: 'runtime-page-size',
|
||||||
@ -149,7 +149,7 @@ const openDetail = (row: Runtime.Runtime) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openLog = (row: Runtime.RuntimeDTO) => {
|
const openLog = (row: Runtime.RuntimeDTO) => {
|
||||||
logRef.value.acceptParams({ path: row.path + '/' + 'build.log' });
|
logRef.value.acceptParams({ id: row.id, type: 'php' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDelete = async (row: Runtime.Runtime) => {
|
const openDelete = async (row: Runtime.Runtime) => {
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('website.log')" prop="">
|
<el-table-column :label="$t('website.log')" width="100px">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button @click="openLog(row)" link type="primary">{{ $t('website.check') }}</el-button>
|
<el-button @click="openLog(row)" link type="primary">{{ $t('website.check') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
@ -143,7 +143,7 @@ import { MsgSuccess } from '@/utils/message';
|
|||||||
import { GlobalStore } from '@/store';
|
import { GlobalStore } from '@/store';
|
||||||
import SSLUpload from './upload/index.vue';
|
import SSLUpload from './upload/index.vue';
|
||||||
import Apply from './apply/index.vue';
|
import Apply from './apply/index.vue';
|
||||||
import Log from '@/components/log/index.vue';
|
import Log from '@/components/log-dialog/index.vue';
|
||||||
|
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
const paginationConfig = reactive({
|
const paginationConfig = reactive({
|
||||||
@ -248,7 +248,7 @@ const openDetail = (id: number) => {
|
|||||||
detailRef.value.acceptParams(id);
|
detailRef.value.acceptParams(id);
|
||||||
};
|
};
|
||||||
const openLog = (row: Website.SSLDTO) => {
|
const openLog = (row: Website.SSLDTO) => {
|
||||||
logRef.value.acceptParams({ path: row.logPath });
|
logRef.value.acceptParams({ id: row.id, type: 'ssl' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const applySSL = (row: Website.SSLDTO) => {
|
const applySSL = (row: Website.SSLDTO) => {
|
||||||
|
@ -4,48 +4,24 @@
|
|||||||
<el-form-item :label="$t('website.enable')">
|
<el-form-item :label="$t('website.enable')">
|
||||||
<el-switch v-model="data.enable" @change="updateEnable"></el-switch>
|
<el-switch v-model="data.enable" @change="updateEnable"></el-switch>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div class="mt-2.5">
|
</div>
|
||||||
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail">
|
<LogFile :config="{ id: id, type: 'website', name: logType }" :style="style">
|
||||||
{{ $t('commons.button.watch') }}
|
<template #button>
|
||||||
</el-checkbox>
|
<el-button @click="cleanLog" icon="Delete" :disabled="data.content === ''">
|
||||||
<el-button class="ml-5" @click="onDownload" icon="Download" :disabled="data.content === ''">
|
|
||||||
{{ $t('file.download') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button class="ml-5" @click="cleanLog" icon="Delete" :disabled="data.content === ''">
|
|
||||||
{{ $t('commons.button.clean') }}
|
{{ $t('commons.button.clean') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</LogFile>
|
||||||
<br />
|
|
||||||
<codemirror
|
|
||||||
re="logContainer"
|
|
||||||
style="height: calc(100vh - 430px); width: 100%"
|
|
||||||
:autofocus="true"
|
|
||||||
:placeholder="$t('website.noLog')"
|
|
||||||
:indent-with-tab="true"
|
|
||||||
:tabSize="4"
|
|
||||||
:lineWrapping="true"
|
|
||||||
:matchBrackets="true"
|
|
||||||
theme="cobalt"
|
|
||||||
:styleActiveLine="true"
|
|
||||||
:extensions="extensions"
|
|
||||||
v-model="content"
|
|
||||||
:disabled="true"
|
|
||||||
@ready="handleReady"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<OpDialog ref="opRef" @search="getContent()" />
|
<OpDialog ref="opRef" />
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { computed, ref } from 'vue';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
|
||||||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef } from 'vue';
|
|
||||||
import { OpWebsiteLog } from '@/api/modules/website';
|
import { OpWebsiteLog } from '@/api/modules/website';
|
||||||
import { downloadFile } from '@/utils/util';
|
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
|
import LogFile from '@/components/log-file/index.vue';
|
||||||
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
|
||||||
const extensions = [javascript(), oneDark];
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
logType: {
|
logType: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -62,75 +38,15 @@ const logType = computed(() => {
|
|||||||
const id = computed(() => {
|
const id = computed(() => {
|
||||||
return props.id;
|
return props.id;
|
||||||
});
|
});
|
||||||
|
const style = ref('height: calc(100vh - 400px); width: 100%; min-height: 400px');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const data = ref({
|
const data = ref({
|
||||||
enable: false,
|
enable: false,
|
||||||
content: '',
|
content: '',
|
||||||
path: '',
|
path: '',
|
||||||
});
|
});
|
||||||
const tailLog = ref(false);
|
|
||||||
let timer: NodeJS.Timer | null = null;
|
|
||||||
const opRef = ref();
|
const opRef = ref();
|
||||||
|
|
||||||
const view = shallowRef();
|
|
||||||
const editorContainer = ref<HTMLDivElement | null>(null);
|
|
||||||
const handleReady = (payload) => {
|
|
||||||
view.value = payload.view;
|
|
||||||
editorContainer.value = payload.container;
|
|
||||||
};
|
|
||||||
const content = ref('');
|
|
||||||
const end = ref(false);
|
|
||||||
const lastContent = ref('');
|
|
||||||
|
|
||||||
const readReq = reactive({
|
|
||||||
id: id.value,
|
|
||||||
operate: 'get',
|
|
||||||
logType: logType.value,
|
|
||||||
page: 0,
|
|
||||||
pageSize: 500,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getContent = () => {
|
|
||||||
if (!end.value) {
|
|
||||||
readReq.page += 1;
|
|
||||||
}
|
|
||||||
OpWebsiteLog(readReq).then((res) => {
|
|
||||||
if (!end.value && res.data.end) {
|
|
||||||
lastContent.value = content.value;
|
|
||||||
}
|
|
||||||
data.value = res.data;
|
|
||||||
if (res.data.content != '') {
|
|
||||||
if (end.value) {
|
|
||||||
content.value = lastContent.value + '\n' + res.data.content;
|
|
||||||
} else {
|
|
||||||
if (content.value == '') {
|
|
||||||
content.value = res.data.content;
|
|
||||||
} else {
|
|
||||||
content.value = content.value + '\n' + res.data.content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end.value = res.data.end;
|
|
||||||
nextTick(() => {
|
|
||||||
const state = view.value.state;
|
|
||||||
view.value.dispatch({
|
|
||||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
|
||||||
});
|
|
||||||
view.value.focus();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeTail = () => {
|
|
||||||
if (tailLog.value) {
|
|
||||||
timer = setInterval(() => {
|
|
||||||
getContent();
|
|
||||||
}, 1000 * 5);
|
|
||||||
} else {
|
|
||||||
onCloseLog();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateEnable = () => {
|
const updateEnable = () => {
|
||||||
const operate = data.value.enable ? 'enable' : 'disable';
|
const operate = data.value.enable ? 'enable' : 'disable';
|
||||||
const req = {
|
const req = {
|
||||||
@ -141,7 +57,7 @@ const updateEnable = () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
OpWebsiteLog(req)
|
OpWebsiteLog(req)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
getContent();
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -158,37 +74,4 @@ const cleanLog = async () => {
|
|||||||
params: { id: id.value, operate: 'delete', logType: logType.value },
|
params: { id: id.value, operate: 'delete', logType: logType.value },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDownload = async () => {
|
|
||||||
downloadFile(data.value.path);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCloseLog = async () => {
|
|
||||||
tailLog.value = false;
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isScrolledToBottom(element: HTMLElement): boolean {
|
|
||||||
return element.scrollTop + element.clientHeight === element.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getContent();
|
|
||||||
nextTick(() => {
|
|
||||||
let editorElement = editorContainer.value.querySelector('.cm-editor');
|
|
||||||
let scrollerElement = editorElement.querySelector('.cm-scroller') as HTMLElement;
|
|
||||||
if (scrollerElement) {
|
|
||||||
scrollerElement.addEventListener('scroll', function () {
|
|
||||||
if (isScrolledToBottom(scrollerElement)) {
|
|
||||||
getContent();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
onCloseLog();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -84,7 +84,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||||||
outDir: '../cmd/server/web',
|
outDir: '../cmd/server/web',
|
||||||
minify: 'esbuild',
|
minify: 'esbuild',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['codemirror'],
|
external: ['codemirror'],
|
||||||
output: {
|
output: {
|
||||||
chunkFileNames: 'assets/js/[name]-[hash].js',
|
chunkFileNames: 'assets/js/[name]-[hash].js',
|
||||||
entryFileNames: 'assets/js/[name]-[hash].js',
|
entryFileNames: 'assets/js/[name]-[hash].js',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user