mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 00:09:16 +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
|
||||
// @Summary Read file by Line
|
||||
// @Description 按行读取文件
|
||||
// @Description 按行读取日志文件
|
||||
// @Param request body request.FileReadByLineReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /files/read [post]
|
||||
// @Router /files/log/read [post]
|
||||
func (b *BaseApi) ReadFileByLine(c *gin.Context) {
|
||||
var req request.FileReadByLineReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
lines, end, err := files.ReadFileByLine(req.Path, req.Page, req.PageSize)
|
||||
res, err := fileService.ReadLogByLine(req)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -118,9 +118,11 @@ type FileRoleUpdate struct {
|
||||
}
|
||||
|
||||
type FileReadByLineReq struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Page int `json:"page" 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 {
|
||||
|
@ -1,6 +1,11 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WebsiteSSL struct {
|
||||
BaseModel
|
||||
@ -28,3 +33,7 @@ type WebsiteSSL struct {
|
||||
func (w WebsiteSSL) TableName() string {
|
||||
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"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
@ -699,18 +698,15 @@ func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error
|
||||
|
||||
func (u *ContainerService) LoadContainerLogs(req dto.OperationWithNameAndType) string {
|
||||
filePath := ""
|
||||
switch req.Type {
|
||||
case "image-pull", "image-push", "image-build", "compose-create":
|
||||
filePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name))
|
||||
case "compose-detail":
|
||||
client, err := docker.NewDockerClient()
|
||||
if req.Type == "compose-detail" {
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
options := types.ContainerListOptions{All: true}
|
||||
options.Filters = filters.NewArgs()
|
||||
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 {
|
||||
return ""
|
||||
}
|
||||
@ -725,9 +721,6 @@ func (u *ContainerService) LoadContainerLogs(req dto.OperationWithNameAndType) s
|
||||
break
|
||||
}
|
||||
}
|
||||
if req.Type == "compose-create" {
|
||||
filePath = path.Join(path.Dir(filePath), "compose.log")
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
return ""
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@ -41,6 +42,7 @@ type IFileService interface {
|
||||
ChangeOwner(req request.FileRoleUpdate) error
|
||||
ChangeMode(op request.FileCreate) error
|
||||
BatchChangeModeAndOwner(op request.FileRoleReq) error
|
||||
ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error)
|
||||
}
|
||||
|
||||
func NewIFileService() IFileService {
|
||||
@ -297,3 +299,62 @@ func (f *FileService) DirSize(req request.DirSizeReq) (response.DirSizeRes, erro
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
if !info.IsDir() && strings.HasPrefix(info.Name(), "1Panel-") {
|
||||
if info.Name() == "1Panel.log" {
|
||||
files = append(files, time.Now().Format("2006-01-02"))
|
||||
return nil
|
||||
|
@ -6,4 +6,9 @@ const (
|
||||
DB DBContext = "db"
|
||||
|
||||
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) {
|
||||
if !NewFileOp().Stat(filename) {
|
||||
return nil, true, nil
|
||||
}
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
|
@ -95,47 +95,47 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string) error {
|
||||
case DnsPod:
|
||||
dnsPodConfig := dnspod.NewDefaultConfig()
|
||||
dnsPodConfig.LoginToken = param.ID + "," + param.Token
|
||||
dnsPodConfig.PropagationTimeout = 30 * time.Minute
|
||||
dnsPodConfig.PollingInterval = 30 * time.Second
|
||||
dnsPodConfig.PropagationTimeout = 60 * time.Minute
|
||||
dnsPodConfig.PollingInterval = 5 * time.Second
|
||||
dnsPodConfig.TTL = 3600
|
||||
p, err = dnspod.NewDNSProviderConfig(dnsPodConfig)
|
||||
case AliYun:
|
||||
alidnsConfig := alidns.NewDefaultConfig()
|
||||
alidnsConfig.SecretKey = param.SecretKey
|
||||
alidnsConfig.APIKey = param.AccessKey
|
||||
alidnsConfig.PropagationTimeout = 30 * time.Minute
|
||||
alidnsConfig.PollingInterval = 30 * time.Second
|
||||
alidnsConfig.PropagationTimeout = 60 * time.Minute
|
||||
alidnsConfig.PollingInterval = 5 * time.Second
|
||||
alidnsConfig.TTL = 3600
|
||||
p, err = alidns.NewDNSProviderConfig(alidnsConfig)
|
||||
case CloudFlare:
|
||||
cloudflareConfig := cloudflare.NewDefaultConfig()
|
||||
cloudflareConfig.AuthEmail = param.Email
|
||||
cloudflareConfig.AuthKey = param.APIkey
|
||||
cloudflareConfig.PropagationTimeout = 30 * time.Minute
|
||||
cloudflareConfig.PollingInterval = 30 * time.Second
|
||||
cloudflareConfig.PropagationTimeout = 60 * time.Minute
|
||||
cloudflareConfig.PollingInterval = 5 * time.Second
|
||||
cloudflareConfig.TTL = 3600
|
||||
p, err = cloudflare.NewDNSProviderConfig(cloudflareConfig)
|
||||
case NameCheap:
|
||||
namecheapConfig := namecheap.NewDefaultConfig()
|
||||
namecheapConfig.APIKey = param.APIkey
|
||||
namecheapConfig.APIUser = param.APIUser
|
||||
namecheapConfig.PropagationTimeout = 30 * time.Minute
|
||||
namecheapConfig.PollingInterval = 30 * time.Second
|
||||
namecheapConfig.PropagationTimeout = 60 * time.Minute
|
||||
namecheapConfig.PollingInterval = 5 * time.Second
|
||||
namecheapConfig.TTL = 3600
|
||||
p, err = namecheap.NewDNSProviderConfig(namecheapConfig)
|
||||
case NameSilo:
|
||||
nameSiloConfig := namesilo.NewDefaultConfig()
|
||||
nameSiloConfig.APIKey = param.APIkey
|
||||
nameSiloConfig.PropagationTimeout = 30 * time.Minute
|
||||
nameSiloConfig.PollingInterval = 30 * time.Second
|
||||
nameSiloConfig.PropagationTimeout = 60 * time.Minute
|
||||
nameSiloConfig.PollingInterval = 5 * time.Second
|
||||
nameSiloConfig.TTL = 3600
|
||||
p, err = namesilo.NewDNSProviderConfig(nameSiloConfig)
|
||||
case Godaddy:
|
||||
godaddyConfig := godaddy.NewDefaultConfig()
|
||||
godaddyConfig.APIKey = param.APIkey
|
||||
godaddyConfig.APISecret = param.APISecret
|
||||
godaddyConfig.PropagationTimeout = 30 * time.Minute
|
||||
godaddyConfig.PollingInterval = 30 * time.Second
|
||||
godaddyConfig.PropagationTimeout = 60 * time.Minute
|
||||
godaddyConfig.PollingInterval = 5 * time.Second
|
||||
godaddyConfig.TTL = 3600
|
||||
p, err = godaddy.NewDNSProviderConfig(godaddyConfig)
|
||||
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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"post": {
|
||||
"security": [
|
||||
@ -17777,16 +17777,22 @@ const docTemplate = `{
|
||||
"required": [
|
||||
"page",
|
||||
"pageSize",
|
||||
"path"
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"ID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": {
|
||||
"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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"post": {
|
||||
"security": [
|
||||
@ -17770,16 +17770,22 @@
|
||||
"required": [
|
||||
"page",
|
||||
"pageSize",
|
||||
"path"
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"ID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
@ -3137,16 +3137,20 @@ definitions:
|
||||
type: object
|
||||
request.FileReadByLineReq:
|
||||
properties:
|
||||
ID:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
page:
|
||||
type: integer
|
||||
pageSize:
|
||||
type: integer
|
||||
path:
|
||||
type:
|
||||
type: string
|
||||
required:
|
||||
- page
|
||||
- pageSize
|
||||
- path
|
||||
- type
|
||||
type: object
|
||||
request.FileRename:
|
||||
properties:
|
||||
@ -8145,6 +8149,24 @@ paths:
|
||||
summary: List favorites
|
||||
tags:
|
||||
- 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:
|
||||
post:
|
||||
consumes:
|
||||
@ -8230,24 +8252,6 @@ paths:
|
||||
formatEN: Change owner [paths] => [user]/[group]
|
||||
formatZH: 修改用户/组 [paths] => [user]/[group]
|
||||
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:
|
||||
post:
|
||||
consumes:
|
||||
|
@ -165,7 +165,9 @@ export namespace File {
|
||||
}
|
||||
|
||||
export interface FileReadByLine {
|
||||
path: string;
|
||||
id?: number;
|
||||
type: string;
|
||||
name?: string;
|
||||
page: 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"
|
||||
/>
|
||||
</div>
|
||||
<codemirror
|
||||
v-if="mode === 'log'"
|
||||
:autofocus="true"
|
||||
placeholder="Waiting for docker-compose up output..."
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="width: 100%; height: calc(100vh - 375px)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
@ready="handleReady"
|
||||
v-model="logInfo"
|
||||
:disabled="true"
|
||||
<LogFile
|
||||
ref="logRef"
|
||||
:config="logConfig"
|
||||
:default-button="false"
|
||||
v-if="mode === 'log' && showLog"
|
||||
:style="'height: calc(100vh - 370px);min-height: 200px'"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@ -113,40 +104,37 @@
|
||||
</template>
|
||||
|
||||
<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 { Codemirror } from 'vue-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm, ElMessageBox } from 'element-plus';
|
||||
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 { formatImageStdout } from '@/utils/docker';
|
||||
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 mode = ref('edit');
|
||||
const onCreating = ref();
|
||||
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 templateOptions = ref();
|
||||
|
||||
const baseDir = ref();
|
||||
const composeFile = ref();
|
||||
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
const logRef = ref();
|
||||
|
||||
const logConfig = reactive({
|
||||
type: 'compose-create',
|
||||
name: '',
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
@ -174,7 +162,6 @@ const acceptParams = (): void => {
|
||||
form.path = '';
|
||||
form.file = '';
|
||||
form.template = null;
|
||||
logInfo.value = '';
|
||||
loadTemplates();
|
||||
loadPath();
|
||||
};
|
||||
@ -243,7 +230,6 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
logInfo.value = '';
|
||||
await testCompose(form)
|
||||
.then(async (res) => {
|
||||
loading.value = false;
|
||||
@ -252,8 +238,8 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
mode.value = 'log';
|
||||
await upCompose(form)
|
||||
.then((res) => {
|
||||
logInfo.value = '';
|
||||
loadLogs(res.data);
|
||||
logConfig.name = res.data;
|
||||
loadLogs();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
@ -267,26 +253,14 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
});
|
||||
};
|
||||
|
||||
const loadLogs = async (name: string) => {
|
||||
timer = setInterval(async () => {
|
||||
const res = await loadContainerLog('compose-create', name);
|
||||
logInfo.value = formatImageStdout(res.data);
|
||||
const loadLogs = () => {
|
||||
showLog.value = false;
|
||||
nextTick(() => {
|
||||
showLog.value = true;
|
||||
nextTick(() => {
|
||||
const state = view.value.state;
|
||||
view.value.dispatch({
|
||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
||||
scrollIntoView: true,
|
||||
});
|
||||
logRef.value.changeTail(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) => {
|
||||
|
@ -54,21 +54,12 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<codemirror
|
||||
<LogFile
|
||||
ref="logRef"
|
||||
:config="logConfig"
|
||||
:default-button="false"
|
||||
v-if="logVisiable"
|
||||
:autofocus="true"
|
||||
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"
|
||||
:style="'height: calc(100vh - 370px);min-height: 200px'"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -89,26 +80,23 @@ import FileList from '@/components/file-list/index.vue';
|
||||
import { Codemirror } from 'vue-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
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 i18n from '@/lang';
|
||||
import { ElForm, ElMessage } from 'element-plus';
|
||||
import { imageBuild, loadContainerLog } from '@/api/modules/container';
|
||||
import { formatImageStdout } from '@/utils/docker';
|
||||
import { imageBuild } from '@/api/modules/container';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
|
||||
const logVisiable = ref<boolean>(false);
|
||||
const logInfo = ref();
|
||||
const view = shallowRef();
|
||||
const handleReady = (payload) => {
|
||||
view.value = payload.view;
|
||||
};
|
||||
const extensions = [javascript(), oneDark];
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
const buttonDisabled = ref(false);
|
||||
|
||||
const drawerVisiable = ref(false);
|
||||
const logRef = ref();
|
||||
|
||||
const logConfig = reactive({
|
||||
type: 'image-build',
|
||||
name: '',
|
||||
});
|
||||
const form = reactive({
|
||||
from: 'path',
|
||||
dockerfile: '',
|
||||
@ -129,7 +117,6 @@ const acceptParams = async () => {
|
||||
form.dockerfile = '';
|
||||
form.tagStr = '';
|
||||
form.name = '';
|
||||
logInfo.value = '';
|
||||
buttonDisabled.value = false;
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
@ -137,8 +124,6 @@ const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
const handleClose = () => {
|
||||
drawerVisiable.value = false;
|
||||
emit('search');
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
};
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
@ -153,38 +138,22 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
}
|
||||
const res = await imageBuild(form);
|
||||
buttonDisabled.value = true;
|
||||
logVisiable.value = true;
|
||||
loadLogs(res.data);
|
||||
logConfig.name = res.data;
|
||||
loadLogs();
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
});
|
||||
};
|
||||
|
||||
const loadLogs = async (path: string) => {
|
||||
timer = setInterval(async () => {
|
||||
if (logVisiable.value) {
|
||||
const res = await loadContainerLog('image-build', path);
|
||||
logInfo.value = formatImageStdout(res.data);
|
||||
nextTick(() => {
|
||||
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);
|
||||
const loadLogs = () => {
|
||||
logVisiable.value = false;
|
||||
nextTick(() => {
|
||||
logVisiable.value = true;
|
||||
nextTick(() => {
|
||||
logRef.value.changeTail(true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
});
|
||||
|
||||
const loadBuildDir = async (path: string) => {
|
||||
form.dockerfile = path;
|
||||
};
|
||||
|
@ -31,22 +31,12 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<codemirror
|
||||
v-if="logVisible"
|
||||
:autofocus="true"
|
||||
placeholder="Waiting for pull output..."
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="height: calc(100vh - 415px)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
@ready="handleReady"
|
||||
v-model="logInfo"
|
||||
:disabled="true"
|
||||
<LogFile
|
||||
ref="logRef"
|
||||
:config="logConfig"
|
||||
:default-button="false"
|
||||
v-if="showLog"
|
||||
:style="'height: calc(100vh - 370px);min-height: 200px'"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -64,18 +54,15 @@
|
||||
</template>
|
||||
|
||||
<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 i18n from '@/lang';
|
||||
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 { 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 { formatImageStdout } from '@/utils/docker';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import LogFile from '@/components/log-file/index.vue';
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const form = reactive({
|
||||
@ -83,17 +70,15 @@ const form = reactive({
|
||||
repoID: null as number,
|
||||
imageName: '',
|
||||
});
|
||||
|
||||
const logConfig = reactive({
|
||||
type: 'image-pull',
|
||||
name: '',
|
||||
});
|
||||
const showLog = ref(false);
|
||||
const logRef = ref();
|
||||
const buttonDisabled = ref(false);
|
||||
|
||||
const logVisible = ref(false);
|
||||
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 {
|
||||
repos: Array<Container.RepoOptions>;
|
||||
@ -108,6 +93,7 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
repos.value = params.repos;
|
||||
buttonDisabled.value = false;
|
||||
logInfo.value = '';
|
||||
showLog.value = false;
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
@ -124,35 +110,24 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
const res = await imagePull(form);
|
||||
logVisible.value = true;
|
||||
buttonDisabled.value = true;
|
||||
loadLogs(res.data);
|
||||
logConfig.name = res.data;
|
||||
search();
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
});
|
||||
};
|
||||
|
||||
const loadLogs = async (path: string) => {
|
||||
timer = setInterval(async () => {
|
||||
if (logVisible.value) {
|
||||
const res = await loadContainerLog('image-pull', path);
|
||||
logInfo.value = formatImageStdout(res.data);
|
||||
nextTick(() => {
|
||||
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 search = () => {
|
||||
showLog.value = false;
|
||||
nextTick(() => {
|
||||
showLog.value = true;
|
||||
nextTick(() => {
|
||||
logRef.value.changeTail(true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onCloseLog = async () => {
|
||||
emit('search');
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
@ -165,11 +140,6 @@ function loadDetailInfo(id: number) {
|
||||
return '';
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
|
@ -34,21 +34,13 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<codemirror
|
||||
v-if="logVisible"
|
||||
:autofocus="true"
|
||||
placeholder="Waiting for push output..."
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="height: calc(100vh - 415px)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
@ready="handleReady"
|
||||
v-model="logInfo"
|
||||
:disabled="true"
|
||||
<LogFile
|
||||
ref="logRef"
|
||||
:config="logConfig"
|
||||
:default-button="false"
|
||||
v-if="logVisiable"
|
||||
:style="'height: calc(100vh - 370px);min-height: 200px'"
|
||||
v-model:loading="loading"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -58,7 +50,7 @@
|
||||
<el-button @click="drawerVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="buttonDisabled" type="primary" @click="onSubmit(formRef)">
|
||||
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
|
||||
{{ $t('container.push') }}
|
||||
</el-button>
|
||||
</span>
|
||||
@ -67,17 +59,13 @@
|
||||
</template>
|
||||
|
||||
<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 i18n from '@/lang';
|
||||
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 { 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 { formatImageStdout } from '@/utils/docker';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
@ -88,16 +76,14 @@ const form = reactive({
|
||||
name: '',
|
||||
});
|
||||
|
||||
const buttonDisabled = ref(false);
|
||||
const logVisiable = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const logVisible = ref(false);
|
||||
const logInfo = ref();
|
||||
const view = shallowRef();
|
||||
const handleReady = (payload) => {
|
||||
view.value = payload.view;
|
||||
};
|
||||
const extensions = [javascript(), oneDark];
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
const logRef = ref();
|
||||
const logConfig = reactive({
|
||||
type: 'image-push',
|
||||
name: '',
|
||||
});
|
||||
|
||||
interface DialogProps {
|
||||
repos: Array<Container.RepoOptions>;
|
||||
@ -109,7 +95,8 @@ const dialogData = ref<DialogProps>({
|
||||
});
|
||||
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
logVisible.value = false;
|
||||
logVisiable.value = false;
|
||||
loading.value = false;
|
||||
drawerVisible.value = true;
|
||||
form.tags = params.tags;
|
||||
form.repoID = 1;
|
||||
@ -127,37 +114,25 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
const res = await imagePush(form);
|
||||
logVisible.value = true;
|
||||
buttonDisabled.value = true;
|
||||
loadLogs(res.data);
|
||||
logVisiable.value = true;
|
||||
logConfig.name = res.data;
|
||||
loadLogs();
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
});
|
||||
};
|
||||
|
||||
const loadLogs = async (path: string) => {
|
||||
timer = setInterval(async () => {
|
||||
if (logVisible.value) {
|
||||
const res = await loadContainerLog('image-push', path);
|
||||
logInfo.value = formatImageStdout(res.data);
|
||||
nextTick(() => {
|
||||
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 loadLogs = () => {
|
||||
logVisiable.value = false;
|
||||
nextTick(() => {
|
||||
logVisiable.value = true;
|
||||
nextTick(() => {
|
||||
logRef.value.changeTail(true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onCloseLog = async () => {
|
||||
emit('search');
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
@ -170,11 +145,6 @@ function loadDetailInfo(id: number) {
|
||||
return '';
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
|
@ -17,31 +17,25 @@
|
||||
</el-row>
|
||||
</template>
|
||||
<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>
|
||||
<el-option v-for="(item, index) in fileList" :key="index" :label="item" :value="item" />
|
||||
</el-select>
|
||||
<div class="watchCheckbox">
|
||||
<el-checkbox border @change="changeWatch" v-model="isWatch">
|
||||
<el-checkbox border @change="changeTail" v-model="isWatch">
|
||||
{{ $t('commons.button.watch') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<codemirror
|
||||
:autofocus="true"
|
||||
:placeholder="$t('commons.msg.noneData')"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="height: calc(100vh - 370px)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
@ready="handleReady"
|
||||
v-model="logs"
|
||||
:disabled="true"
|
||||
<LogFile
|
||||
ref="logRef"
|
||||
:config="logConfig"
|
||||
:default-button="false"
|
||||
v-if="showLog"
|
||||
v-model:loading="loading"
|
||||
v-model:hasContent="hasContent"
|
||||
:style="'height: calc(100vh - 370px);min-height: 200px'"
|
||||
/>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
@ -49,77 +43,48 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Codemirror } from 'vue-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||
import { nextTick, onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getSystemFiles, getSystemLogs } from '@/api/modules/log';
|
||||
const router = useRouter();
|
||||
import { getSystemFiles } from '@/api/modules/log';
|
||||
import LogFile from '@/components/log-file/index.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref();
|
||||
const isWatch = ref();
|
||||
const currentFile = ref();
|
||||
const fileList = ref();
|
||||
const logRef = ref();
|
||||
|
||||
const extensions = [javascript(), oneDark];
|
||||
const logs = ref();
|
||||
const view = shallowRef();
|
||||
const handleReady = (payload) => {
|
||||
view.value = payload.view;
|
||||
};
|
||||
const hasContent = ref(false);
|
||||
const logConfig = reactive({
|
||||
type: 'system',
|
||||
name: '',
|
||||
});
|
||||
const showLog = ref(false);
|
||||
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
const changeWatch = async () => {
|
||||
if (isWatch.value) {
|
||||
timer = setInterval(() => {
|
||||
search();
|
||||
}, 1000 * 3);
|
||||
} else {
|
||||
if (timer) {
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
const changeTail = () => {
|
||||
logRef.value.changeTail(true);
|
||||
};
|
||||
|
||||
const loadFiles = async () => {
|
||||
const res = await getSystemFiles();
|
||||
fileList.value = res.data || [];
|
||||
if (fileList.value) {
|
||||
currentFile.value = fileList.value[0];
|
||||
logConfig.name = fileList.value[0];
|
||||
search();
|
||||
}
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
await getSystemLogs(currentFile.value)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
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 search = () => {
|
||||
showLog.value = false;
|
||||
nextTick(() => {
|
||||
showLog.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeRoute = async (addr: string) => {
|
||||
router.push({ name: addr });
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadFiles();
|
||||
});
|
||||
|
@ -6,16 +6,16 @@
|
||||
<el-col :span="16">
|
||||
<el-button
|
||||
class="tag-button"
|
||||
:class="logReq.logType === 'access.log' ? '' : 'no-active'"
|
||||
:type="logReq.logType === 'access.log' ? 'primary' : ''"
|
||||
:class="logConfig.name === 'access.log' ? '' : 'no-active'"
|
||||
:type="logConfig.name === 'access.log' ? 'primary' : ''"
|
||||
@click="changeType('access.log')"
|
||||
>
|
||||
{{ $t('logs.runLog') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="tag-button"
|
||||
:class="logReq.logType === 'error.log' ? '' : 'no-active'"
|
||||
:type="logReq.logType === 'error.log' ? 'primary' : ''"
|
||||
:class="logConfig.name === 'error.log' ? '' : 'no-active'"
|
||||
:type="logConfig.name === 'error.log' ? 'primary' : ''"
|
||||
@click="changeType('error.log')"
|
||||
>
|
||||
{{ $t('logs.errLog') }}
|
||||
@ -25,7 +25,7 @@
|
||||
</template>
|
||||
<template #search>
|
||||
<div>
|
||||
<el-select v-model="logReq.id" @change="changeWebsite()">
|
||||
<el-select v-model="logConfig.id" @change="changeWebsite()">
|
||||
<template #prefix>{{ $t('website.website') }}</template>
|
||||
<el-option
|
||||
v-for="(website, index) in websites"
|
||||
@ -39,35 +39,23 @@
|
||||
{{ $t('commons.button.watch') }}
|
||||
</el-checkbox>
|
||||
</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') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="onClean()"
|
||||
class="left-button"
|
||||
:disabled="data.content.length === 0"
|
||||
>
|
||||
<el-button type="primary" plain @click="onClean()" class="left-button" :disabled="!hasContent">
|
||||
{{ $t('logs.deleteLogs') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<Codemirror
|
||||
style="height: calc(100vh - 368px); 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"
|
||||
<LogFile
|
||||
ref="logRef"
|
||||
:config="logConfig"
|
||||
:default-button="false"
|
||||
v-if="showLog"
|
||||
v-model:loading="loading"
|
||||
v-model:hasContent="hasContent"
|
||||
:style="'height: calc(100vh - 370px)'"
|
||||
/>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
@ -76,40 +64,32 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ListWebsites, OpWebsiteLog } from '@/api/modules/website';
|
||||
import { nextTick, reactive, shallowRef } from 'vue';
|
||||
import { reactive } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { Codemirror } from 'vue-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { ref, nextTick } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { dateFormatForName, downloadWithContent } from '@/utils/util';
|
||||
|
||||
const extensions = [javascript(), oneDark];
|
||||
import LogFile from '@/components/log-file/index.vue';
|
||||
|
||||
const logConfig = reactive({
|
||||
type: 'website',
|
||||
id: 0,
|
||||
name: 'access.log',
|
||||
});
|
||||
const showLog = ref(false);
|
||||
const loading = ref(false);
|
||||
const websites = ref();
|
||||
const data = ref({
|
||||
enable: false,
|
||||
content: '',
|
||||
});
|
||||
const confirmDialogRef = ref();
|
||||
const tailLog = ref(false);
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
const logRef = ref();
|
||||
const hasContent = ref(false);
|
||||
|
||||
const content = ref('');
|
||||
const end = ref(false);
|
||||
const lastContent = ref('');
|
||||
const editorContainer = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const logReq = reactive({
|
||||
id: undefined,
|
||||
operate: 'get',
|
||||
logType: 'access.log',
|
||||
page: 0,
|
||||
pageSize: 500,
|
||||
});
|
||||
const searchLog = () => {
|
||||
showLog.value = false;
|
||||
nextTick(() => {
|
||||
showLog.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const getWebsites = async () => {
|
||||
loading.value = true;
|
||||
@ -117,19 +97,8 @@ const getWebsites = async () => {
|
||||
.then((res) => {
|
||||
websites.value = res.data || [];
|
||||
if (websites.value.length > 0) {
|
||||
logReq.id = websites.value[0].id;
|
||||
search();
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
logConfig.id = websites.value[0].id;
|
||||
showLog.value = true;
|
||||
}
|
||||
})
|
||||
.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) => {
|
||||
logReq.logType = type;
|
||||
if (logReq.id != undefined) {
|
||||
logReq.page = 0;
|
||||
logReq.pageSize = 500;
|
||||
search();
|
||||
logConfig.name = type;
|
||||
if (logConfig.id != undefined) {
|
||||
searchLog();
|
||||
}
|
||||
};
|
||||
|
||||
const changeWebsite = () => {
|
||||
logReq.page = 0;
|
||||
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();
|
||||
});
|
||||
});
|
||||
searchLog();
|
||||
};
|
||||
|
||||
const onClean = async () => {
|
||||
@ -200,52 +124,41 @@ const onClean = async () => {
|
||||
submitInputInfo: i18n.global.t('logs.deleteLogs'),
|
||||
};
|
||||
confirmDialogRef.value!.acceptParams(params);
|
||||
searchLog();
|
||||
};
|
||||
|
||||
const onDownload = async () => {
|
||||
downloadWithContent(data.value.content, logReq.logType + '-' + dateFormatForName(new Date()) + '.log');
|
||||
logRef.value.onDownload();
|
||||
};
|
||||
|
||||
const changeTail = () => {
|
||||
if (tailLog.value) {
|
||||
timer = setInterval(() => {
|
||||
search();
|
||||
}, 1000 * 5);
|
||||
} else {
|
||||
onCloseLog();
|
||||
}
|
||||
logRef.value.changeTail(true);
|
||||
};
|
||||
|
||||
const onCloseLog = async () => {
|
||||
tailLog.value = false;
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
};
|
||||
// const onCloseLog = async () => {
|
||||
// tailLog.value = false;
|
||||
// clearInterval(Number(timer));
|
||||
// timer = null;
|
||||
// };
|
||||
|
||||
const onSubmitClean = async () => {
|
||||
search();
|
||||
const req = {
|
||||
id: logReq.id,
|
||||
id: logConfig.id,
|
||||
operate: 'delete',
|
||||
logType: logReq.logType,
|
||||
logType: logConfig.name,
|
||||
};
|
||||
loading.value = true;
|
||||
OpWebsiteLog(req)
|
||||
.then(() => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
searchLog();
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
function isScrolledToBottom(element: HTMLElement): boolean {
|
||||
return element.scrollTop + element.clientHeight === element.scrollHeight;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
logReq.logType = 'access.log';
|
||||
getWebsites();
|
||||
});
|
||||
</script>
|
||||
|
@ -87,7 +87,7 @@ import CreateRuntime from '@/views/website/runtime/php/create/index.vue';
|
||||
import Status from '@/components/status/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import RouterMenu from '../index.vue';
|
||||
import Log from '@/components/log/index.vue';
|
||||
import Log from '@/components/log-dialog/index.vue';
|
||||
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'runtime-page-size',
|
||||
@ -149,7 +149,7 @@ const openDetail = (row: Runtime.Runtime) => {
|
||||
};
|
||||
|
||||
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) => {
|
||||
|
@ -81,7 +81,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('website.log')" prop="">
|
||||
<el-table-column :label="$t('website.log')" width="100px">
|
||||
<template #default="{ row }">
|
||||
<el-button @click="openLog(row)" link type="primary">{{ $t('website.check') }}</el-button>
|
||||
</template>
|
||||
@ -143,7 +143,7 @@ import { MsgSuccess } from '@/utils/message';
|
||||
import { GlobalStore } from '@/store';
|
||||
import SSLUpload from './upload/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 paginationConfig = reactive({
|
||||
@ -248,7 +248,7 @@ const openDetail = (id: number) => {
|
||||
detailRef.value.acceptParams(id);
|
||||
};
|
||||
const openLog = (row: Website.SSLDTO) => {
|
||||
logRef.value.acceptParams({ path: row.logPath });
|
||||
logRef.value.acceptParams({ id: row.id, type: 'ssl' });
|
||||
};
|
||||
|
||||
const applySSL = (row: Website.SSLDTO) => {
|
||||
|
@ -4,48 +4,24 @@
|
||||
<el-form-item :label="$t('website.enable')">
|
||||
<el-switch v-model="data.enable" @change="updateEnable"></el-switch>
|
||||
</el-form-item>
|
||||
<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>
|
||||
<el-button class="ml-5" @click="cleanLog" icon="Delete" :disabled="data.content === ''">
|
||||
</div>
|
||||
<LogFile :config="{ id: id, type: 'website', name: logType }" :style="style">
|
||||
<template #button>
|
||||
<el-button @click="cleanLog" icon="Delete" :disabled="data.content === ''">
|
||||
{{ $t('commons.button.clean') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
/>
|
||||
</template>
|
||||
</LogFile>
|
||||
</div>
|
||||
<OpDialog ref="opRef" @search="getContent()" />
|
||||
<OpDialog ref="opRef" />
|
||||
</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 { computed, ref } from 'vue';
|
||||
import { OpWebsiteLog } from '@/api/modules/website';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
import i18n from '@/lang';
|
||||
import LogFile from '@/components/log-file/index.vue';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const extensions = [javascript(), oneDark];
|
||||
const props = defineProps({
|
||||
logType: {
|
||||
type: String,
|
||||
@ -62,75 +38,15 @@ const logType = computed(() => {
|
||||
const id = computed(() => {
|
||||
return props.id;
|
||||
});
|
||||
const style = ref('height: calc(100vh - 400px); width: 100%; min-height: 400px');
|
||||
const loading = ref(false);
|
||||
const data = ref({
|
||||
enable: false,
|
||||
content: '',
|
||||
path: '',
|
||||
});
|
||||
const tailLog = ref(false);
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
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 operate = data.value.enable ? 'enable' : 'disable';
|
||||
const req = {
|
||||
@ -141,7 +57,7 @@ const updateEnable = () => {
|
||||
loading.value = true;
|
||||
OpWebsiteLog(req)
|
||||
.then(() => {
|
||||
getContent();
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
@ -158,37 +74,4 @@ const cleanLog = async () => {
|
||||
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>
|
||||
|
@ -84,7 +84,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
outDir: '../cmd/server/web',
|
||||
minify: 'esbuild',
|
||||
rollupOptions: {
|
||||
external: ['codemirror'],
|
||||
external: ['codemirror'],
|
||||
output: {
|
||||
chunkFileNames: 'assets/js/[name]-[hash].js',
|
||||
entryFileNames: 'assets/js/[name]-[hash].js',
|
||||
|
Loading…
x
Reference in New Issue
Block a user