feat: support real-time command monitoring
This commit is contained in:
parent
665f8801ca
commit
10ec866037
|
@ -0,0 +1,174 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"tinyrdm/backend/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type monitorItem struct {
|
||||||
|
client *redis.Client
|
||||||
|
cmd *redis.MonitorCmd
|
||||||
|
ch chan string
|
||||||
|
closeCh chan struct{}
|
||||||
|
eventName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type monitorService struct {
|
||||||
|
ctx context.Context
|
||||||
|
ctxCancel context.CancelFunc
|
||||||
|
mutex sync.Mutex
|
||||||
|
items map[string]*monitorItem
|
||||||
|
}
|
||||||
|
|
||||||
|
var monitor *monitorService
|
||||||
|
var onceMonitor sync.Once
|
||||||
|
|
||||||
|
func Monitor() *monitorService {
|
||||||
|
if monitor == nil {
|
||||||
|
onceMonitor.Do(func() {
|
||||||
|
monitor = &monitorService{
|
||||||
|
items: map[string]*monitorItem{},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return monitor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *monitorService) getItem(server string) (*monitorItem, error) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
item, ok := c.items[server]
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
conf := Connection().getConnection(server)
|
||||||
|
if conf == nil {
|
||||||
|
return nil, fmt.Errorf("no connection profile named: %s", server)
|
||||||
|
}
|
||||||
|
var uniClient redis.UniversalClient
|
||||||
|
if uniClient, err = Connection().createRedisClient(conf.ConnectionConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var client *redis.Client
|
||||||
|
if client, ok = uniClient.(*redis.Client); !ok {
|
||||||
|
return nil, errors.New("create redis client fail")
|
||||||
|
}
|
||||||
|
item = &monitorItem{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
c.items[server] = item
|
||||||
|
}
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *monitorService) Start(ctx context.Context) {
|
||||||
|
c.ctx, c.ctxCancel = context.WithCancel(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartMonitor start a monitor by server name
|
||||||
|
func (c *monitorService) StartMonitor(server string) (resp types.JSResp) {
|
||||||
|
item, err := c.getItem(server)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item.ch = make(chan string)
|
||||||
|
item.closeCh = make(chan struct{})
|
||||||
|
item.eventName = "monitor:" + strconv.Itoa(int(time.Now().Unix()))
|
||||||
|
item.cmd = item.client.Monitor(c.ctx, item.ch)
|
||||||
|
item.cmd.Start()
|
||||||
|
|
||||||
|
go c.processMonitor(item.ch, item.closeCh, item.eventName)
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = struct {
|
||||||
|
EventName string `json:"eventName"`
|
||||||
|
}{
|
||||||
|
EventName: item.eventName,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *monitorService) processMonitor(ch <-chan string, closeCh <-chan struct{}, eventName string) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case data := <-ch:
|
||||||
|
if data != "OK" {
|
||||||
|
runtime.EventsEmit(c.ctx, eventName, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-closeCh:
|
||||||
|
// monitor stopped
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopMonitor stop monitor by server name
|
||||||
|
func (c *monitorService) StopMonitor(server string) (resp types.JSResp) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
item, ok := c.items[server]
|
||||||
|
if !ok || item.cmd == nil {
|
||||||
|
resp.Success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item.cmd.Stop()
|
||||||
|
//close(item.ch)
|
||||||
|
close(item.closeCh)
|
||||||
|
delete(c.items, server)
|
||||||
|
resp.Success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopAll stop all monitor
|
||||||
|
func (c *monitorService) StopAll() {
|
||||||
|
if c.ctxCancel != nil {
|
||||||
|
c.ctxCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
for server := range c.items {
|
||||||
|
c.StopMonitor(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *monitorService) ExportLog(logs []string) (resp types.JSResp) {
|
||||||
|
filepath, err := runtime.SaveFileDialog(c.ctx, runtime.SaveDialogOptions{
|
||||||
|
ShowHiddenFiles: false,
|
||||||
|
DefaultFilename: fmt.Sprintf("monitor_log_%s.txt", time.Now().Format("20060102150405")),
|
||||||
|
Filters: []runtime.FileFilter{
|
||||||
|
{Pattern: "*.txt"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(filepath)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
writer := bufio.NewWriter(file)
|
||||||
|
for _, line := range logs {
|
||||||
|
_, _ = writer.WriteString(line + "\n")
|
||||||
|
}
|
||||||
|
writer.Flush()
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
return
|
||||||
|
}
|
|
@ -133,12 +133,8 @@ defineExpose({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card
|
<div class="content-log content-container content-value fill-height flex-box-v">
|
||||||
:bordered="false"
|
<n-h3>{{ $t('log.title') }}</n-h3>
|
||||||
:theme-overrides="{ borderRadius: '0px' }"
|
|
||||||
:title="$t('log.title')"
|
|
||||||
class="content-container flex-box-v"
|
|
||||||
content-style="display: flex;flex-direction: column; overflow: hidden; backgroundColor: gray">
|
|
||||||
<n-form :disabled="data.loading" class="flex-item" inline>
|
<n-form :disabled="data.loading" class="flex-item" inline>
|
||||||
<n-form-item :label="$t('log.filter_server')">
|
<n-form-item :label="$t('log.filter_server')">
|
||||||
<n-select
|
<n-select
|
||||||
|
@ -157,24 +153,17 @@ defineExpose({
|
||||||
<icon-button :icon="Delete" border t-tooltip="log.clean_log" @click="cleanHistory" />
|
<icon-button :icon="Delete" border t-tooltip="log.clean_log" @click="cleanHistory" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<div class="content-value fill-height flex-box-h">
|
<n-data-table
|
||||||
<n-data-table
|
ref="tableRef"
|
||||||
ref="tableRef"
|
:columns="columns"
|
||||||
:columns="columns"
|
:data="data.history"
|
||||||
:data="data.history"
|
:loading="data.loading"
|
||||||
:loading="data.loading"
|
class="flex-item-expand"
|
||||||
class="flex-item-expand"
|
flex-height
|
||||||
flex-height
|
virtual-scroll />
|
||||||
virtual-scroll />
|
</div>
|
||||||
</div>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/styles/content';
|
@import '@/styles/content';
|
||||||
|
|
||||||
.content-container {
|
|
||||||
padding: 5px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -15,6 +15,7 @@ import Monitor from '@/components/icons/Monitor.vue'
|
||||||
import Pub from '@/components/icons/Pub.vue'
|
import Pub from '@/components/icons/Pub.vue'
|
||||||
import ContentSlog from '@/components/content_value/ContentSlog.vue'
|
import ContentSlog from '@/components/content_value/ContentSlog.vue'
|
||||||
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
||||||
|
import ContentMonitor from '@/components/content_value/ContentMonitor.vue'
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
|
|
||||||
|
@ -173,7 +174,7 @@ watch(
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<!-- command monitor pane -->
|
<!-- command monitor pane -->
|
||||||
<n-tab-pane :disabled="true" :name="BrowserTabType.CmdMonitor.toString()" display-directive="if">
|
<n-tab-pane :name="BrowserTabType.CmdMonitor.toString()" display-directive="show:lazy">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
||||||
<n-icon size="16">
|
<n-icon size="16">
|
||||||
|
@ -185,6 +186,7 @@ watch(
|
||||||
<span>{{ $t('interface.sub_tab.cmd_monitor') }}</span>
|
<span>{{ $t('interface.sub_tab.cmd_monitor') }}</span>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
|
<content-monitor :server="props.server" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<!-- pub/sub message pane -->
|
<!-- pub/sub message pane -->
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||||
|
import { filter, get, includes, isEmpty, join } from 'lodash'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useThemeVars } from 'naive-ui'
|
||||||
|
import useBrowserStore from 'stores/browser.js'
|
||||||
|
import Play from '@/components/icons/Play.vue'
|
||||||
|
import Pause from '@/components/icons/Pause.vue'
|
||||||
|
import { ExportLog, StartMonitor, StopMonitor } from 'wailsjs/go/services/monitorService.js'
|
||||||
|
import { ClipboardSetText, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
||||||
|
import Copy from '@/components/icons/Copy.vue'
|
||||||
|
import Export from '@/components/icons/Export.vue'
|
||||||
|
import Delete from '@/components/icons/Delete.vue'
|
||||||
|
import IconButton from '@/components/common/IconButton.vue'
|
||||||
|
|
||||||
|
const themeVars = useThemeVars()
|
||||||
|
|
||||||
|
const browserStore = useBrowserStore()
|
||||||
|
const i18n = useI18n()
|
||||||
|
const props = defineProps({
|
||||||
|
server: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = reactive({
|
||||||
|
monitorEvent: '',
|
||||||
|
list: [],
|
||||||
|
listLimit: 20,
|
||||||
|
keyword: '',
|
||||||
|
autoShowLast: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const listRef = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// try to stop prev monitor first
|
||||||
|
onStopMonitor()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
onStopMonitor()
|
||||||
|
})
|
||||||
|
|
||||||
|
const isMonitoring = computed(() => {
|
||||||
|
return !isEmpty(data.monitorEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayList = computed(() => {
|
||||||
|
if (!isEmpty(data.keyword)) {
|
||||||
|
return filter(data.list, (line) => includes(line, data.keyword))
|
||||||
|
}
|
||||||
|
return data.list
|
||||||
|
})
|
||||||
|
|
||||||
|
const onStartMonitor = async () => {
|
||||||
|
if (isMonitoring.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: ret, success, msg } = await StartMonitor(props.server)
|
||||||
|
if (!success) {
|
||||||
|
$message.error(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.monitorEvent = get(ret, 'eventName')
|
||||||
|
EventsOn(data.monitorEvent, (content) => {
|
||||||
|
data.list.push(content)
|
||||||
|
if (data.autoShowLast) {
|
||||||
|
nextTick(() => {
|
||||||
|
listRef.value.scrollTo({ position: 'bottom' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onStopMonitor = async () => {
|
||||||
|
const { success, msg } = await StopMonitor(props.server)
|
||||||
|
if (!success) {
|
||||||
|
$message.error(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
EventsOff(data.monitorEvent)
|
||||||
|
data.monitorEvent = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCopyLog = async () => {
|
||||||
|
try {
|
||||||
|
const content = join(data.list, '\n')
|
||||||
|
const succ = await ClipboardSetText(content)
|
||||||
|
if (succ) {
|
||||||
|
$message.success(i18n.t('interface.copy_succ'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$message.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onExportLog = () => {
|
||||||
|
ExportLog(data.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCleanLog = () => {
|
||||||
|
data.list = []
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="content-log content-container fill-height flex-box-v">
|
||||||
|
<n-form class="flex-item" label-align="left" label-placement="left" label-width="auto">
|
||||||
|
<n-form-item :label="$t('monitor.actions')">
|
||||||
|
<n-space>
|
||||||
|
<n-button
|
||||||
|
v-if="!isMonitoring"
|
||||||
|
:focusable="false"
|
||||||
|
secondary
|
||||||
|
strong
|
||||||
|
type="success"
|
||||||
|
@click="onStartMonitor">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Play" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('monitor.start') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button v-else :focusable="false" secondary strong type="warning" @click="onStopMonitor">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Pause" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('monitor.stop') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button-group>
|
||||||
|
<icon-button :icon="Copy" border t-tooltip="monitor.copy_log" @click="onCopyLog" />
|
||||||
|
<icon-button :icon="Export" border t-tooltip="monitor.save_log" @click="onExportLog" />
|
||||||
|
</n-button-group>
|
||||||
|
<icon-button :icon="Delete" border t-tooltip="monitor.clean_log" @click="onCleanLog" />
|
||||||
|
</n-space>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('monitor.search')">
|
||||||
|
<n-input v-model:value="data.keyword" clearable placeholder="" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('monitor.always_show_last')">
|
||||||
|
<n-switch v-model:value="data.autoShowLast" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<n-virtual-list ref="listRef" :item-size="25" :items="displayList" class="list-wrapper">
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div class="line-item content-value">
|
||||||
|
<b>></b>
|
||||||
|
{{ item }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-virtual-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '@/styles/content';
|
||||||
|
|
||||||
|
.line-item {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-wrapper {
|
||||||
|
background-color: v-bind('themeVars.codeColor');
|
||||||
|
border: solid 1px v-bind('themeVars.borderColor');
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -150,12 +150,7 @@ const onListLimitChanged = (limit) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card
|
<div class="content-log content-container content-value fill-height flex-box-v">
|
||||||
:bordered="false"
|
|
||||||
:theme-overrides="{ borderRadius: '0px' }"
|
|
||||||
:title="$t('slog.title')"
|
|
||||||
class="content-container flex-box-v"
|
|
||||||
content-style="display: flex;flex-direction: column; overflow: hidden; backgroundColor: gray">
|
|
||||||
<n-form :disabled="data.loading" class="flex-item" inline>
|
<n-form :disabled="data.loading" class="flex-item" inline>
|
||||||
<n-form-item :label="$t('slog.limit')">
|
<n-form-item :label="$t('slog.limit')">
|
||||||
<n-input-number
|
<n-input-number
|
||||||
|
@ -183,18 +178,16 @@ const onListLimitChanged = (limit) => {
|
||||||
<n-input v-model:value="data.keyword" clearable placeholder="" />
|
<n-input v-model:value="data.keyword" clearable placeholder="" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<div class="content-value fill-height flex-box-h">
|
<n-data-table
|
||||||
<n-data-table
|
ref="tableRef"
|
||||||
ref="tableRef"
|
:columns="columns"
|
||||||
:columns="columns"
|
:data="data.list"
|
||||||
:data="data.list"
|
:loading="data.loading"
|
||||||
:loading="data.loading"
|
class="flex-item-expand"
|
||||||
class="flex-item-expand"
|
flex-height
|
||||||
flex-height
|
virtual-scroll
|
||||||
virtual-scroll
|
@update:sorter="({ order }) => (data.sortOrder = order)" />
|
||||||
@update:sorter="({ order }) => (data.sortOrder = order)" />
|
</div>
|
||||||
</div>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M16 12V36"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M32 12V36"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M15 24V11.8756L25.5 17.9378L36 24L25.5 30.0622L15 36.1244V24Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -353,5 +353,16 @@
|
||||||
"cost_time": "Cost",
|
"cost_time": "Cost",
|
||||||
"refresh": "Refresh Now",
|
"refresh": "Refresh Now",
|
||||||
"auto_refresh": "Auto Refresh"
|
"auto_refresh": "Auto Refresh"
|
||||||
|
},
|
||||||
|
"monitor": {
|
||||||
|
"title": "Monitor Commands",
|
||||||
|
"actions": "Actions",
|
||||||
|
"start": "Start",
|
||||||
|
"stop": "Stop",
|
||||||
|
"search": "Search",
|
||||||
|
"copy_log": "Copy Log",
|
||||||
|
"save_log": "Save Log",
|
||||||
|
"clean_log": "Clean Log",
|
||||||
|
"always_show_last": "Always To Last Line"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -353,5 +353,16 @@
|
||||||
"cost_time": "耗时",
|
"cost_time": "耗时",
|
||||||
"refresh": "立即刷新",
|
"refresh": "立即刷新",
|
||||||
"auto_refresh": "自动刷新"
|
"auto_refresh": "自动刷新"
|
||||||
|
},
|
||||||
|
"monitor": {
|
||||||
|
"title": "监控命令",
|
||||||
|
"actions": "操作",
|
||||||
|
"start": "开启监控",
|
||||||
|
"stop": "停止监控",
|
||||||
|
"search": "搜索",
|
||||||
|
"copy_log": "复制日志",
|
||||||
|
"save_log": "保存日志",
|
||||||
|
"clean_log": "清空日志",
|
||||||
|
"always_show_last": "总是显示最新"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-log {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.content-value {
|
.content-value {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
4
main.go
4
main.go
|
@ -31,6 +31,7 @@ func main() {
|
||||||
connSvc := services.Connection()
|
connSvc := services.Connection()
|
||||||
browserSvc := services.Browser()
|
browserSvc := services.Browser()
|
||||||
cliSvc := services.Cli()
|
cliSvc := services.Cli()
|
||||||
|
monitorSvc := services.Monitor()
|
||||||
prefSvc := services.Preferences()
|
prefSvc := services.Preferences()
|
||||||
prefSvc.SetAppVersion(version)
|
prefSvc.SetAppVersion(version)
|
||||||
windowWidth, windowHeight, maximised := prefSvc.GetWindowSize()
|
windowWidth, windowHeight, maximised := prefSvc.GetWindowSize()
|
||||||
|
@ -68,6 +69,7 @@ func main() {
|
||||||
connSvc.Start(ctx)
|
connSvc.Start(ctx)
|
||||||
browserSvc.Start(ctx)
|
browserSvc.Start(ctx)
|
||||||
cliSvc.Start(ctx)
|
cliSvc.Start(ctx)
|
||||||
|
monitorSvc.Start(ctx)
|
||||||
|
|
||||||
services.GA().SetSecretKey(gaMeasurementID, gaSecretKey)
|
services.GA().SetSecretKey(gaMeasurementID, gaSecretKey)
|
||||||
services.GA().Startup(version)
|
services.GA().Startup(version)
|
||||||
|
@ -85,12 +87,14 @@ func main() {
|
||||||
OnShutdown: func(ctx context.Context) {
|
OnShutdown: func(ctx context.Context) {
|
||||||
browserSvc.Stop()
|
browserSvc.Stop()
|
||||||
cliSvc.CloseAll()
|
cliSvc.CloseAll()
|
||||||
|
monitorSvc.StopAll()
|
||||||
},
|
},
|
||||||
Bind: []interface{}{
|
Bind: []interface{}{
|
||||||
sysSvc,
|
sysSvc,
|
||||||
connSvc,
|
connSvc,
|
||||||
browserSvc,
|
browserSvc,
|
||||||
cliSvc,
|
cliSvc,
|
||||||
|
monitorSvc,
|
||||||
prefSvc,
|
prefSvc,
|
||||||
},
|
},
|
||||||
Mac: &mac.Options{
|
Mac: &mac.Options{
|
||||||
|
|
Loading…
Reference in New Issue