Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use xterm.js for log outputs #846

Merged
merged 18 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
"pinia": "2.0.0",
"vue": "v3.2.20",
"vue-i18n": "9",
"vue-router": "4.0.10"
"vue-router": "4.0.10",
"xterm": "4.17.0",
"xterm-addon-fit": "0.5.0",
"xterm-addon-web-links": "0.5.1"
},
"devDependencies": {
"@iconify/json": "1.1.421",
Expand Down
6 changes: 4 additions & 2 deletions web/src/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,14 @@
"created": "Created",
"tasks": "Tasks",
"config": "Config",
"files": "Changed files ({0})",
"files": "Changed files ({files})",
"no_files": "No files have been changed.",
"execution_error": "Execution error",
"no_pipelines": "No pipelines have been started yet.",
"step_not_started": "This step hasn't started yet.",
"pipelines_for": "Pipelines for branch \"{0}\"",
"pipelines_for": "Pipelines for branch \"{branch}\"",
"exit_code": "exit code {exitCode}",
anbraten marked this conversation as resolved.
Show resolved Hide resolved
"loading": "Loading ...",

"actions": {
"cancel": "Cancel",
Expand Down
185 changes: 154 additions & 31 deletions web/src/components/repo/build/BuildLog.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div v-if="build" class="font-mono bg-gray-700 pt-14 md:pt-4 dark:bg-dark-gray-700 p-4 overflow-y-scroll">
<div v-if="build" class="flex flex-col pt-10 md:pt-0">
<div
class="fixed top-0 left-0 w-full md:hidden flex px-4 py-2 bg-gray-600 dark:bg-dark-gray-800 text-gray-50"
@click="$emit('update:proc-id', null)"
Expand All @@ -8,36 +8,54 @@
<Icon name="close" class="ml-auto" />
</div>

<template v-if="!proc?.error">
<div v-for="logLine in logLines" :key="logLine.pos" class="flex items-center">
<div class="text-gray-500 text-sm w-4">{{ (logLine.pos || 0) + 1 }}</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="mx-4 text-gray-200 dark:text-gray-400" v-html="logLine.out" />
<div class="ml-auto text-gray-500 text-sm">{{ logLine.time || 0 }}s</div>
<div class="flex flex-grow flex-col bg-gray-300 dark:bg-dark-gray-700 md:m-2 md:mt-0 md:rounded-md overflow-hidden">
<div v-show="loadedLogs" class="w-full flex-grow p-2">
<div id="terminal" class="w-full h-full" />
</div>
<div v-if="proc?.end_time !== undefined" class="text-gray-500 text-sm mt-4 ml-8">
exit code {{ proc.exit_code }}

<div class="m-auto text-xl text-gray-500 dark:text-gray-500">
<span v-if="proc?.error" class="text-red-400">{{ proc.error }}</span>
<span v-else-if="proc?.state === 'skipped'" class="text-red-400">{{ $t('repo.build.actions.canceled') }}</span>
<span v-else-if="!proc?.start_time">{{ $t('repo.build.step_not_started') }}</span>
<div v-else-if="!loadedLogs">{{ $t('repo.build.loading') }}</div>
</div>
</template>

<div class="text-gray-300 mx-auto">
<span v-if="proc?.error" class="text-red-500">{{ proc.error }}</span>
<span v-else-if="proc?.state === 'skipped'" class="text-orange-300 dark:text-orange-800">
>{{ $t('repo.build.actions.canceled') }}</span
<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"
>
<span v-else-if="!proc?.start_time" class="dark:text-gray-500">{{ $t('repo.build.step_not_started') }}</span>
{{ $t('repo.build.exit_code', { exitCode: proc.exit_code }) }}
</div>
</div>
</div>
</template>

<script lang="ts">
import AnsiConvert from 'ansi-to-html';
import { computed, defineComponent, inject, onBeforeUnmount, onMounted, PropType, Ref, toRef, watch } from 'vue';
import 'xterm/css/xterm.css';

import {
computed,
defineComponent,
inject,
nextTick,
onBeforeUnmount,
onMounted,
PropType,
Ref,
ref,
toRef,
watch,
} from 'vue';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';

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

export default defineComponent({
name: 'BuildLog',
Expand Down Expand Up @@ -67,37 +85,142 @@ export default defineComponent({
const build = toRef(props, 'build');
const procId = toRef(props, 'procId');
const repo = inject<Ref<Repo>>('repo');
const buildProc = useBuildProc();
const apiClient = useApiClient();

const ansiConvert = new AnsiConvert({ escapeXML: true });
const logLines = computed(() => buildProc.logs.value?.map((l) => ({ ...l, out: ansiConvert.toHtml(l.out) })));
const loadedProcSlug = ref<string>();
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

async function loadLogs() {
if (loadedProcSlug.value === procSlug.value) {
return;
}
loadedProcSlug.value = procSlug.value;
loadedLogs.value = false;
term.value.reset();
term.value.write('\x1b[?25l');

function loadBuildProc() {
if (!repo) {
throw new Error('Unexpected: "repo" should be provided at this place');
}

if (!repo.value || !build.value || !proc.value) {
if (stream.value) {
stream.value.close();
}

// we do not have logs for skipped jobs
if (
!repo.value ||
!build.value ||
!proc.value ||
proc.value.state === 'skipped' ||
proc.value.state === 'killed'
) {
return;
}

buildProc.load(repo.value.owner, repo.value.name, build.value.number, proc.value);
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;
}

if (isProcRunning(proc.value)) {
// load stream of parent process (which receives all child processes logs)
// TODO: change stream to only send data of single child process
stream.value = apiClient.streamLogs(
repo.value.owner,
repo.value.name,
build.value.number,
proc.value.ppid,
(l) => {
loadedLogs.value = true;
term.value.write(l.out, () => {
if (autoScroll.value) {
term.value.scrollToBottom();
}
});
},
);
}
}

function resize() {
fitAddon.value.fit();
}

onMounted(() => {
loadBuildProc();
onMounted(async () => {
term.value.loadAddon(fitAddon.value);
term.value.loadAddon(new WebLinksAddon());

await nextTick(() => {
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([repo, build, procId], () => {
loadBuildProc();
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',
},
};
}
},
{ immediate: true },
);

onBeforeUnmount(() => {
buildProc.unload();
if (stream.value) {
stream.value.close();
}
window.removeEventListener('resize', resize);
});

return { logLines, proc };
return { proc, loadedLogs };
},
});
</script>
38 changes: 32 additions & 6 deletions web/src/components/repo/build/BuildProcList.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
<template>
<div class="flex flex-col w-full md:w-3/12 text-gray-200 dark:text-gray-400 bg-gray-600 dark:bg-dark-gray-800">
<div class="flex py-4 px-2 mx-2 space-x-1 justify-between flex-shrink-0 border-b-1 dark:border-dark-gray-600">
<div class="flex flex-col w-full md:w-3/12 text-gray-600 dark:text-gray-400">
<div
class="
flex
md:ml-2
p-4
space-x-1
justify-between
flex-shrink-0
border-b-1
md:rounded-md
bg-gray-300
dark:border-b-dark-gray-600 dark:bg-dark-gray-700
"
>
<div class="flex space-x-1 items-center flex-shrink-0">
<div class="flex items-center"><img class="w-6" :src="build.author_avatar" /></div>
<span>{{ build.author }}</span>
Expand All @@ -25,7 +38,7 @@
<Icon name="commit" />
<span>{{ build.commit.slice(0, 10) }}</span>
</template>
<a v-else class="text-link flex items-center" :href="build.link_url" target="_blank">
<a v-else class="text-blue-700 dark:text-link flex items-center" :href="build.link_url" target="_blank">
<Icon name="commit" />
<span>{{ build.commit.slice(0, 10) }}</span>
</a>
Expand All @@ -40,7 +53,9 @@
<div class="md:absolute top-0 left-0 w-full">
<div v-for="proc in build.procs" :key="proc.id">
<div class="p-4 pb-1 flex flex-wrap items-center justify-between">
<span>{{ proc.name }}</span>
<div class="flex items-center">
<span class="ml-2">{{ proc.name }}</span>
</div>
<div v-if="proc.environ" class="text-xs">
<div v-for="(value, key) in proc.environ" :key="key">
<span
Expand All @@ -49,6 +64,7 @@
pr-1
py-0.5
bg-gray-800
text-gray-200
dark:bg-gray-600
border-2 border-gray-800
dark:border-gray-600
Expand All @@ -65,8 +81,18 @@
<div
v-for="job in proc.children"
:key="job.pid"
class="flex p-2 pl-6 cursor-pointer items-center hover:bg-gray-700 hover:dark:bg-dark-gray-900"
:class="{ 'bg-gray-700 !dark:bg-dark-gray-600': selectedProcId && selectedProcId === job.pid }"
class="
flex
mx-2
mb-1
p-2
pl-6
cursor-pointer
rounded-md
items-center
hover:bg-gray-300 hover:dark:bg-dark-gray-700
"
:class="{ 'bg-gray-300 !dark:bg-dark-gray-700': selectedProcId && selectedProcId === job.pid }"
@click="$emit('update:selected-proc-id', job.pid)"
>
<div v-if="['success'].includes(job.state)" class="w-2 h-2 bg-lime-400 rounded-full" />
Expand Down
49 changes: 0 additions & 49 deletions web/src/compositions/useBuildProc.ts

This file was deleted.

2 changes: 1 addition & 1 deletion web/src/views/repo/RepoBranch.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="flex w-full mb-4 justify-center">
<span class="text-gray-600 dark:text-gray-500 text-xl">{{ $t('repo.build.pipelines_for', [branch]) }}</span>
<span class="text-gray-600 dark:text-gray-500 text-xl">{{ $t('repo.build.pipelines_for', { branch }) }}</span>
</div>
<BuildList :builds="builds" :repo="repo" />
</template>
Expand Down
Loading