fix: Operation logs Add node information (#8212)

This commit is contained in:
ssongliu 2025-03-21 17:17:24 +08:00 committed by GitHub
parent 57f7cb30b4
commit 337dad2ca6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 121 additions and 39 deletions

View file

@ -137,11 +137,7 @@ func (b *BaseApi) UpdateCommand(c *gin.Context) {
return return
} }
upMap := make(map[string]interface{}) if err := commandService.Update(req); err != nil {
upMap["name"] = req.Name
upMap["group_id"] = req.GroupID
upMap["command"] = req.Command
if err := commandService.Update(req.ID, upMap); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }

View file

@ -5,9 +5,9 @@ import (
) )
type OperationLog struct { type OperationLog struct {
ID uint `json:"id"` ID uint `json:"id"`
Source string `json:"source"` Source string `json:"source"`
Node string `json:"node"`
IP string `json:"ip"` IP string `json:"ip"`
Path string `json:"path"` Path string `json:"path"`
Method string `json:"method"` Method string `json:"method"`
@ -26,6 +26,7 @@ type SearchOpLogWithPage struct {
PageInfo PageInfo
Source string `json:"source"` Source string `json:"source"`
Status string `json:"status"` Status string `json:"status"`
Node string `json:"node"`
Operation string `json:"operation"` Operation string `json:"operation"`
} }

View file

@ -8,6 +8,7 @@ type OperationLog struct {
BaseModel BaseModel
Source string `json:"source"` Source string `json:"source"`
IP string `json:"ip"` IP string `json:"ip"`
Node string `json:"node"`
Path string `json:"path"` Path string `json:"path"`
Method string `json:"method"` Method string `json:"method"`
UserAgent string `json:"userAgent"` UserAgent string `json:"userAgent"`

View file

@ -55,6 +55,11 @@ func WithByStatus(status string) global.DBOption {
return g.Where("status = ?", status) return g.Where("status = ?", status)
} }
} }
func WithByNode(node string) global.DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("node = ?", node)
}
}
func WithByGroupBelong(group string) global.DBOption { func WithByGroupBelong(group string) global.DBOption {
return func(g *gorm.DB) *gorm.DB { return func(g *gorm.DB) *gorm.DB {
return g.Where("group_belong = ?", group) return g.Where("group_belong = ?", group)

View file

@ -15,7 +15,7 @@ type ICommandService interface {
SearchForTree(req dto.OperateByType) ([]dto.CommandTree, error) SearchForTree(req dto.OperateByType) ([]dto.CommandTree, error)
SearchWithPage(search dto.SearchCommandWithPage) (int64, interface{}, error) SearchWithPage(search dto.SearchCommandWithPage) (int64, interface{}, error)
Create(req dto.CommandOperate) error Create(req dto.CommandOperate) error
Update(id uint, upMap map[string]interface{}) error Update(req dto.CommandOperate) error
Delete(ids []uint) error Delete(ids []uint) error
} }
@ -123,6 +123,14 @@ func (u *CommandService) Delete(ids []uint) error {
return commandRepo.Delete(repo.WithByIDs(ids)) return commandRepo.Delete(repo.WithByIDs(ids))
} }
func (u *CommandService) Update(id uint, upMap map[string]interface{}) error { func (u *CommandService) Update(req dto.CommandOperate) error {
return commandRepo.Update(id, upMap) command, _ := commandRepo.Get(repo.WithByName(req.Name), repo.WithByType(req.Type))
if command.ID != req.ID {
return buserr.New("ErrRecordExist")
}
upMap := make(map[string]interface{})
upMap["name"] = req.Name
upMap["group_id"] = req.GroupID
upMap["command"] = req.Command
return commandRepo.Update(req.ID, upMap)
} }

View file

@ -122,6 +122,9 @@ func (u *LogService) PageOperationLog(req dto.SearchOpLogWithPage) (int64, inter
if len(req.Status) != 0 { if len(req.Status) != 0 {
options = append(options, repo.WithByStatus(req.Status)) options = append(options, repo.WithByStatus(req.Status))
} }
if len(req.Node) != 0 {
options = append(options, repo.WithByNode(req.Node))
}
total, ops, err := logRepo.PageOperationLog( total, ops, err := logRepo.PageOperationLog(
req.Page, req.Page,

View file

@ -2,7 +2,6 @@ package i18n
import ( import (
"embed" "embed"
"fmt"
"strings" "strings"
"github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/global"
@ -65,10 +64,9 @@ func GetErrMsg(key string, maps map[string]interface{}) string {
} }
func GetMsgByKey(key string) string { func GetMsgByKey(key string) string {
content, err := global.I18n.Localize(&i18n.LocalizeConfig{ content, _ := global.I18n.Localize(&i18n.LocalizeConfig{
MessageID: key, MessageID: key,
}) })
fmt.Println(err)
return content return content
} }

View file

@ -24,6 +24,7 @@ func Init() {
migrations.UpdateXpackHideMemu, migrations.UpdateXpackHideMemu,
migrations.AddSystemIP, migrations.AddSystemIP,
migrations.InitScriptLibrary, migrations.InitScriptLibrary,
migrations.AddOperationNode,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)

View file

@ -339,3 +339,13 @@ var InitScriptLibrary = &gormigrate.Migration{
return nil return nil
}, },
} }
var AddOperationNode = &gormigrate.Migration{
ID: "20250321-add-operation-node",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.OperationLog{}); err != nil {
return err
}
return nil
},
}

View file

@ -32,8 +32,10 @@ func OperationLog() gin.HandlerFunc {
source := loadLogInfo(c.Request.URL.Path) source := loadLogInfo(c.Request.URL.Path)
pathItem := strings.TrimPrefix(c.Request.URL.Path, "/api/v2") pathItem := strings.TrimPrefix(c.Request.URL.Path, "/api/v2")
pathItem = strings.TrimPrefix(pathItem, "/api/v2/core") pathItem = strings.TrimPrefix(pathItem, "/api/v2/core")
currentNode := c.Request.Header.Get("CurrentNode")
record := &model.OperationLog{ record := &model.OperationLog{
Source: source, Source: source,
Node: currentNode,
IP: c.ClientIP(), IP: c.ClientIP(),
Method: strings.ToLower(c.Request.Method), Method: strings.ToLower(c.Request.Method),
Path: pathItem, Path: pathItem,

View file

@ -89,7 +89,7 @@ const systemRules: TokenRule[] = [
const taskRules: TokenRule[] = [ const taskRules: TokenRule[] = [
{ {
type: 'bracket-text', type: 'bracket-text',
pattern: /\[([^\]]+)\]/g, pattern: /\[([^\,]]+)\]/g,
color: '#B87A2B', color: '#B87A2B',
}, },
]; ];

View file

@ -31,11 +31,12 @@
v-if="version !== 'Waiting' && !globalStore.hasNewVersion" v-if="version !== 'Waiting' && !globalStore.hasNewVersion"
type="primary" type="primary"
:underline="false" :underline="false"
class="ml-2"
@click="onLoadUpgradeInfo" @click="onLoadUpgradeInfo"
> >
{{ $t('commons.button.update') }} {{ $t('commons.button.update') }}
</el-link> </el-link>
<el-tag v-if="version === 'Waiting'" round style="margin-left: 10px"> <el-tag v-if="version === 'Waiting'" round class="ml-2.5">
{{ $t('setting.upgrading') }} {{ $t('setting.upgrading') }}
</el-tag> </el-tag>
</div> </div>

View file

@ -7,15 +7,19 @@
@mouseenter="collapseButtonShow = true" @mouseenter="collapseButtonShow = true"
@mouseleave="collapseButtonShow = false" @mouseleave="collapseButtonShow = false"
> >
<el-affix v-if="collapseButtonShow" :offset="124" class="affix"> <el-affix v-if="collapseButtonShow" :offset="18" class="affix">
<el-button <el-tooltip
size="small" :content="menuStore.isCollapse ? $t('commons.button.expand') : $t('commons.button.collapse')"
circle >
:style="{ 'margin-left': menuStore.isCollapse ? '60px' : '165px', position: 'absolute' }" <el-button
:icon="menuStore.isCollapse ? 'ArrowRight' : 'ArrowLeft'" size="small"
plain circle
@click="handleCollapse()" :style="{ 'margin-left': menuStore.isCollapse ? '60px' : '165px', position: 'absolute' }"
></el-button> :icon="menuStore.isCollapse ? 'ArrowRight' : 'ArrowLeft'"
plain
@click="handleCollapse()"
></el-button>
</el-tooltip>
</el-affix> </el-affix>
<Sidebar @menu-click="handleMenuClick" :menu-router="!classObj.openMenuTabs" @open-task="openTask" /> <Sidebar @menu-click="handleMenuClick" :menu-router="!classObj.openMenuTabs" @open-task="openTask" />
</div> </div>

View file

@ -29,6 +29,7 @@
@sort-change="search" @sort-change="search"
@search="search" @search="search"
:data="data" :data="data"
:heightDiff="300"
> >
<el-table-column type="selection" fix /> <el-table-column type="selection" fix />
<el-table-column <el-table-column

View file

@ -387,7 +387,6 @@ const forDetail = async (row: Cronjob.Record) => {
}; };
const loadRecord = async (row: Cronjob.Record) => { const loadRecord = async (row: Cronjob.Record) => {
currentRecord.value = row; currentRecord.value = row;
console.log(currentRecord.value);
if (row.records) { if (row.records) {
const res = await getRecordLog(row.id); const res = await getRecordLog(row.id);
let log = res.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); let log = res.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');

View file

@ -29,7 +29,7 @@
:pagination-config="paginationConfig" :pagination-config="paginationConfig"
:data="data" :data="data"
@search="search" @search="search"
:heightDiff="370" :heightDiff="300"
> >
<el-table-column type="selection" fix /> <el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name" min-width="60"> <el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name" min-width="60">

View file

@ -87,6 +87,7 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
formEl.validate(async (valid) => { formEl.validate(async (valid) => {
if (!valid) return; if (!valid) return;
loading.value = true; loading.value = true;
dialogData.value.rowData.groupList = dialogData.value.rowData.groupList || [];
dialogData.value.rowData.groups = dialogData.value.rowData.groupList.join(','); dialogData.value.rowData.groups = dialogData.value.rowData.groupList.join(',');
if (dialogData.value.title === 'create') { if (dialogData.value.title === 'create') {
await addScript(dialogData.value.rowData) await addScript(dialogData.value.rowData)

View file

@ -1,7 +1,11 @@
<template> <template>
<div v-loading="loading"> <div v-loading="loading">
<LayoutContent :title="props.database + ' ' + $t('commons.button.set')" backName="PostgreSQL"> <LayoutContent backName="PostgreSQL">
<template #leftToolBar> <template #leftToolBar>
<el-text class="mx-1">
{{ props.database }}
</el-text>
<el-divider direction="vertical" />
<el-button type="primary" :plain="activeName !== 'conf'" @click="jumpToConf"> <el-button type="primary" :plain="activeName !== 'conf'" @click="jumpToConf">
{{ $t('database.confChange') }} {{ $t('database.confChange') }}
</el-button> </el-button>

View file

@ -156,7 +156,7 @@ const buttons = [
}, },
}, },
{ {
label: i18n.global.t('commons.button.delete'), label: i18n.global.t('commons.button.unbind'),
click: (row: Database.DatabaseInfo) => { click: (row: Database.DatabaseInfo) => {
onDelete(row); onDelete(row);
}, },

View file

@ -1,7 +1,11 @@
<template> <template>
<div v-show="settingShow" v-loading="loading"> <div v-show="settingShow" v-loading="loading">
<LayoutContent :title="database + ' ' + $t('commons.button.set')" :reload="true"> <LayoutContent :reload="true">
<template #leftToolBar> <template #leftToolBar>
<el-text class="mx-1">
{{ database }}
</el-text>
<el-divider direction="vertical" />
<el-button type="primary" :plain="activeName !== 'conf'" @click="changeTab('conf')"> <el-button type="primary" :plain="activeName !== 'conf'" @click="changeTab('conf')">
{{ $t('database.confChange') }} {{ $t('database.confChange') }}
</el-button> </el-button>

View file

@ -10,7 +10,7 @@
</el-button> </el-button>
</template> </template>
<template #rightToolBar> <template #rightToolBar>
<el-select v-model="searchGroup" @change="search()" clearable class="p-w-200 mr-2.5"> <el-select v-model="searchGroup" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('logs.resource') }}</template> <template #prefix>{{ $t('logs.resource') }}</template>
<el-option :label="$t('commons.table.all')" value=""></el-option> <el-option :label="$t('commons.table.all')" value=""></el-option>
<el-option :label="$t('logs.detail.apps')" value="apps"></el-option> <el-option :label="$t('logs.detail.apps')" value="apps"></el-option>
@ -25,12 +25,18 @@
<el-option :label="$t('logs.detail.logs')" value="logs"></el-option> <el-option :label="$t('logs.detail.logs')" value="logs"></el-option>
<el-option :label="$t('logs.detail.settings')" value="settings"></el-option> <el-option :label="$t('logs.detail.settings')" value="settings"></el-option>
</el-select> </el-select>
<el-select v-model="searchStatus" @change="search()" clearable class="p-w-200 mr-2.5"> <el-select v-model="searchStatus" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('commons.table.status') }}</template> <template #prefix>{{ $t('xpack.node.node') }}</template>
<el-option :label="$t('commons.table.all')" value=""></el-option> <el-option :label="$t('commons.table.all')" value=""></el-option>
<el-option :label="$t('commons.status.success')" value="Success"></el-option> <el-option :label="$t('commons.status.success')" value="Success"></el-option>
<el-option :label="$t('commons.status.failed')" value="Failed"></el-option> <el-option :label="$t('commons.status.failed')" value="Failed"></el-option>
</el-select> </el-select>
<el-select v-model="searchNode" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('xpack.node.node') }}</template>
<el-option :label="$t('commons.table.all')" value=""></el-option>
<el-option :label="$t('xpack.node.master')" value="local" />
<el-option v-for="(node, index) in nodes" :key="index" :label="node.name" :value="node.name" />
</el-select>
<TableSearch @search="search()" v-model:searchName="searchName" /> <TableSearch @search="search()" v-model:searchName="searchName" />
<TableRefresh @search="search()" /> <TableRefresh @search="search()" />
<TableSetting title="operation-log-refresh" @search="search()" /> <TableSetting title="operation-log-refresh" @search="search()" />
@ -52,7 +58,11 @@
<span v-if="globalStore.language === 'en'">{{ row.detailEN }}</span> <span v-if="globalStore.language === 'en'">{{ row.detailEN }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column v-if="globalStore.isMasterProductPro" :label="$t('xpack.node.node')" prop="node">
<template #default="{ row }">
<span>{{ row.node === 'local' ? $t('xpack.node.master') : row.node }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status"> <el-table-column :label="$t('commons.table.status')" prop="status">
<template #default="{ row }"> <template #default="{ row }">
<Status :status="row.status" :msg="row.message" /> <Status :status="row.status" :msg="row.message" />
@ -81,6 +91,7 @@ import { onMounted, reactive, ref } from '@vue/runtime-core';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import { listNodeOptions } from '@/api/modules/setting';
const loading = ref(); const loading = ref();
const data = ref(); const data = ref();
@ -94,6 +105,8 @@ const paginationConfig = reactive({
const searchName = ref<string>(''); const searchName = ref<string>('');
const searchGroup = ref<string>(''); const searchGroup = ref<string>('');
const searchStatus = ref<string>(''); const searchStatus = ref<string>('');
const searchNode = ref<string>('');
const nodes = ref();
const globalStore = GlobalStore(); const globalStore = GlobalStore();
@ -104,6 +117,7 @@ const search = async () => {
pageSize: paginationConfig.pageSize, pageSize: paginationConfig.pageSize,
status: searchStatus.value, status: searchStatus.value,
source: searchGroup.value, source: searchGroup.value,
node: searchNode.value,
}; };
loading.value = true; loading.value = true;
await getOperationLogs(params) await getOperationLogs(params)
@ -140,6 +154,23 @@ const loadDetail = (log: string) => {
return log; return log;
}; };
const loadNodes = async () => {
await listNodeOptions()
.then((res) => {
if (!res) {
nodes.value = [];
return;
}
nodes.value = res.data || [];
if (nodes.value.length === 0) {
globalStore.currentNode = 'local';
}
})
.catch(() => {
nodes.value = [];
});
};
const replacements = { const replacements = {
'[enable]': 'commons.button.enable', '[enable]': 'commons.button.enable',
'[Enable]': 'commons.button.enable', '[Enable]': 'commons.button.enable',
@ -172,6 +203,9 @@ const onSubmitClean = async () => {
}; };
onMounted(() => { onMounted(() => {
if (globalStore.isMasterProductPro) {
loadNodes();
}
search(); search();
}); });
</script> </script>

View file

@ -14,7 +14,12 @@
{{ $t('commons.button.watch') }} {{ $t('commons.button.watch') }}
</el-checkbox> </el-checkbox>
</el-button> </el-button>
<el-radio-group class="ml-2" @change="search()" v-model="itemType"> <el-radio-group
v-if="globalStore.currentNode === 'local'"
class="ml-2"
@change="search()"
v-model="itemType"
>
<el-radio-button :label="$t('logs.agent')" value="Agent" /> <el-radio-button :label="$t('logs.agent')" value="Agent" />
<el-radio-button :label="$t('logs.core')" value="Core" /> <el-radio-button :label="$t('logs.core')" value="Core" />
</el-radio-group> </el-radio-group>
@ -39,6 +44,8 @@ import LogFile from '@/components/log/file/index.vue';
import LogRouter from '@/views/log/router/index.vue'; import LogRouter from '@/views/log/router/index.vue';
import { nextTick, onMounted, reactive, ref } from 'vue'; import { nextTick, onMounted, reactive, ref } from 'vue';
import { getSystemFiles } from '@/api/modules/log'; import { getSystemFiles } from '@/api/modules/log';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const loading = ref(); const loading = ref();
const isWatch = ref(); const isWatch = ref();

View file

@ -297,6 +297,8 @@ const agreeWithLogin = () => {
const login = (formEl: FormInstance | undefined) => { const login = (formEl: FormInstance | undefined) => {
if (!formEl || isLoggingIn) return; if (!formEl || isLoggingIn) return;
errAuthInfo.value = false;
errCaptcha.value = false;
formEl.validate(async (valid) => { formEl.validate(async (valid) => {
if (!valid) return; if (!valid) return;
if (isIntl.value) { if (isIntl.value) {

View file

@ -157,7 +157,7 @@
inactive-value="Disable" inactive-value="Disable"
/> />
<span class="input-help">{{ $t('setting.apiInterfaceHelper') }}</span> <span class="input-help">{{ $t('setting.apiInterfaceHelper') }}</span>
<div v-if="form.apiInterfaceStatus === 'enable'"> <div v-if="form.apiInterfaceStatus === 'Enable'">
<div> <div>
<el-button link type="primary" @click="onChangeApiInterfaceStatus"> <el-button link type="primary" @click="onChangeApiInterfaceStatus">
{{ $t('commons.button.view') }} {{ $t('commons.button.view') }}

View file

@ -1,5 +1,5 @@
<template> <template>
<div @keydown.esc="toggleFullscreen()"> <div tabindex="0">
<el-tabs <el-tabs
type="card" type="card"
class="terminal-tabs" class="terminal-tabs"
@ -164,12 +164,12 @@ const mobile = computed(() => {
return globalStore.isMobile(); return globalStore.isMobile();
}); });
function toggleFullscreen() { const toggleFullscreen = () => {
if (screenfull.isEnabled) { if (screenfull.isEnabled) {
screenfull.toggle(); screenfull.toggle();
} }
globalStore.isFullScreen = !screenfull.isFullscreen; globalStore.isFullScreen = !screenfull.isFullscreen;
} };
const loadTooltip = () => { const loadTooltip = () => {
return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen')); return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen'));
}; };