mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-09 23:17:21 +08:00
feat: PHP 运行环境增加日志 (#2764)
Refs https://github.com/1Panel-dev/1Panel/issues/2761
This commit is contained in:
parent
c47075beeb
commit
d15bd1d6b4
11 changed files with 350 additions and 2 deletions
|
@ -683,3 +683,27 @@ func (b *BaseApi) Keys(c *gin.Context) {
|
||||||
res.Keys = keys
|
res.Keys = keys
|
||||||
helper.SuccessWithData(c, res)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -109,6 +109,12 @@ type FileRoleUpdate struct {
|
||||||
Sub bool `json:"sub" validate:"required"`
|
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 {
|
type FileExistReq struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Dir string `json:"dir" validate:"required"`
|
Dir string `json:"dir" validate:"required"`
|
||||||
|
|
|
@ -33,6 +33,12 @@ type FileWgetRes struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileLineContent struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
End bool `json:"end"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
type FileExist struct {
|
type FileExist struct {
|
||||||
Exist bool `json:"exist"`
|
Exist bool `json:"exist"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
|
||||||
fileRouter.POST("/size", baseApi.Size)
|
fileRouter.POST("/size", baseApi.Size)
|
||||||
fileRouter.GET("/ws", baseApi.Ws)
|
fileRouter.GET("/ws", baseApi.Ws)
|
||||||
fileRouter.GET("/keys", baseApi.Keys)
|
fileRouter.GET("/keys", baseApi.Keys)
|
||||||
|
fileRouter.POST("/read", baseApi.ReadFileByLine)
|
||||||
|
|
||||||
fileRouter.POST("/recycle/search", baseApi.SearchRecycleBinFile)
|
fileRouter.POST("/recycle/search", baseApi.SearchRecycleBinFile)
|
||||||
fileRouter.POST("/recycle/reduce", baseApi.ReduceRecycleBinFile)
|
fileRouter.POST("/recycle/reduce", baseApi.ReduceRecycleBinFile)
|
||||||
|
|
|
@ -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
|
package docs
|
||||||
|
|
||||||
import "github.com/swaggo/swag"
|
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": {
|
"/files/recycle/clear": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"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": {
|
"request.FileRename": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
@ -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": {
|
"/files/recycle/clear": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"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": {
|
"request.FileRename": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
@ -2998,6 +2998,19 @@ definitions:
|
||||||
required:
|
required:
|
||||||
- path
|
- path
|
||||||
type: object
|
type: object
|
||||||
|
request.FileReadByLineReq:
|
||||||
|
properties:
|
||||||
|
page:
|
||||||
|
type: integer
|
||||||
|
pageSize:
|
||||||
|
type: integer
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- page
|
||||||
|
- pageSize
|
||||||
|
- path
|
||||||
|
type: object
|
||||||
request.FileRename:
|
request.FileRename:
|
||||||
properties:
|
properties:
|
||||||
newName:
|
newName:
|
||||||
|
@ -7955,6 +7968,24 @@ paths:
|
||||||
formatEN: Change owner [paths] => [user]/[group]
|
formatEN: Change owner [paths] => [user]/[group]
|
||||||
formatZH: 修改用户/组 [paths] => [user]/[group]
|
formatZH: 修改用户/组 [paths] => [user]/[group]
|
||||||
paramKeys: []
|
paramKeys: []
|
||||||
|
/files/read:
|
||||||
|
post:
|
||||||
|
description: 按行读取文件
|
||||||
|
parameters:
|
||||||
|
- description: request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/request.FileReadByLineReq'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Read file by Line
|
||||||
|
tags:
|
||||||
|
- File
|
||||||
/files/recycle/clear:
|
/files/recycle/clear:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
@ -164,6 +164,12 @@ export namespace File {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileReadByLine {
|
||||||
|
path: string;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Favorite extends CommonModel {
|
export interface Favorite extends CommonModel {
|
||||||
path: string;
|
path: string;
|
||||||
isDir: boolean;
|
isDir: boolean;
|
||||||
|
|
|
@ -109,6 +109,10 @@ export const AddFavorite = (path: string) => {
|
||||||
return http.post<any>('files/favorite', { path: path });
|
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) => {
|
export const RemoveFavorite = (id: number) => {
|
||||||
return http.post<any>('files/favorite/del', { id: id });
|
return http.post<any>('files/favorite/del', { id: id });
|
||||||
};
|
};
|
||||||
|
|
|
@ -39,6 +39,11 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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
|
<el-table-column
|
||||||
prop="createdAt"
|
prop="createdAt"
|
||||||
:label="$t('commons.table.date')"
|
:label="$t('commons.table.date')"
|
||||||
|
@ -61,6 +66,7 @@
|
||||||
|
|
||||||
<CreateRuntime ref="createRef" @close="search" />
|
<CreateRuntime ref="createRef" @close="search" />
|
||||||
<OpDialog ref="opRef" @search="search" />
|
<OpDialog ref="opRef" @search="search" />
|
||||||
|
<Log ref="logRef" @close="search" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -74,6 +80,7 @@ import CreateRuntime from '@/views/website/runtime/php/create/index.vue';
|
||||||
import Status from '@/components/status/index.vue';
|
import Status from '@/components/status/index.vue';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import RouterMenu from '../index.vue';
|
import RouterMenu from '../index.vue';
|
||||||
|
import Log from '@/views/website/runtime/php/log/index.vue';
|
||||||
|
|
||||||
const paginationConfig = reactive({
|
const paginationConfig = reactive({
|
||||||
cacheSizeKey: 'runtime-page-size',
|
cacheSizeKey: 'runtime-page-size',
|
||||||
|
@ -89,6 +96,7 @@ let req = reactive<Runtime.RuntimeReq>({
|
||||||
});
|
});
|
||||||
let timer: NodeJS.Timer | null = null;
|
let timer: NodeJS.Timer | null = null;
|
||||||
const opRef = ref();
|
const opRef = ref();
|
||||||
|
const logRef = ref();
|
||||||
|
|
||||||
const buttons = [
|
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 });
|
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) => {
|
const openDelete = async (row: Runtime.Runtime) => {
|
||||||
opRef.value.acceptParams({
|
opRef.value.acceptParams({
|
||||||
title: i18n.global.t('commons.msg.deleteTitle'),
|
title: i18n.global.t('commons.msg.deleteTitle'),
|
||||||
|
|
160
frontend/src/views/website/runtime/php/log/index.vue
Normal file
160
frontend/src/views/website/runtime/php/log/index.vue
Normal 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>
|
Loading…
Add table
Reference in a new issue