mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 00:09:16 +08:00
feat(task): Add init taskDB (#7572)
* feat(task): Add init taskDB * feat(task): Optimize Container Log Reading
This commit is contained in:
parent
8fe40b7a97
commit
e4a191c092
@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -470,34 +469,6 @@ func (b *BaseApi) Inspect(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, result)
|
helper.SuccessWithData(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Container
|
|
||||||
// @Summary Container logs
|
|
||||||
// @Description 容器日志
|
|
||||||
// @Param container query string false "容器名称"
|
|
||||||
// @Param since query string false "时间筛选"
|
|
||||||
// @Param follow query string false "是否追踪"
|
|
||||||
// @Param tail query string false "显示行号"
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Router /containers/search/log [post]
|
|
||||||
func (b *BaseApi) ContainerLogs(c *gin.Context) {
|
|
||||||
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
|
|
||||||
if err != nil {
|
|
||||||
global.LOG.Errorf("gin context http handler failed, err: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer wsConn.Close()
|
|
||||||
|
|
||||||
container := c.Query("container")
|
|
||||||
since := c.Query("since")
|
|
||||||
follow := c.Query("follow") == "true"
|
|
||||||
tail := c.Query("tail")
|
|
||||||
|
|
||||||
if err := containerService.ContainerLogs(wsConn, "container", container, since, tail, follow); err != nil {
|
|
||||||
_ = wsConn.WriteMessage(1, []byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Description 下载容器日志
|
// @Description 下载容器日志
|
||||||
// @Router /containers/download/log [post]
|
// @Router /containers/download/log [post]
|
||||||
func (b *BaseApi) DownloadContainerLogs(c *gin.Context) {
|
func (b *BaseApi) DownloadContainerLogs(c *gin.Context) {
|
||||||
@ -707,30 +678,38 @@ func (b *BaseApi) ComposeUpdate(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, nil)
|
helper.SuccessWithData(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Container Compose
|
// @Tags Container
|
||||||
// @Summary Container Compose logs
|
// @Summary Container logs
|
||||||
// @Description docker-compose 日志
|
// @Description 容器日志
|
||||||
// @Param compose query string false "compose 文件地址"
|
// @Param container query string false "容器名称"
|
||||||
// @Param since query string false "时间筛选"
|
// @Param since query string false "时间筛选"
|
||||||
// @Param follow query string false "是否追踪"
|
// @Param follow query string false "是否追踪"
|
||||||
// @Param tail query string false "显示行号"
|
// @Param tail query string false "显示行号"
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Router /containers/compose/search/log [get]
|
// @Router /containers/search/log [post]
|
||||||
func (b *BaseApi) ComposeLogs(c *gin.Context) {
|
func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
|
||||||
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
|
c.Header("Content-Type", "text/event-stream")
|
||||||
if err != nil {
|
c.Header("Cache-Control", "no-cache")
|
||||||
global.LOG.Errorf("gin context http handler failed, err: %v", err)
|
c.Header("Connection", "keep-alive")
|
||||||
return
|
c.Header("Transfer-Encoding", "chunked")
|
||||||
}
|
|
||||||
defer wsConn.Close()
|
|
||||||
|
|
||||||
compose := c.Query("compose")
|
|
||||||
since := c.Query("since")
|
since := c.Query("since")
|
||||||
follow := c.Query("follow") == "true"
|
follow := c.Query("follow") == "true"
|
||||||
tail := c.Query("tail")
|
tail := c.Query("tail")
|
||||||
|
|
||||||
if err := containerService.ContainerLogs(wsConn, "compose", compose, since, tail, follow); err != nil {
|
container := c.Query("container")
|
||||||
_ = wsConn.WriteMessage(1, []byte(err.Error()))
|
compose := c.Query("compose")
|
||||||
return
|
streamLog := dto.StreamLog{
|
||||||
|
Compose: compose,
|
||||||
|
Container: container,
|
||||||
|
Since: since,
|
||||||
|
Follow: follow,
|
||||||
|
Tail: tail,
|
||||||
|
Type: "container",
|
||||||
}
|
}
|
||||||
|
if compose != "" {
|
||||||
|
streamLog.Type = "compose"
|
||||||
|
}
|
||||||
|
|
||||||
|
containerService.StreamLogs(c, streamLog)
|
||||||
}
|
}
|
||||||
|
@ -268,3 +268,12 @@ type ContainerLog struct {
|
|||||||
Tail uint `json:"tail"`
|
Tail uint `json:"tail"`
|
||||||
ContainerType string `json:"containerType"`
|
ContainerType string `json:"containerType"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StreamLog struct {
|
||||||
|
Compose string
|
||||||
|
Container string
|
||||||
|
Since string
|
||||||
|
Follow bool
|
||||||
|
Tail string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -19,9 +20,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
@ -44,7 +42,6 @@ import (
|
|||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/shirou/gopsutil/v3/cpu"
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
"github.com/shirou/gopsutil/v3/mem"
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
@ -74,7 +71,6 @@ type IContainerService interface {
|
|||||||
ContainerCommit(req dto.ContainerCommit) error
|
ContainerCommit(req dto.ContainerCommit) error
|
||||||
ContainerLogClean(req dto.OperationWithName) error
|
ContainerLogClean(req dto.OperationWithName) error
|
||||||
ContainerOperation(req dto.ContainerOperation) error
|
ContainerOperation(req dto.ContainerOperation) error
|
||||||
ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) error
|
|
||||||
DownloadContainerLogs(containerType, container, since, tail string, c *gin.Context) error
|
DownloadContainerLogs(containerType, container, since, tail string, c *gin.Context) error
|
||||||
ContainerStats(id string) (*dto.ContainerStats, error)
|
ContainerStats(id string) (*dto.ContainerStats, error)
|
||||||
Inspect(req dto.InspectReq) (string, error)
|
Inspect(req dto.InspectReq) (string, error)
|
||||||
@ -87,6 +83,8 @@ type IContainerService interface {
|
|||||||
Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error)
|
Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error)
|
||||||
|
|
||||||
LoadContainerLogs(req dto.OperationWithNameAndType) string
|
LoadContainerLogs(req dto.OperationWithNameAndType) string
|
||||||
|
|
||||||
|
StreamLogs(ctx *gin.Context, params dto.StreamLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIContainerService() IContainerService {
|
func NewIContainerService() IContainerService {
|
||||||
@ -794,87 +792,87 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) error {
|
func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) {
|
||||||
defer func() { wsConn.Close() }()
|
messageChan := make(chan string, 1024)
|
||||||
if cmd.CheckIllegal(container, since, tail) {
|
errorChan := make(chan error, 1)
|
||||||
return buserr.New(constant.ErrCmdIllegal)
|
|
||||||
}
|
go collectLogs(params, messageChan, errorChan)
|
||||||
commandName := "docker"
|
|
||||||
commandArg := []string{"logs", container}
|
ctx.Stream(func(w io.Writer) bool {
|
||||||
if containerType == "compose" {
|
select {
|
||||||
commandArg = []string{"compose", "-f", container, "logs"}
|
case msg, ok := <-messageChan:
|
||||||
}
|
if !ok {
|
||||||
if tail != "0" {
|
return false
|
||||||
commandArg = append(commandArg, "--tail")
|
}
|
||||||
commandArg = append(commandArg, tail)
|
_, err := fmt.Fprintf(w, "data: %v\n\n", msg)
|
||||||
}
|
if err != nil {
|
||||||
if since != "all" {
|
return false
|
||||||
commandArg = append(commandArg, "--since")
|
}
|
||||||
commandArg = append(commandArg, since)
|
return true
|
||||||
}
|
case err := <-errorChan:
|
||||||
if follow {
|
_, err = fmt.Fprintf(w, "data: {\"event\": \"error\", \"data\": \"%s\"}\n\n", err.Error())
|
||||||
commandArg = append(commandArg, "-f")
|
if err != nil {
|
||||||
}
|
return false
|
||||||
if !follow {
|
}
|
||||||
cmd := exec.Command(commandName, commandArg...)
|
return false
|
||||||
cmd.Stderr = cmd.Stdout
|
case <-ctx.Request.Context().Done():
|
||||||
stdout, _ := cmd.CombinedOutput()
|
return false
|
||||||
if !utf8.Valid(stdout) {
|
}
|
||||||
return errors.New("invalid utf8")
|
})
|
||||||
}
|
}
|
||||||
if err := wsConn.WriteMessage(websocket.TextMessage, stdout); err != nil {
|
|
||||||
global.LOG.Errorf("send message with log to ws failed, err: %v", err)
|
func collectLogs(params dto.StreamLog, messageChan chan<- string, errorChan chan<- error) {
|
||||||
}
|
defer close(messageChan)
|
||||||
return nil
|
defer close(errorChan)
|
||||||
}
|
|
||||||
|
var cmdArgs []string
|
||||||
|
if params.Type == "compose" {
|
||||||
|
cmdArgs = []string{"compose", "-f", params.Compose}
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "logs")
|
||||||
|
if params.Follow {
|
||||||
|
cmdArgs = append(cmdArgs, "-f")
|
||||||
|
}
|
||||||
|
if params.Tail != "all" {
|
||||||
|
cmdArgs = append(cmdArgs, "--tail", params.Tail)
|
||||||
|
}
|
||||||
|
if params.Since != "all" {
|
||||||
|
cmdArgs = append(cmdArgs, "--since", params.Since)
|
||||||
|
}
|
||||||
|
if params.Container != "" {
|
||||||
|
cmdArgs = append(cmdArgs, params.Container)
|
||||||
|
}
|
||||||
|
cmd := exec.Command("docker", cmdArgs...)
|
||||||
|
|
||||||
cmd := exec.Command(commandName, commandArg...)
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
errorChan <- fmt.Errorf("failed to get stdout pipe: %v", err)
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
cmd.Stderr = cmd.Stdout
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
errorChan <- fmt.Errorf("failed to start command: %v", err)
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
exitCh := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
_, wsData, _ := wsConn.ReadMessage()
|
|
||||||
if string(wsData) == "close conn" {
|
|
||||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
exitCh <- struct{}{}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
scanner := bufio.NewScanner(stdout)
|
||||||
buffer := make([]byte, 1024)
|
lineNumber := 0
|
||||||
for {
|
|
||||||
select {
|
for scanner.Scan() {
|
||||||
case <-exitCh:
|
lineNumber++
|
||||||
return
|
message := scanner.Text()
|
||||||
default:
|
select {
|
||||||
n, err := stdout.Read(buffer)
|
case messageChan <- message:
|
||||||
if err != nil {
|
case <-time.After(time.Second):
|
||||||
if err == io.EOF {
|
errorChan <- fmt.Errorf("message channel blocked")
|
||||||
return
|
return
|
||||||
}
|
|
||||||
global.LOG.Errorf("read bytes from log failed, err: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !utf8.Valid(buffer[:n]) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err = wsConn.WriteMessage(websocket.TextMessage, buffer[:n]); err != nil {
|
|
||||||
global.LOG.Errorf("send message with log to ws failed, err: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
_ = cmd.Wait()
|
|
||||||
return nil
|
if err := scanner.Err(); err != nil {
|
||||||
|
errorChan <- fmt.Errorf("scanner error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) DownloadContainerLogs(containerType, container, since, tail string, c *gin.Context) error {
|
func (u *ContainerService) DownloadContainerLogs(containerType, container, since, tail string, c *gin.Context) error {
|
||||||
|
@ -8,6 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
|
InitAgentDB()
|
||||||
|
InitTaskDB()
|
||||||
|
global.LOG.Info("Migration run successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitAgentDB() {
|
||||||
m := gormigrate.New(global.DB, gormigrate.DefaultOptions, []*gormigrate.Migration{
|
m := gormigrate.New(global.DB, gormigrate.DefaultOptions, []*gormigrate.Migration{
|
||||||
migrations.AddTable,
|
migrations.AddTable,
|
||||||
migrations.AddMonitorTable,
|
migrations.AddMonitorTable,
|
||||||
@ -20,5 +26,14 @@ func Init() {
|
|||||||
global.LOG.Error(err)
|
global.LOG.Error(err)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
global.LOG.Info("Migration run successfully")
|
}
|
||||||
|
|
||||||
|
func InitTaskDB() {
|
||||||
|
m := gormigrate.New(global.TaskDB, gormigrate.DefaultOptions, []*gormigrate.Migration{
|
||||||
|
migrations.AddTaskTable,
|
||||||
|
})
|
||||||
|
if err := m.Migrate(); err != nil {
|
||||||
|
global.LOG.Error(err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,3 +217,12 @@ var InitPHPExtensions = &gormigrate.Migration{
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var AddTaskTable = &gormigrate.Migration{
|
||||||
|
ID: "20241226-add-task",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
return tx.AutoMigrate(
|
||||||
|
&model.Task{},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -23,7 +23,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
|||||||
baRouter.POST("/list", baseApi.ListContainer)
|
baRouter.POST("/list", baseApi.ListContainer)
|
||||||
baRouter.GET("/status", baseApi.LoadContainerStatus)
|
baRouter.GET("/status", baseApi.LoadContainerStatus)
|
||||||
baRouter.GET("/list/stats", baseApi.ContainerListStats)
|
baRouter.GET("/list/stats", baseApi.ContainerListStats)
|
||||||
baRouter.GET("/search/log", baseApi.ContainerLogs)
|
baRouter.GET("/search/log", baseApi.ContainerStreamLogs)
|
||||||
baRouter.POST("/download/log", baseApi.DownloadContainerLogs)
|
baRouter.POST("/download/log", baseApi.DownloadContainerLogs)
|
||||||
baRouter.GET("/limit", baseApi.LoadResourceLimit)
|
baRouter.GET("/limit", baseApi.LoadResourceLimit)
|
||||||
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
|
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
|
||||||
@ -46,7 +46,6 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
|||||||
baRouter.POST("/compose/test", baseApi.TestCompose)
|
baRouter.POST("/compose/test", baseApi.TestCompose)
|
||||||
baRouter.POST("/compose/operate", baseApi.OperatorCompose)
|
baRouter.POST("/compose/operate", baseApi.OperatorCompose)
|
||||||
baRouter.POST("/compose/update", baseApi.ComposeUpdate)
|
baRouter.POST("/compose/update", baseApi.ComposeUpdate)
|
||||||
baRouter.GET("/compose/search/log", baseApi.ComposeLogs)
|
|
||||||
|
|
||||||
baRouter.GET("/template", baseApi.ListComposeTemplate)
|
baRouter.GET("/template", baseApi.ListComposeTemplate)
|
||||||
baRouter.POST("/template/search", baseApi.SearchComposeTemplate)
|
baRouter.POST("/template/search", baseApi.SearchComposeTemplate)
|
||||||
|
@ -55,7 +55,8 @@
|
|||||||
"vue-codemirror": "^6.1.1",
|
"vue-codemirror": "^6.1.1",
|
||||||
"vue-demi": "^0.14.6",
|
"vue-demi": "^0.14.6",
|
||||||
"vue-i18n": "^9.13.1",
|
"vue-i18n": "^9.13.1",
|
||||||
"vue-router": "^4.3.3"
|
"vue-router": "^4.3.3",
|
||||||
|
"vue-virtual-scroller": "^2.0.0-beta.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.14.8",
|
"@types/node": "^20.14.8",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<DrawerPro
|
<DrawerPro
|
||||||
v-model="open"
|
v-model="open"
|
||||||
:header="$t('commons.button.log')"
|
:header="resource"
|
||||||
:back="handleClose"
|
:back="handleClose"
|
||||||
:size="globalStore.isFullScreen ? 'full' : 'large'"
|
:size="globalStore.isFullScreen ? 'full' : 'large'"
|
||||||
:resource="resource"
|
:resource="resource"
|
||||||
@ -12,72 +12,34 @@
|
|||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex flex-wrap">
|
<ContainerLog :compose="compose" />
|
||||||
<el-select @change="searchLogs" v-model="logSearch.mode" class="selectWidth">
|
|
||||||
<template #prefix>{{ $t('container.fetch') }}</template>
|
|
||||||
<el-option v-for="item in timeOptions" :key="item.label" :value="item.value" :label="item.label" />
|
|
||||||
</el-select>
|
|
||||||
<el-select @change="searchLogs" class="ml-5 selectWidth" v-model.number="logSearch.tail">
|
|
||||||
<template #prefix>{{ $t('container.lines') }}</template>
|
|
||||||
<el-option :value="0" :label="$t('commons.table.all')" />
|
|
||||||
<el-option :value="100" :label="100" />
|
|
||||||
<el-option :value="200" :label="200" />
|
|
||||||
<el-option :value="500" :label="500" />
|
|
||||||
<el-option :value="1000" :label="1000" />
|
|
||||||
</el-select>
|
|
||||||
<div class="ml-5">
|
|
||||||
<el-checkbox border @change="searchLogs" v-model="logSearch.isWatch">
|
|
||||||
{{ $t('commons.button.watch') }}
|
|
||||||
</el-checkbox>
|
|
||||||
</div>
|
|
||||||
<el-button class="ml-5" @click="onDownload" icon="Download">
|
|
||||||
{{ $t('file.download') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2.5">
|
|
||||||
<highlightjs
|
|
||||||
ref="editorRef"
|
|
||||||
class="editor-main"
|
|
||||||
language="JavaScript"
|
|
||||||
:autodetect="false"
|
|
||||||
:code="logInfo"
|
|
||||||
></highlightjs>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</DrawerPro>
|
</DrawerPro>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { dateFormatForName } from '@/utils/util';
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
|
||||||
import { MsgError } from '@/utils/message';
|
|
||||||
import { GlobalStore } from '@/store';
|
import { GlobalStore } from '@/store';
|
||||||
import screenfull from 'screenfull';
|
import screenfull from 'screenfull';
|
||||||
import { DownloadFile } from '@/api/modules/container';
|
import ContainerLog from '@/components/container-log/index.vue';
|
||||||
|
|
||||||
const logInfo = ref('');
|
|
||||||
const logSocket = ref<WebSocket>();
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
const resource = ref('');
|
const resource = ref('');
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
const logVisible = ref(false);
|
const logVisible = ref(false);
|
||||||
const editorRef = ref();
|
const compose = ref('');
|
||||||
const scrollerElement = ref<HTMLElement | null>(null);
|
|
||||||
|
interface DialogProps {
|
||||||
|
compose: string;
|
||||||
|
resource: string;
|
||||||
|
}
|
||||||
|
|
||||||
const mobile = computed(() => {
|
const mobile = computed(() => {
|
||||||
return globalStore.isMobile();
|
return globalStore.isMobile();
|
||||||
});
|
});
|
||||||
|
|
||||||
const logSearch = reactive({
|
|
||||||
isWatch: true,
|
|
||||||
compose: '',
|
|
||||||
mode: 'all',
|
|
||||||
tail: 500,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
logSocket.value?.send('close conn');
|
|
||||||
open.value = false;
|
open.value = false;
|
||||||
globalStore.isFullScreen = false;
|
globalStore.isFullScreen = false;
|
||||||
};
|
};
|
||||||
@ -93,104 +55,10 @@ watch(logVisible, (val) => {
|
|||||||
if (screenfull.isEnabled && !val && !mobile.value) screenfull.exit();
|
if (screenfull.isEnabled && !val && !mobile.value) screenfull.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeOptions = ref([
|
|
||||||
{ label: i18n.global.t('container.all'), value: 'all' },
|
|
||||||
{
|
|
||||||
label: i18n.global.t('container.lastDay'),
|
|
||||||
value: new Date(new Date().getTime() - 3600 * 1000 * 24 * 1).getTime() / 1000 + '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('container.last4Hour'),
|
|
||||||
value: new Date(new Date().getTime() - 3600 * 1000 * 4).getTime() / 1000 + '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('container.lastHour'),
|
|
||||||
value: new Date(new Date().getTime() - 3600 * 1000).getTime() / 1000 + '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('container.last10Min'),
|
|
||||||
value: new Date(new Date().getTime() - 600 * 1000).getTime() / 1000 + '',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const searchLogs = async () => {
|
|
||||||
if (Number(logSearch.tail) < 0) {
|
|
||||||
MsgError(i18n.global.t('container.linesHelper'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logSocket.value?.send('close conn');
|
|
||||||
logSocket.value?.close();
|
|
||||||
logInfo.value = '';
|
|
||||||
const href = window.location.href;
|
|
||||||
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
|
|
||||||
const host = href.split('//')[1].split('/')[0];
|
|
||||||
logSocket.value = new WebSocket(
|
|
||||||
`${protocol}://${host}/api/v2/containers/compose/search/log?compose=${logSearch.compose}&since=${logSearch.mode}&tail=${logSearch.tail}&follow=${logSearch.isWatch}`,
|
|
||||||
);
|
|
||||||
logSocket.value.onmessage = (event) => {
|
|
||||||
logInfo.value += event.data;
|
|
||||||
nextTick(() => {
|
|
||||||
scrollerElement.value.scrollTop = scrollerElement.value.scrollHeight;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDownload = async () => {
|
|
||||||
let msg =
|
|
||||||
logSearch.tail === 0
|
|
||||||
? i18n.global.t('app.downloadLogHelper1', [resource.value])
|
|
||||||
: i18n.global.t('app.downloadLogHelper2', [resource.value, logSearch.tail]);
|
|
||||||
ElMessageBox.confirm(msg, i18n.global.t('file.download'), {
|
|
||||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
|
||||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
|
||||||
type: 'info',
|
|
||||||
}).then(async () => {
|
|
||||||
let params = {
|
|
||||||
container: logSearch.compose,
|
|
||||||
since: logSearch.mode,
|
|
||||||
tail: logSearch.tail,
|
|
||||||
containerType: 'compose',
|
|
||||||
};
|
|
||||||
let addItem = {};
|
|
||||||
addItem['name'] = logSearch.compose + '-' + dateFormatForName(new Date()) + '.log';
|
|
||||||
DownloadFile(params).then((res) => {
|
|
||||||
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.style.display = 'none';
|
|
||||||
a.href = downloadUrl;
|
|
||||||
a.download = addItem['name'];
|
|
||||||
const event = new MouseEvent('click');
|
|
||||||
a.dispatchEvent(event);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DialogProps {
|
|
||||||
compose: string;
|
|
||||||
resource: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const acceptParams = (props: DialogProps): void => {
|
const acceptParams = (props: DialogProps): void => {
|
||||||
logSearch.compose = props.compose;
|
compose.value = props.compose;
|
||||||
logSearch.tail = 200;
|
|
||||||
logSearch.mode = timeOptions.value[3].value;
|
|
||||||
logSearch.isWatch = true;
|
|
||||||
resource.value = props.resource;
|
resource.value = props.resource;
|
||||||
open.value = true;
|
open.value = true;
|
||||||
if (!mobile.value) {
|
|
||||||
screenfull.on('change', () => {
|
|
||||||
globalStore.isFullScreen = screenfull.isFullscreen;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
nextTick(() => {
|
|
||||||
if (editorRef.value) {
|
|
||||||
searchLogs();
|
|
||||||
scrollerElement.value = editorRef.value.$el as HTMLElement;
|
|
||||||
let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement;
|
|
||||||
hljsDom.style['min-height'] = '300px';
|
|
||||||
hljsDom.style['flex-grow'] = 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@ -206,13 +74,4 @@ defineExpose({
|
|||||||
.fullScreen {
|
.fullScreen {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
.selectWidth {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 80vh;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,120 +1,158 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-wrap">
|
<el-select @change="searchLogs" class="fetchClass" v-model="logSearch.mode">
|
||||||
<el-select @change="searchLogs" v-model="logSearch.mode" class="selectWidth">
|
<template #prefix>{{ $t('container.fetch') }}</template>
|
||||||
<template #prefix>{{ $t('container.fetch') }}</template>
|
<el-option v-for="item in timeOptions" :key="item.label" :value="item.value" :label="item.label" />
|
||||||
<el-option v-for="item in timeOptions" :key="item.label" :value="item.value" :label="item.label" />
|
</el-select>
|
||||||
</el-select>
|
<el-select @change="searchLogs" class="tailClass" v-model.number="logSearch.tail">
|
||||||
<el-select @change="searchLogs" class="margin-button selectWidth" v-model.number="logSearch.tail">
|
<template #prefix>{{ $t('container.lines') }}</template>
|
||||||
<template #prefix>{{ $t('container.lines') }}</template>
|
<el-option :value="0" :label="$t('commons.table.all')" />
|
||||||
<el-option :value="0" :label="$t('commons.table.all')" />
|
<el-option :value="100" :label="100" />
|
||||||
<el-option :value="100" :label="100" />
|
<el-option :value="200" :label="200" />
|
||||||
<el-option :value="200" :label="200" />
|
<el-option :value="500" :label="500" />
|
||||||
<el-option :value="500" :label="500" />
|
<el-option :value="1000" :label="1000" />
|
||||||
<el-option :value="1000" :label="1000" />
|
</el-select>
|
||||||
</el-select>
|
<div class="margin-button float-left">
|
||||||
<div class="margin-button">
|
<el-checkbox border @change="searchLogs" v-model="logSearch.isWatch">
|
||||||
<el-checkbox border @change="searchLogs" v-model="logSearch.isWatch">
|
{{ $t('commons.button.watch') }}
|
||||||
{{ $t('commons.button.watch') }}
|
</el-checkbox>
|
||||||
</el-checkbox>
|
|
||||||
</div>
|
|
||||||
<el-button class="margin-button" @click="onDownload" icon="Download">
|
|
||||||
{{ $t('file.download') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button class="margin-button" @click="onClean" icon="Delete">
|
|
||||||
{{ $t('commons.button.clean') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
<LogPro v-model="logInfo" :heightDiff="400" />
|
<el-button class="margin-button" @click="onDownload" icon="Download">
|
||||||
|
{{ $t('file.download') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button class="margin-button" @click="onClean" icon="Delete">
|
||||||
|
{{ $t('commons.button.clean') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="log-container" ref="logContainer">
|
||||||
|
<DynamicScroller :items="logs" :min-item-size="32" v-if="logs.length">
|
||||||
|
<template #default="{ item, active }">
|
||||||
|
<DynamicScrollerItem
|
||||||
|
:item="item"
|
||||||
|
:active="active"
|
||||||
|
class="msgBox"
|
||||||
|
:size-dependencies="[item]"
|
||||||
|
:data-index="item"
|
||||||
|
>
|
||||||
|
<span class="log-item">{{ item }}</span>
|
||||||
|
</DynamicScrollerItem>
|
||||||
|
</template>
|
||||||
|
</DynamicScroller>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cleanContainerLog } from '@/api/modules/container';
|
import { cleanContainerLog, DownloadFile } from '@/api/modules/container';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { dateFormatForName, downloadWithContent } from '@/utils/util';
|
import { dateFormatForName } from '@/utils/util';
|
||||||
import { onBeforeUnmount, reactive, ref } from 'vue';
|
import { onUnmounted, reactive, ref } from 'vue';
|
||||||
|
import { ElMessageBox } from 'element-plus';
|
||||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||||
import LogPro from '@/components/log-pro/index.vue';
|
|
||||||
|
|
||||||
const logInfo = ref();
|
const props = defineProps({
|
||||||
const terminalSocket = ref<WebSocket>();
|
container: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
compose: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const logVisible = ref(false);
|
||||||
|
const logContainer = ref<HTMLElement | null>(null);
|
||||||
|
const logs = ref<string[]>([]);
|
||||||
|
let eventSource: EventSource | null = null;
|
||||||
const logSearch = reactive({
|
const logSearch = reactive({
|
||||||
isWatch: false,
|
isWatch: true,
|
||||||
container: '',
|
container: '',
|
||||||
containerID: '',
|
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
tail: 100,
|
tail: 100,
|
||||||
|
compose: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeOptions = ref([
|
const timeOptions = ref([
|
||||||
{ label: i18n.global.t('container.all'), value: 'all' },
|
{ label: i18n.global.t('container.all'), value: 'all' },
|
||||||
{
|
{
|
||||||
label: i18n.global.t('container.lastDay'),
|
label: i18n.global.t('container.lastDay'),
|
||||||
value: new Date(new Date().getTime() - 3600 * 1000 * 24 * 1).getTime() / 1000 + '',
|
value: '24h',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.global.t('container.last4Hour'),
|
label: i18n.global.t('container.last4Hour'),
|
||||||
value: new Date(new Date().getTime() - 3600 * 1000 * 4).getTime() / 1000 + '',
|
value: '4h',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.global.t('container.lastHour'),
|
label: i18n.global.t('container.lastHour'),
|
||||||
value: new Date(new Date().getTime() - 3600 * 1000).getTime() / 1000 + '',
|
value: '1h',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.global.t('container.last10Min'),
|
label: i18n.global.t('container.last10Min'),
|
||||||
value: new Date(new Date().getTime() - 600 * 1000).getTime() / 1000 + '',
|
value: '10m',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const stopListening = () => {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = async () => {
|
||||||
|
stopListening();
|
||||||
|
logVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const searchLogs = async () => {
|
const searchLogs = async () => {
|
||||||
if (Number(logSearch.tail) < 0) {
|
if (Number(logSearch.tail) < 0) {
|
||||||
MsgError(i18n.global.t('container.linesHelper'));
|
MsgError(i18n.global.t('container.linesHelper'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminalSocket.value?.send('close conn');
|
logs.value = [];
|
||||||
terminalSocket.value?.close();
|
let url = `/api/v2/containers/search/log?container=${logSearch.container}&since=${logSearch.mode}&tail=${logSearch.tail}&follow=${logSearch.isWatch}`;
|
||||||
logInfo.value = '';
|
if (logSearch.compose !== '') {
|
||||||
const href = window.location.href;
|
url = `/api/v2/containers/search/log?compose=${logSearch.compose}&since=${logSearch.mode}&tail=${logSearch.tail}&follow=${logSearch.isWatch}`;
|
||||||
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
|
}
|
||||||
const host = href.split('//')[1].split('/')[0];
|
eventSource = new EventSource(url);
|
||||||
terminalSocket.value = new WebSocket(
|
eventSource.onmessage = (event: MessageEvent) => {
|
||||||
`${protocol}://${host}/api/v2/containers/search/log?container=${logSearch.containerID}&since=${logSearch.mode}&tail=${logSearch.tail}&follow=${logSearch.isWatch}`,
|
const data = event.data;
|
||||||
);
|
logs.value.push(data);
|
||||||
terminalSocket.value.onmessage = (event) => {
|
nextTick(() => {
|
||||||
logInfo.value += event.data;
|
if (logContainer.value) {
|
||||||
|
logContainer.value.scrollTop = logContainer.value.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDownload = async () => {
|
const onDownload = async () => {
|
||||||
let msg =
|
logSearch.tail = 0;
|
||||||
logSearch.tail === 0
|
let msg = i18n.global.t('container.downLogHelper1', [logSearch.container]);
|
||||||
? i18n.global.t('container.downLogHelper1', [logSearch.container])
|
|
||||||
: i18n.global.t('container.downLogHelper2', [logSearch.container, logSearch.tail]);
|
|
||||||
ElMessageBox.confirm(msg, i18n.global.t('file.download'), {
|
ElMessageBox.confirm(msg, i18n.global.t('file.download'), {
|
||||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||||
type: 'info',
|
type: 'info',
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
downloadWithContent(logInfo.value, logSearch.container + '-' + dateFormatForName(new Date()) + '.log');
|
let params = {
|
||||||
|
container: logSearch.container,
|
||||||
|
since: logSearch.mode,
|
||||||
|
tail: logSearch.tail,
|
||||||
|
containerType: 'container',
|
||||||
|
};
|
||||||
|
let addItem = {};
|
||||||
|
addItem['name'] = logSearch.container + '-' + dateFormatForName(new Date()) + '.log';
|
||||||
|
DownloadFile(params).then((res) => {
|
||||||
|
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.style.display = 'none';
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = addItem['name'];
|
||||||
|
const event = new MouseEvent('click');
|
||||||
|
a.dispatchEvent(event);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DialogProps {
|
|
||||||
container: string;
|
|
||||||
containerID: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const acceptParams = (props: DialogProps): void => {
|
|
||||||
logSearch.containerID = props.containerID;
|
|
||||||
logSearch.tail = 100;
|
|
||||||
logSearch.mode = 'all';
|
|
||||||
logSearch.isWatch = false;
|
|
||||||
logSearch.container = props.container;
|
|
||||||
searchLogs();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClean = async () => {
|
const onClean = async () => {
|
||||||
ElMessageBox.confirm(i18n.global.t('container.cleanLogHelper'), i18n.global.t('container.cleanLog'), {
|
ElMessageBox.confirm(i18n.global.t('container.cleanLogHelper'), i18n.global.t('container.cleanLog'), {
|
||||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
@ -127,12 +165,20 @@ const onClean = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
terminalSocket.value?.send('close conn');
|
handleClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
onMounted(() => {
|
||||||
acceptParams,
|
logSearch.container = props.container;
|
||||||
|
logSearch.compose = props.compose;
|
||||||
|
|
||||||
|
logVisible.value = true;
|
||||||
|
logSearch.tail = 100;
|
||||||
|
logSearch.mode = 'all';
|
||||||
|
logSearch.isWatch = true;
|
||||||
|
|
||||||
|
searchLogs();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -140,13 +186,34 @@ defineExpose({
|
|||||||
.margin-button {
|
.margin-button {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
.selectWidth {
|
.fullScreen {
|
||||||
width: 150px;
|
border: none;
|
||||||
}
|
}
|
||||||
.editor-main {
|
.tailClass {
|
||||||
height: calc(100vh - 480px);
|
width: 20%;
|
||||||
|
float: left;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
.fetchClass {
|
||||||
|
width: 30%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container {
|
||||||
|
height: calc(100vh - 405px);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
position: relative;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 600px;
|
padding: 2px;
|
||||||
overflow: auto;
|
color: #f5f5f5;
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -218,7 +218,6 @@ const getContent = async (pre: boolean) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
console.log('pre', pre);
|
|
||||||
if (pre) {
|
if (pre) {
|
||||||
logContainer.value.scrollTop = 2000;
|
logContainer.value.scrollTop = 2000;
|
||||||
} else {
|
} else {
|
||||||
|
@ -43,9 +43,12 @@ const open = ref(false);
|
|||||||
const showTail = ref(true);
|
const showTail = ref(true);
|
||||||
|
|
||||||
const openWithTaskID = (id: string, tail: boolean) => {
|
const openWithTaskID = (id: string, tail: boolean) => {
|
||||||
|
console.log('openWithTaskID', id, tail);
|
||||||
config.taskID = id;
|
config.taskID = id;
|
||||||
if (!tail) {
|
if (!tail) {
|
||||||
config.tail = false;
|
config.tail = false;
|
||||||
|
} else {
|
||||||
|
config.tail = true;
|
||||||
}
|
}
|
||||||
open.value = true;
|
open.value = true;
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,9 @@ import Fit2CloudPlus from 'fit2cloud-ui-plus';
|
|||||||
import * as Icons from '@element-plus/icons-vue';
|
import * as Icons from '@element-plus/icons-vue';
|
||||||
import hljsVuePlugin from '@highlightjs/vue-plugin';
|
import hljsVuePlugin from '@highlightjs/vue-plugin';
|
||||||
|
|
||||||
|
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
|
||||||
|
import VirtualScroller from 'vue-virtual-scroller';
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
app.use(hljsVuePlugin);
|
app.use(hljsVuePlugin);
|
||||||
app.component('SvgIcon', SvgIcon);
|
app.component('SvgIcon', SvgIcon);
|
||||||
@ -35,6 +38,7 @@ Object.keys(Icons).forEach((key) => {
|
|||||||
app.component(key, Icons[key as keyof typeof Icons]);
|
app.component(key, Icons[key as keyof typeof Icons]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(VirtualScroller);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
|
@ -12,32 +12,7 @@
|
|||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div>
|
<ContainerLog :container="config.container" />
|
||||||
<el-select @change="searchLogs" class="fetchClass" v-model="logSearch.mode">
|
|
||||||
<template #prefix>{{ $t('container.fetch') }}</template>
|
|
||||||
<el-option v-for="item in timeOptions" :key="item.label" :value="item.value" :label="item.label" />
|
|
||||||
</el-select>
|
|
||||||
<el-select @change="searchLogs" class="tailClass" v-model.number="logSearch.tail">
|
|
||||||
<template #prefix>{{ $t('container.lines') }}</template>
|
|
||||||
<el-option :value="0" :label="$t('commons.table.all')" />
|
|
||||||
<el-option :value="100" :label="100" />
|
|
||||||
<el-option :value="200" :label="200" />
|
|
||||||
<el-option :value="500" :label="500" />
|
|
||||||
<el-option :value="1000" :label="1000" />
|
|
||||||
</el-select>
|
|
||||||
<div class="margin-button float-left">
|
|
||||||
<el-checkbox border @change="searchLogs" v-model="logSearch.isWatch">
|
|
||||||
{{ $t('commons.button.watch') }}
|
|
||||||
</el-checkbox>
|
|
||||||
</div>
|
|
||||||
<el-button class="margin-button" @click="onDownload" icon="Download">
|
|
||||||
{{ $t('file.download') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button class="margin-button" @click="onClean" icon="Delete">
|
|
||||||
{{ $t('commons.button.clean') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
<LogPro v-model="logInfo"></LogPro>
|
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
@ -48,25 +23,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cleanContainerLog, DownloadFile } from '@/api/modules/container';
|
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { dateFormatForName } from '@/utils/util';
|
|
||||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
||||||
import { ElMessageBox } from 'element-plus';
|
|
||||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
|
||||||
import screenfull from 'screenfull';
|
import screenfull from 'screenfull';
|
||||||
import { GlobalStore } from '@/store';
|
import { GlobalStore } from '@/store';
|
||||||
import LogPro from '@/components/log-pro/index.vue';
|
|
||||||
|
|
||||||
const logVisible = ref(false);
|
const logVisible = ref(false);
|
||||||
const mobile = computed(() => {
|
const mobile = computed(() => {
|
||||||
return globalStore.isMobile();
|
return globalStore.isMobile();
|
||||||
});
|
});
|
||||||
|
|
||||||
const logInfo = ref<string>('');
|
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
const terminalSocket = ref<WebSocket>();
|
|
||||||
|
|
||||||
const logSearch = reactive({
|
const logSearch = reactive({
|
||||||
isWatch: true,
|
isWatch: true,
|
||||||
container: '',
|
container: '',
|
||||||
@ -75,26 +41,6 @@ const logSearch = reactive({
|
|||||||
tail: 100,
|
tail: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeOptions = ref([
|
|
||||||
{ label: i18n.global.t('container.all'), value: 'all' },
|
|
||||||
{
|
|
||||||
label: i18n.global.t('container.lastDay'),
|
|
||||||
value: '24h',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('container.last4Hour'),
|
|
||||||
value: '4h',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('container.lastHour'),
|
|
||||||
value: '1h',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('container.last10Min'),
|
|
||||||
value: '10m',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
function toggleFullscreen() {
|
function toggleFullscreen() {
|
||||||
globalStore.isFullScreen = !globalStore.isFullScreen;
|
globalStore.isFullScreen = !globalStore.isFullScreen;
|
||||||
}
|
}
|
||||||
@ -102,86 +48,30 @@ function toggleFullscreen() {
|
|||||||
const loadTooltip = () => {
|
const loadTooltip = () => {
|
||||||
return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen'));
|
return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = async () => {
|
const handleClose = async () => {
|
||||||
terminalSocket.value?.send('close conn');
|
|
||||||
logVisible.value = false;
|
logVisible.value = false;
|
||||||
globalStore.isFullScreen = false;
|
globalStore.isFullScreen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(logVisible, (val) => {
|
watch(logVisible, (val) => {
|
||||||
if (screenfull.isEnabled && !val && !mobile.value) screenfull.exit();
|
if (screenfull.isEnabled && !val && !mobile.value) screenfull.exit();
|
||||||
});
|
});
|
||||||
const searchLogs = async () => {
|
|
||||||
if (Number(logSearch.tail) < 0) {
|
|
||||||
MsgError(i18n.global.t('container.linesHelper'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
terminalSocket.value?.send('close conn');
|
|
||||||
terminalSocket.value?.close();
|
|
||||||
logInfo.value = '';
|
|
||||||
const href = window.location.href;
|
|
||||||
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
|
|
||||||
const host = href.split('//')[1].split('/')[0];
|
|
||||||
terminalSocket.value = new WebSocket(
|
|
||||||
`${protocol}://${host}/api/v2/containers/search/log?container=${logSearch.containerID}&since=${logSearch.mode}&tail=${logSearch.tail}&follow=${logSearch.isWatch}`,
|
|
||||||
);
|
|
||||||
terminalSocket.value.onmessage = (event) => {
|
|
||||||
logInfo.value += event.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDownload = async () => {
|
|
||||||
logSearch.tail = 0;
|
|
||||||
let msg = i18n.global.t('container.downLogHelper1', [logSearch.container]);
|
|
||||||
ElMessageBox.confirm(msg, i18n.global.t('file.download'), {
|
|
||||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
|
||||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
|
||||||
type: 'info',
|
|
||||||
}).then(async () => {
|
|
||||||
let params = {
|
|
||||||
container: logSearch.containerID,
|
|
||||||
since: logSearch.mode,
|
|
||||||
tail: logSearch.tail,
|
|
||||||
containerType: 'container',
|
|
||||||
};
|
|
||||||
let addItem = {};
|
|
||||||
addItem['name'] = logSearch.container + '-' + dateFormatForName(new Date()) + '.log';
|
|
||||||
DownloadFile(params).then((res) => {
|
|
||||||
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.style.display = 'none';
|
|
||||||
a.href = downloadUrl;
|
|
||||||
a.download = addItem['name'];
|
|
||||||
const event = new MouseEvent('click');
|
|
||||||
a.dispatchEvent(event);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClean = async () => {
|
|
||||||
ElMessageBox.confirm(i18n.global.t('container.cleanLogHelper'), i18n.global.t('container.cleanLog'), {
|
|
||||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
|
||||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
|
||||||
type: 'info',
|
|
||||||
}).then(async () => {
|
|
||||||
await cleanContainerLog(logSearch.container);
|
|
||||||
searchLogs();
|
|
||||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DialogProps {
|
interface DialogProps {
|
||||||
container: string;
|
container: string;
|
||||||
containerID: string;
|
containerID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = ref<DialogProps>({
|
||||||
|
container: '',
|
||||||
|
containerID: '',
|
||||||
|
});
|
||||||
|
|
||||||
const acceptParams = (props: DialogProps): void => {
|
const acceptParams = (props: DialogProps): void => {
|
||||||
|
config.value.containerID = props.containerID;
|
||||||
|
config.value.container = props.container;
|
||||||
logVisible.value = true;
|
logVisible.value = true;
|
||||||
logSearch.containerID = props.containerID;
|
|
||||||
logSearch.tail = 100;
|
|
||||||
logSearch.mode = 'all';
|
|
||||||
logSearch.isWatch = true;
|
|
||||||
logSearch.container = props.container;
|
|
||||||
searchLogs();
|
|
||||||
|
|
||||||
if (!mobile.value) {
|
if (!mobile.value) {
|
||||||
screenfull.on('change', () => {
|
screenfull.on('change', () => {
|
||||||
@ -200,26 +90,7 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.margin-button {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
.fullScreen {
|
.fullScreen {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
.tailClass {
|
|
||||||
width: 20%;
|
|
||||||
float: left;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
.fetchClass {
|
|
||||||
width: 30%;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-main {
|
|
||||||
height: calc(100vh - 250px);
|
|
||||||
width: 100%;
|
|
||||||
min-height: 600px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -86,7 +86,7 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<ContainerLog v-if="activeName === 'log'" ref="dialogContainerLogRef" />
|
<ContainerLog v-if="activeName === 'log'" :container="containerID" />
|
||||||
<SlowLog
|
<SlowLog
|
||||||
@loading="changeLoading"
|
@loading="changeLoading"
|
||||||
@refresh="loadBaseInfo"
|
@refresh="loadBaseInfo"
|
||||||
@ -155,6 +155,7 @@ const mysqlName = ref();
|
|||||||
const mysqlStatus = ref();
|
const mysqlStatus = ref();
|
||||||
const mysqlVersion = ref();
|
const mysqlVersion = ref();
|
||||||
const variables = ref();
|
const variables = ref();
|
||||||
|
const containerID = ref('');
|
||||||
|
|
||||||
interface DBProps {
|
interface DBProps {
|
||||||
type: string;
|
type: string;
|
||||||
@ -165,7 +166,6 @@ const props = withDefaults(defineProps<DBProps>(), {
|
|||||||
database: '',
|
database: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const dialogContainerLogRef = ref();
|
|
||||||
const jumpToConf = async () => {
|
const jumpToConf = async () => {
|
||||||
activeName.value = 'conf';
|
activeName.value = 'conf';
|
||||||
loadMysqlConf();
|
loadMysqlConf();
|
||||||
@ -275,8 +275,8 @@ const onSaveConf = async () => {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadContainerLog = async (containerID: string) => {
|
const loadContainerLog = async (conID: string) => {
|
||||||
dialogContainerLogRef.value!.acceptParams({ containerID: containerID, container: containerID });
|
containerID.value = conID;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadBaseInfo = async () => {
|
const loadBaseInfo = async () => {
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<ContainerLog v-show="activeName === 'log'" ref="dialogContainerLogRef" />
|
<ContainerLog v-if="activeName === 'log'" :container="containerID" />
|
||||||
</template>
|
</template>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ const props = withDefaults(defineProps<DBProps>(), {
|
|||||||
database: '',
|
database: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const dialogContainerLogRef = ref();
|
const containerID = ref('');
|
||||||
const jumpToConf = async () => {
|
const jumpToConf = async () => {
|
||||||
activeName.value = 'conf';
|
activeName.value = 'conf';
|
||||||
loadPostgresqlConf();
|
loadPostgresqlConf();
|
||||||
@ -181,8 +181,8 @@ const onSaveConf = async () => {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadContainerLog = async (containerID: string) => {
|
const loadContainerLog = async (conID: string) => {
|
||||||
dialogContainerLogRef.value!.acceptParams({ containerID: containerID, container: containerID });
|
containerID.value = conID;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadBaseInfo = async () => {
|
const loadBaseInfo = async () => {
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<Status v-if="activeName === '1'" :status="status" />
|
<Status v-if="activeName === '1'" :status="status" />
|
||||||
<Source v-if="activeName === '2'" />
|
<Source v-if="activeName === '2'" />
|
||||||
<NginxPer v-if="activeName === '3'" />
|
<NginxPer v-if="activeName === '3'" />
|
||||||
<ContainerLog v-if="activeName === '4'" ref="dialogContainerLogRef" />
|
<ContainerLog v-if="activeName === '4'" :container="containerName" />
|
||||||
<Module v-if="activeName === '5'" />
|
<Module v-if="activeName === '5'" />
|
||||||
</template>
|
</template>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
@ -39,14 +39,13 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import Source from './source/index.vue';
|
import Source from './source/index.vue';
|
||||||
import { nextTick, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import ContainerLog from '@/components/container-log/index.vue';
|
import ContainerLog from '@/components/container-log/index.vue';
|
||||||
import NginxPer from './performance/index.vue';
|
import NginxPer from './performance/index.vue';
|
||||||
import Status from './status/index.vue';
|
import Status from './status/index.vue';
|
||||||
import Module from './module/index.vue';
|
import Module from './module/index.vue';
|
||||||
|
|
||||||
const activeName = ref('1');
|
const activeName = ref('1');
|
||||||
const dialogContainerLogRef = ref();
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
containerName: {
|
containerName: {
|
||||||
@ -60,15 +59,6 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
const changeTab = (index: string) => {
|
const changeTab = (index: string) => {
|
||||||
activeName.value = index;
|
activeName.value = index;
|
||||||
|
|
||||||
if (index === '4') {
|
|
||||||
nextTick(() => {
|
|
||||||
dialogContainerLogRef.value!.acceptParams({
|
|
||||||
containerID: props.containerName,
|
|
||||||
container: props.containerName,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user