fix: Fix missing line breaks in copied logs on the log page (#9283)

This commit is contained in:
2025-06-25 22:05:12 +08:00 committed by GitHub
parent 763df5606b
commit 4f377c04b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 23 additions and 121 deletions

View file

@ -25,15 +25,7 @@
</el-button> </el-button>
</div> </div>
<div class="log-container" :style="styleVars" ref="logContainer"> <div class="log-container" :style="styleVars" ref="logContainer">
<div class="log-spacer" :style="{ height: `${totalHeight}px` }"></div> <CodemirrorPro v-model="logInfo" :lineWrapping="true" :heightDiff="230" :disabled="true"></CodemirrorPro>
<div
v-for="(log, index) in visibleLogs"
:key="startIndex + index"
class="log-item"
:style="{ top: `${(startIndex + index) * logHeight}px` }"
>
<hightlight :log="log" type="container"></hightlight>
</div>
</div> </div>
</template> </template>
@ -44,7 +36,6 @@ import { dateFormatForName } from '@/utils/util';
import { onUnmounted, reactive, ref } from 'vue'; import { onUnmounted, reactive, ref } from 'vue';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { MsgError, MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
import hightlight from '@/components/log/custom-hightlight/index.vue';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
@ -72,8 +63,6 @@ const styleVars = computed(() => ({
})); }));
const logVisible = ref(false); const logVisible = ref(false);
const logContainer = ref<HTMLElement | null>(null);
const logs = ref<string[]>([]);
let eventSource: EventSource | null = null; let eventSource: EventSource | null = null;
const logSearch = reactive({ const logSearch = reactive({
isWatch: true, isWatch: true,
@ -82,17 +71,7 @@ const logSearch = reactive({
tail: 100, tail: 100,
compose: '', compose: '',
}); });
const logHeight = 20; const logInfo = ref<string>('');
const logCount = computed(() => logs.value.length);
const totalHeight = computed(() => logHeight * logCount.value);
const startIndex = ref(0);
const containerHeight = ref(500);
const visibleCount = computed(() => Math.ceil(containerHeight.value / logHeight) + 2);
const visibleLogs = computed(() => {
const start = Math.max(0, startIndex.value - 1);
const end = startIndex.value + visibleCount.value + 1;
return logs.value.slice(start, end);
});
const timeOptions = ref([ const timeOptions = ref([
{ label: i18n.global.t('commons.table.all'), value: 'all' }, { label: i18n.global.t('commons.table.all'), value: 'all' },
@ -135,7 +114,7 @@ const searchLogs = async () => {
if (!logSearch.isWatch) { if (!logSearch.isWatch) {
return; return;
} }
logs.value = []; logInfo.value = '';
let currentNode = globalStore.currentNode; let currentNode = globalStore.currentNode;
let url = `/api/v2/containers/search/log?container=${logSearch.container}&since=${logSearch.mode}&tail=${logSearch.tail}&follow=${logSearch.isWatch}&operateNode=${currentNode}`; let url = `/api/v2/containers/search/log?container=${logSearch.container}&since=${logSearch.mode}&tail=${logSearch.tail}&follow=${logSearch.isWatch}&operateNode=${currentNode}`;
if (logSearch.compose !== '') { if (logSearch.compose !== '') {
@ -143,13 +122,7 @@ const searchLogs = async () => {
} }
eventSource = new EventSource(url); eventSource = new EventSource(url);
eventSource.onmessage = (event: MessageEvent) => { eventSource.onmessage = (event: MessageEvent) => {
const data = event.data; logInfo.value += event.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '') + '\n';
logs.value.push(data);
nextTick(() => {
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight;
}
});
}; };
eventSource.onerror = (event: MessageEvent) => { eventSource.onerror = (event: MessageEvent) => {
stopListening(); stopListening();
@ -199,49 +172,23 @@ const onClean = async () => {
cancelButtonText: i18n.global.t('commons.button.cancel'), cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info', type: 'info',
}).then(async () => { }).then(async () => {
console.log(logSearch);
await cleanContainerLog(logSearch.container); await cleanContainerLog(logSearch.container);
searchLogs(); await searchLogs();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
}); });
}; };
const handleScroll = () => {
if (logContainer.value) {
const scrollTop = logContainer.value.scrollTop;
startIndex.value = Math.max(0, Math.floor(scrollTop / logHeight) - 1);
}
};
onUnmounted(() => { onUnmounted(() => {
handleClose(); handleClose();
if (logContainer.value) {
logContainer.value.removeEventListener('scroll', handleScroll);
}
}); });
const resizeObserver = ref<ResizeObserver | null>(null);
onMounted(() => { onMounted(() => {
logSearch.container = props.container; logSearch.container = props.container;
logSearch.compose = props.compose; logSearch.compose = props.compose;
logVisible.value = true; logVisible.value = true;
logSearch.tail = 100; logSearch.tail = 100;
logSearch.mode = 'all'; logSearch.mode = 'all';
logSearch.isWatch = true; logSearch.isWatch = true;
nextTick(() => {
if (logContainer.value) {
containerHeight.value = logContainer.value.clientHeight;
logContainer.value.addEventListener('scroll', handleScroll);
resizeObserver.value = new ResizeObserver((entries) => {
containerHeight.value = entries[0].contentRect.height;
});
resizeObserver.value.observe(logContainer.value);
}
});
searchLogs(); searchLogs();
}); });
</script> </script>
@ -264,8 +211,7 @@ onMounted(() => {
} }
.log-container { .log-container {
height: calc(100vh - var(--custom-height, 320px)); overflow-y: hidden;
overflow-y: auto;
overflow-x: auto; overflow-x: auto;
position: relative; position: relative;
background-color: #1e1e1e; background-color: #1e1e1e;

View file

@ -1,7 +1,7 @@
<template> <template>
<div v-loading="firstLoading"> <div v-loading="firstLoading">
<div v-if="defaultButton"> <div v-if="defaultButton">
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail(false)" v-if="showTail"> <el-checkbox border v-model="tailLog" class="float-left" @change="changeTail()" v-if="showTail">
{{ $t('commons.button.watch') }} {{ $t('commons.button.watch') }}
</el-checkbox> </el-checkbox>
<el-button <el-button
@ -17,26 +17,23 @@
<slot name="button"></slot> <slot name="button"></slot>
</span> </span>
</div> </div>
<div class="log-container" ref="logContainer" @scroll="onScroll" :style="containerStyle"> <div class="log-container" ref="logContainer" :style="containerStyle">
<div class="log-spacer" :style="{ height: `${totalHeight}px` }"></div> <CodemirrorPro
<div v-if="true"
v-for="(log, index) in visibleLogs" v-model="logInfo"
:key="startIndex + index" :lineWrapping="true"
class="log-item" :heightDiff="330"
:style="{ top: `${(startIndex + index) * logHeight}px` }" :disabled="true"
> ></CodemirrorPro>
<hightlight :log="log" :type="config.colorMode ?? 'nginx'"></hightlight>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'; import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { downloadFile } from '@/utils/util'; import { downloadFile } from '@/utils/util';
import { readByLine } from '@/api/modules/files'; import { readByLine } from '@/api/modules/files';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import bus from '@/global/bus'; import bus from '@/global/bus';
import hightlight from '@/components/log/custom-hightlight/index.vue';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
interface LogProps { interface LogProps {
@ -129,34 +126,10 @@ const minPage = ref(0);
let timer: NodeJS.Timer | null = null; let timer: NodeJS.Timer | null = null;
const logPath = ref(''); const logPath = ref('');
const logInfo = ref<string>('');
const firstLoading = ref(false); const firstLoading = ref(false);
const logs = ref<string[]>([]); const logs = ref<string[]>([]);
const logContainer = ref<HTMLElement | null>(null);
const logHeight = 20;
const logCount = ref(0); const logCount = ref(0);
const totalHeight = computed(() => logHeight * logCount.value);
const containerHeight = ref(500);
const visibleCount = computed(() => Math.ceil(containerHeight.value / logHeight));
const startIndex = ref(0);
const visibleLogs = computed(() => {
return logs.value.slice(startIndex.value, startIndex.value + visibleCount.value);
});
const onScroll = () => {
if (logContainer.value) {
const scrollTop = logContainer.value.scrollTop;
if (scrollTop == 0) {
readReq.page = minPage.value - 1;
if (readReq.page < 1) {
return;
}
minPage.value = readReq.page;
getContent(true);
}
startIndex.value = Math.floor(scrollTop / logHeight);
}
};
const changeLoading = () => { const changeLoading = () => {
loading.value = !loading.value; loading.value = !loading.value;
@ -169,10 +142,8 @@ const onDownload = async () => {
changeLoading(); changeLoading();
}; };
const changeTail = (fromOutSide: boolean) => { const changeTail = () => {
if (fromOutSide) { tailLog.value = !tailLog.value;
tailLog.value = !tailLog.value;
}
if (tailLog.value) { if (tailLog.value) {
timer = setInterval(() => { timer = setInterval(() => {
getContent(false); getContent(false);
@ -212,7 +183,7 @@ const getContent = async (pre: boolean) => {
isLoading.value = false; isLoading.value = false;
firstLoading.value = false; firstLoading.value = false;
} }
logInfo.value = res.data.content;
logPath.value = res.data.path; logPath.value = res.data.path;
firstLoading.value = false; firstLoading.value = false;
@ -255,15 +226,6 @@ const getContent = async (pre: boolean) => {
logs.value = pre ? [...newLogs, ...logs.value] : [...logs.value, ...newLogs]; logs.value = pre ? [...newLogs, ...logs.value] : [...logs.value, ...newLogs];
} }
} }
nextTick(() => {
if (pre) {
logContainer.value.scrollTop = 2000;
} else {
logContainer.value.scrollTop = totalHeight.value;
containerHeight.value = logContainer.value.getBoundingClientRect().height;
}
});
} }
logCount.value = logs.value.length; logCount.value = logs.value.length;
@ -317,7 +279,7 @@ const init = async () => {
tailLog.value = false; tailLog.value = false;
} }
if (tailLog.value) { if (tailLog.value) {
changeTail(false); changeTail();
} }
readReq.latest = true; readReq.latest = true;
await getContent(false); await getContent(false);
@ -331,12 +293,6 @@ onMounted(async () => {
logs.value = []; logs.value = [];
firstLoading.value = true; firstLoading.value = true;
await init(); await init();
nextTick(() => {
if (logContainer.value) {
logContainer.value.scrollTop = totalHeight.value;
containerHeight.value = logContainer.value.getBoundingClientRect().height;
}
});
}); });
onUnmounted(() => { onUnmounted(() => {
@ -347,7 +303,7 @@ defineExpose({ changeTail, onDownload, clearLog });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.log-container { .log-container {
overflow-y: auto; overflow-y: hidden;
overflow-x: auto; overflow-x: auto;
position: relative; position: relative;
background-color: var(--panel-logs-bg-color); background-color: var(--panel-logs-bg-color);

View file

@ -351,7 +351,7 @@
<PruneDialog @search="search" ref="dialogPruneRef" /> <PruneDialog @search="search" ref="dialogPruneRef" />
<RenameDialog @search="search" ref="dialogRenameRef" /> <RenameDialog @search="search" ref="dialogRenameRef" />
<ContainerLogDialog ref="dialogContainerLogRef" :highlightDiff="235" /> <ContainerLogDialog ref="dialogContainerLogRef" :highlightDiff="210" />
<UpgradeDialog @search="search" ref="dialogUpgradeRef" /> <UpgradeDialog @search="search" ref="dialogUpgradeRef" />
<CommitDialog @search="search" ref="dialogCommitRef" /> <CommitDialog @search="search" ref="dialogCommitRef" />
<MonitorDialog ref="dialogMonitorRef" /> <MonitorDialog ref="dialogMonitorRef" />