Skip to content

Commit

Permalink
Remove xterm and use ansi converter for logs (#1067)
Browse files Browse the repository at this point in the history
* Steaming works without flickering
* Text can be correctly copied
* Show only selected step output when streaming
* Improved exit code colors for better readability
* Adds time display on right side

When compiled assets/Build.js size was 355K, now it is 26K

Fixes #1012 
Fixes #998

Co-authored-by: Anbraten <anton@ju60.de>
  • Loading branch information
lafriks and anbraten authored Aug 8, 2022
1 parent 98636a5 commit 2f5e5b8
Show file tree
Hide file tree
Showing 4 changed files with 319 additions and 137 deletions.
7 changes: 2 additions & 5 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@intlify/vite-plugin-vue-i18n": "^3.4.0",
"@kyvg/vue3-notification": "2.3.4",
"@meforma/vue-toaster": "1.2.2",
"ansi-to-html": "0.7.2",
"ansi_up": "^5.1.0",
"dayjs": "1.10.7",
"floating-vue": "2.0.0-beta.5",
"fuse.js": "6.4.6",
Expand All @@ -31,10 +31,7 @@
"pinia": "2.0.0",
"vue": "v3.2.20",
"vue-i18n": "9",
"vue-router": "4.0.10",
"xterm": "4.17.0",
"xterm-addon-fit": "0.5.0",
"xterm-addon-web-links": "0.5.1"
"vue-router": "4.0.10"
},
"devDependencies": {
"@iconify/json": "1.1.421",
Expand Down
227 changes: 117 additions & 110 deletions web/src/components/repo/build/BuildLog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,26 @@
/>
</div>

<div v-show="loadedLogs" class="w-full flex-grow p-2">
<div id="terminal" class="w-full h-full" />
<div
v-show="loadedLogs"
ref="consoleElement"
class="
w-full
max-w-full
grid grid-cols-[min-content,1fr,min-content]
auto-rows-min
flex-grow
p-2
gap-x-2
overflow-x-hidden overflow-y-auto
"
>
<div v-for="line in log" :key="line.index" class="contents font-mono">
<span class="text-gray-500 whitespace-nowrap select-none text-right">{{ line.index + 1 }}</span>
<!-- eslint-disable-next-line vue/no-v-html -->
<span class="align-top text-color whitespace-pre-wrap break-words" v-html="line.text" />
<span class="text-gray-500 whitespace-nowrap select-none text-right">{{ formatTime(line.time) }}</span>
</div>
</div>

<div class="m-auto text-xl text-color">
Expand All @@ -36,8 +54,8 @@

<div
v-if="proc?.end_time !== undefined"
:class="proc.exit_code == 0 ? 'dark:text-lime-400 text-lime-600' : 'dark:text-red-400 text-red-600'"
class="w-full bg-gray-400 dark:bg-dark-gray-800 text-md p-4"
:class="proc.exit_code == 0 ? 'dark:text-lime-400 text-lime-700' : 'dark:text-red-400 text-red-600'"
class="w-full bg-gray-200 dark:bg-dark-gray-800 text-md p-4"
>
{{ $t('repo.build.exit_code', { exitCode: proc.exit_code }) }}
</div>
Expand All @@ -46,34 +64,26 @@
</template>

<script lang="ts">
import 'xterm/css/xterm.css';

import {
computed,
defineComponent,
inject,
nextTick,
onBeforeUnmount,
onMounted,
PropType,
Ref,
ref,
toRef,
watch,
} from 'vue';
import '~/style/console.css';

import AnsiUp from 'ansi_up';
import { debounce } from 'lodash';
import { computed, defineComponent, inject, nextTick, onMounted, PropType, Ref, ref, toRef, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';

import Button from '~/components/atomic/Button.vue';
import Icon from '~/components/atomic/Icon.vue';
import useApiClient from '~/compositions/useApiClient';
import { useDarkMode } from '~/compositions/useDarkMode';
import useNotifications from '~/compositions/useNotifications';
import { Build, Repo } from '~/lib/api/types';
import { findProc, isProcFinished, isProcRunning } from '~/utils/helpers';

type LogLine = {
index: number;
text: string;
time?: number;
};

export default defineComponent({
name: 'BuildLog',

Expand Down Expand Up @@ -110,24 +120,78 @@ export default defineComponent({
const procSlug = computed(() => `${repo?.value.owner} - ${repo?.value.name} - ${build.value.id} - ${procId.value}`);
const proc = computed(() => build.value && findProc(build.value.procs || [], procId.value));
const stream = ref<EventSource>();
const term = ref(
new Terminal({
convertEol: true,
disableStdin: true,
theme: {
cursor: 'transparent',
},
}),
);
const fitAddon = ref(new FitAddon());
const loadedLogs = ref(true);
const autoScroll = ref(true); // TODO
const log = ref<LogLine[]>();
const consoleElement = ref<Element>();
const loadedLogs = computed(() => !!log.value);
const autoScroll = ref(true); // TODO: allow enable / disable
const showActions = ref(false);
const downloadInProgress = ref(false);
const ansiUp = ref(new AnsiUp());
ansiUp.value.use_classes = true;
const logBuffer = ref<LogLine[]>([]);
const maxLineCount = 500; // TODO: think about way to support lazy-loading more than last 300 logs (#776)
function formatTime(time?: number): string {
return time === undefined ? '' : `${time}s`;
}
function writeLog(line: LogLine) {
logBuffer.value.push({
index: line.index ?? 0,
text: ansiUp.value.ansi_to_html(line.text),
time: line.time ?? 0,
});
}
function scrollDown() {
nextTick(() => {
if (!consoleElement.value) {
return;
}
consoleElement.value.scrollTop = consoleElement.value.scrollHeight;
});
}
const flushLogs = debounce((scroll: boolean) => {
let buffer = logBuffer.value.slice(-maxLineCount);
logBuffer.value = [];
if (buffer.length === 0) {
return;
}
// append old logs lines
if (buffer.length < maxLineCount && log.value) {
buffer = [...log.value.slice(-(maxLineCount - buffer.length)), ...buffer];
}
// deduplicate repeating times
buffer = buffer.reduce(
(acc, line) => ({
lastTime: line.time ?? 0,
lines: [
...acc.lines,
{
...line,
time: acc.lastTime === line.time ? undefined : line.time,
},
],
}),
{ lastTime: -1, lines: [] as LogLine[] },
).lines;
log.value = buffer;
if (scroll && autoScroll.value) {
scrollDown();
}
}, 500);
async function download() {
if (!repo?.value || !build.value || !proc.value) {
throw new Error('The reposiotry, build or proc was undefined');
throw new Error('The repository, build or proc was undefined');
}
let logs;
try {
Expand Down Expand Up @@ -163,9 +227,10 @@ export default defineComponent({
return;
}
loadedProcSlug.value = procSlug.value;
loadedLogs.value = false;
term.value.reset();
term.value.write('\x1b[?25l');
log.value = [];
logBuffer.value = [];
ansiUp.value = new AnsiUp();
ansiUp.value.use_classes = true;
if (!repo) {
throw new Error('Unexpected: "repo" should be provided at this place');
Expand All @@ -188,13 +253,8 @@ export default defineComponent({
if (isProcFinished(proc.value)) {
const logs = await apiClient.getLogs(repo.value.owner, repo.value.name, build.value.number, proc.value.pid);
term.value.write(
logs
.slice(Math.max(logs.length, 0) - 300, logs.length) // TODO: think about way to support lazy-loading more than last 300 logs (#776)
.map((line) => `${(line.pos || 0).toString().padEnd(logs.length.toString().length)} ${line.out}`)
.join(''),
);
loadedLogs.value = true;
logs.forEach((line) => writeLog({ index: line.pos, text: line.out, time: line.time }));
flushLogs(false);
}
if (isProcRunning(proc.value)) {
Expand All @@ -205,87 +265,34 @@ export default defineComponent({
repo.value.name,
build.value.number,
proc.value.ppid,
(l) => {
loadedLogs.value = true;
term.value.write(l.out, () => {
if (autoScroll.value) {
term.value.scrollToBottom();
}
});
(line) => {
if (line?.proc !== proc.value?.name) {
return;
}
writeLog({ index: line.pos, text: line.out, time: line.time });
flushLogs(true);
},
);
}
}
function resize() {
fitAddon.value.fit();
}
const unmounted = ref(false);
onMounted(async () => {
term.value.loadAddon(fitAddon.value);
term.value.loadAddon(new WebLinksAddon());
await nextTick(() => {
if (unmounted.value) {
// need to check if unmounted already because we are async here
return;
}
const element = document.getElementById('terminal');
if (element === null) {
throw new Error('Unexpected: "terminal" should be provided at this place');
}
term.value.open(element);
fitAddon.value.fit();
window.addEventListener('resize', resize);
});
loadLogs();
});
watch(procSlug, () => {
loadLogs();
});
const { darkMode } = useDarkMode();
watch(
darkMode,
() => {
if (darkMode.value) {
term.value.options = {
theme: {
background: '#303440', // dark-gray-700
foreground: '#d3d3d3', // gray-...
},
};
} else {
term.value.options = {
theme: {
background: 'rgb(209,213,219)', // gray-300
foreground: '#000',
selection: '#000',
},
};
watch(proc, (oldProc, newProc) => {
if (oldProc && oldProc.name === newProc?.name && oldProc?.end_time !== newProc?.end_time) {
if (autoScroll.value) {
scrollDown();
}
},
{ immediate: true },
);
onBeforeUnmount(() => {
unmounted.value = true;
if (stream.value) {
stream.value.close();
}
const element = document.getElementById('terminal');
if (element !== null) {
// Clean up any custom DOM added in onMounted above
element.innerHTML = '';
}
window.removeEventListener('resize', resize);
});
return { proc, loadedLogs, showActions, download, downloadInProgress };
return { consoleElement, proc, log, loadedLogs, formatTime, showActions, download, downloadInProgress };
},
});
</script>
Loading

0 comments on commit 2f5e5b8

Please sign in to comment.