1
0
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:
zhengkunwang 2023-11-23 11:00:08 +08:00 committed by GitHub
parent 2c612a5919
commit 038879819d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 738 additions and 838 deletions

View File

@ -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)
}

View File

@ -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 {

View File

@ -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))
}

View File

@ -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 ""

View File

@ -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
}

View File

@ -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

View File

@ -6,4 +6,9 @@ const (
DB DBContext = "db"
SystemRestart = "systemRestart"
TypeWebsite = "website"
TypePhp = "php"
TypeSSL = "ssl"
TypeSystem = "system"
)

View File

@ -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

View File

@ -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:

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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:

View File

@ -165,7 +165,9 @@ export namespace File {
}
export interface FileReadByLine {
path: string;
id?: number;
type: string;
name?: string;
page: number;
pageSize: number;
}

View 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>

View 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>

View File

@ -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>

View File

@ -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) => {

View File

@ -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;
};

View File

@ -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,
});

View File

@ -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,
});

View File

@ -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();
});

View File

@ -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>

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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>

View File

@ -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',