feat: PHP 运行环境增加日志 (#2764)

Refs https://github.com/1Panel-dev/1Panel/issues/2761
pull/2772/head
zhengkunwang 2023-11-01 18:30:56 +08:00 committed by GitHub
parent c47075beeb
commit d15bd1d6b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 350 additions and 2 deletions

View File

@ -683,3 +683,27 @@ func (b *BaseApi) Keys(c *gin.Context) {
res.Keys = keys
helper.SuccessWithData(c, res)
}
// @Tags File
// @Summary Read file by Line
// @Description 按行读取文件
// @Param request body request.FileReadByLineReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /files/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)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
}
res := response.FileLineContent{
End: end,
}
res.Path = req.Path
res.Content = strings.Join(lines, "\n")
helper.SuccessWithData(c, res)
}

View File

@ -109,6 +109,12 @@ type FileRoleUpdate struct {
Sub bool `json:"sub" validate:"required"`
}
type FileReadByLineReq struct {
Path string `json:"path" validate:"required"`
Page int `json:"page" validate:"required"`
PageSize int `json:"pageSize" validate:"required"`
}
type FileExistReq struct {
Name string `json:"name" validate:"required"`
Dir string `json:"dir" validate:"required"`

View File

@ -33,6 +33,12 @@ type FileWgetRes struct {
Key string `json:"key"`
}
type FileLineContent struct {
Content string `json:"content"`
End bool `json:"end"`
Path string `json:"path"`
}
type FileExist struct {
Exist bool `json:"exist"`
}

View File

@ -37,6 +37,7 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
fileRouter.POST("/size", baseApi.Size)
fileRouter.GET("/ws", baseApi.Ws)
fileRouter.GET("/keys", baseApi.Keys)
fileRouter.POST("/read", baseApi.ReadFileByLine)
fileRouter.POST("/recycle/search", baseApi.SearchRecycleBinFile)
fileRouter.POST("/recycle/reduce", baseApi.ReduceRecycleBinFile)

View File

@ -1,5 +1,5 @@
// Code generated by swaggo/swag. DO NOT EDIT.
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
package docs
import "github.com/swaggo/swag"
@ -5666,6 +5666,36 @@ 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": [
@ -16955,6 +16985,25 @@ const docTemplate = `{
}
}
},
"request.FileReadByLineReq": {
"type": "object",
"required": [
"page",
"pageSize",
"path"
],
"properties": {
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
},
"path": {
"type": "string"
}
}
},
"request.FileRename": {
"type": "object",
"required": [

View File

@ -5659,6 +5659,36 @@
}
}
},
"/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": [
@ -16948,6 +16978,25 @@
}
}
},
"request.FileReadByLineReq": {
"type": "object",
"required": [
"page",
"pageSize",
"path"
],
"properties": {
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
},
"path": {
"type": "string"
}
}
},
"request.FileRename": {
"type": "object",
"required": [

View File

@ -2998,6 +2998,19 @@ definitions:
required:
- path
type: object
request.FileReadByLineReq:
properties:
page:
type: integer
pageSize:
type: integer
path:
type: string
required:
- page
- pageSize
- path
type: object
request.FileRename:
properties:
newName:
@ -7955,6 +7968,24 @@ 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

@ -164,6 +164,12 @@ export namespace File {
name: string;
}
export interface FileReadByLine {
path: string;
page: number;
pageSize: number;
}
export interface Favorite extends CommonModel {
path: string;
isDir: boolean;

View File

@ -109,6 +109,10 @@ export const AddFavorite = (path: string) => {
return http.post<any>('files/favorite', { path: path });
};
export const ReadByLine = (req: File.FileReadByLine) => {
return http.post<any>('files/read', req);
};
export const RemoveFavorite = (id: number) => {
return http.post<any>('files/favorite/del', { id: id });
};

View File

@ -39,6 +39,11 @@
</div>
</template>
</el-table-column>
<el-table-column :label="$t('website.log')" prop="">
<template #default="{ row }">
<el-button @click="openLog(row)" link type="primary">{{ $t('website.check') }}</el-button>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
@ -61,6 +66,7 @@
<CreateRuntime ref="createRef" @close="search" />
<OpDialog ref="opRef" @search="search" />
<Log ref="logRef" @close="search" />
</div>
</template>
@ -74,6 +80,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 '@/views/website/runtime/php/log/index.vue';
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
@ -89,6 +96,7 @@ let req = reactive<Runtime.RuntimeReq>({
});
let timer: NodeJS.Timer | null = null;
const opRef = ref();
const logRef = ref();
const buttons = [
{
@ -133,6 +141,10 @@ const openDetail = (row: Runtime.Runtime) => {
createRef.value.acceptParams({ type: row.type, mode: 'edit', id: row.id, appID: row.appID });
};
const openLog = (row: Runtime.RuntimeDTO) => {
logRef.value.acceptParams({ path: row.path + '/' + 'build.log' });
};
const openDelete = async (row: Runtime.Runtime) => {
opRef.value.acceptParams({
title: i18n.global.t('commons.msg.deleteTitle'),

View File

@ -0,0 +1,160 @@
<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>