mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-26 11:51:02 +00:00
Use xterm.js for log outputs (#846)
- use xterm - improve error showing - show build erro
This commit is contained in:
parent
904f9bb194
commit
3c4d451b72
9 changed files with 221 additions and 96 deletions
|
@ -31,7 +31,10 @@
|
||||||
"pinia": "2.0.0",
|
"pinia": "2.0.0",
|
||||||
"vue": "v3.2.20",
|
"vue": "v3.2.20",
|
||||||
"vue-i18n": "9",
|
"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": {
|
"devDependencies": {
|
||||||
"@iconify/json": "1.1.421",
|
"@iconify/json": "1.1.421",
|
||||||
|
|
|
@ -165,12 +165,14 @@
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"config": "Config",
|
"config": "Config",
|
||||||
"files": "Changed files ({0})",
|
"files": "Changed files ({files})",
|
||||||
"no_files": "No files have been changed.",
|
"no_files": "No files have been changed.",
|
||||||
"execution_error": "Execution error",
|
"execution_error": "Execution error",
|
||||||
"no_pipelines": "No pipelines have been started yet.",
|
"no_pipelines": "No pipelines have been started yet.",
|
||||||
"step_not_started": "This step hasn't 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}",
|
||||||
|
"loading": "Loading ...",
|
||||||
|
|
||||||
"actions": {
|
"actions": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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
|
<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"
|
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)"
|
@click="$emit('update:proc-id', null)"
|
||||||
|
@ -8,36 +8,54 @@
|
||||||
<Icon name="close" class="ml-auto" />
|
<Icon name="close" class="ml-auto" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="!proc?.error">
|
<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-for="logLine in logLines" :key="logLine.pos" class="flex items-center">
|
<div v-show="loadedLogs" class="w-full flex-grow p-2">
|
||||||
<div class="text-gray-500 text-sm w-4">{{ (logLine.pos || 0) + 1 }}</div>
|
<div id="terminal" class="w-full h-full" />
|
||||||
<!-- 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>
|
</div>
|
||||||
<div v-if="proc?.end_time !== undefined" class="text-gray-500 text-sm mt-4 ml-8">
|
|
||||||
exit code {{ proc.exit_code }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="text-gray-300 mx-auto">
|
<div class="m-auto text-xl text-gray-500 dark:text-gray-500">
|
||||||
<span v-if="proc?.error" class="text-red-500">{{ proc.error }}</span>
|
<span v-if="proc?.error" class="text-red-400">{{ proc.error }}</span>
|
||||||
<span v-else-if="proc?.state === 'skipped'" class="text-orange-300 dark:text-orange-800">
|
<span v-else-if="proc?.state === 'skipped'" class="text-red-400">{{ $t('repo.build.actions.canceled') }}</span>
|
||||||
>{{ $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>
|
||||||
|
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AnsiConvert from 'ansi-to-html';
|
import 'xterm/css/xterm.css';
|
||||||
import { computed, defineComponent, inject, onBeforeUnmount, onMounted, PropType, Ref, toRef, watch } from 'vue';
|
|
||||||
|
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 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 { Build, Repo } from '~/lib/api/types';
|
||||||
import { findProc } from '~/utils/helpers';
|
import { findProc, isProcFinished, isProcRunning } from '~/utils/helpers';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'BuildLog',
|
name: 'BuildLog',
|
||||||
|
@ -67,37 +85,142 @@ export default defineComponent({
|
||||||
const build = toRef(props, 'build');
|
const build = toRef(props, 'build');
|
||||||
const procId = toRef(props, 'procId');
|
const procId = toRef(props, 'procId');
|
||||||
const repo = inject<Ref<Repo>>('repo');
|
const repo = inject<Ref<Repo>>('repo');
|
||||||
const buildProc = useBuildProc();
|
const apiClient = useApiClient();
|
||||||
|
|
||||||
const ansiConvert = new AnsiConvert({ escapeXML: true });
|
const loadedProcSlug = ref<string>();
|
||||||
const logLines = computed(() => buildProc.logs.value?.map((l) => ({ ...l, out: ansiConvert.toHtml(l.out) })));
|
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 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) {
|
if (!repo) {
|
||||||
throw new Error('Unexpected: "repo" should be provided at this place');
|
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;
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
function resize() {
|
||||||
loadBuildProc();
|
fitAddon.value.fit();
|
||||||
|
}
|
||||||
|
|
||||||
|
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], () => {
|
watch(procSlug, () => {
|
||||||
loadBuildProc();
|
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(() => {
|
onBeforeUnmount(() => {
|
||||||
buildProc.unload();
|
if (stream.value) {
|
||||||
|
stream.value.close();
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', resize);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { logLines, proc };
|
return { proc, loadedLogs };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,19 @@
|
||||||
<template>
|
<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 flex-col w-full md:w-3/12 text-gray-600 dark:text-gray-400">
|
||||||
<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
|
||||||
|
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 space-x-1 items-center flex-shrink-0">
|
||||||
<div class="flex items-center"><img class="w-6" :src="build.author_avatar" /></div>
|
<div class="flex items-center"><img class="w-6" :src="build.author_avatar" /></div>
|
||||||
<span>{{ build.author }}</span>
|
<span>{{ build.author }}</span>
|
||||||
|
@ -25,7 +38,7 @@
|
||||||
<Icon name="commit" />
|
<Icon name="commit" />
|
||||||
<span>{{ build.commit.slice(0, 10) }}</span>
|
<span>{{ build.commit.slice(0, 10) }}</span>
|
||||||
</template>
|
</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" />
|
<Icon name="commit" />
|
||||||
<span>{{ build.commit.slice(0, 10) }}</span>
|
<span>{{ build.commit.slice(0, 10) }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -40,7 +53,9 @@
|
||||||
<div class="md:absolute top-0 left-0 w-full">
|
<div class="md:absolute top-0 left-0 w-full">
|
||||||
<div v-for="proc in build.procs" :key="proc.id">
|
<div v-for="proc in build.procs" :key="proc.id">
|
||||||
<div class="p-4 pb-1 flex flex-wrap items-center justify-between">
|
<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-if="proc.environ" class="text-xs">
|
||||||
<div v-for="(value, key) in proc.environ" :key="key">
|
<div v-for="(value, key) in proc.environ" :key="key">
|
||||||
<span
|
<span
|
||||||
|
@ -49,6 +64,7 @@
|
||||||
pr-1
|
pr-1
|
||||||
py-0.5
|
py-0.5
|
||||||
bg-gray-800
|
bg-gray-800
|
||||||
|
text-gray-200
|
||||||
dark:bg-gray-600
|
dark:bg-gray-600
|
||||||
border-2 border-gray-800
|
border-2 border-gray-800
|
||||||
dark:border-gray-600
|
dark:border-gray-600
|
||||||
|
@ -65,8 +81,18 @@
|
||||||
<div
|
<div
|
||||||
v-for="job in proc.children"
|
v-for="job in proc.children"
|
||||||
:key="job.pid"
|
:key="job.pid"
|
||||||
class="flex p-2 pl-6 cursor-pointer items-center hover:bg-gray-700 hover:dark:bg-dark-gray-900"
|
class="
|
||||||
:class="{ 'bg-gray-700 !dark:bg-dark-gray-600': selectedProcId && selectedProcId === job.pid }"
|
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)"
|
@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" />
|
<div v-if="['success'].includes(job.state)" class="w-2 h-2 bg-lime-400 rounded-full" />
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import { BuildLog, BuildProc } from '~/lib/api/types';
|
|
||||||
import { isProcFinished, isProcRunning } from '~/utils/helpers';
|
|
||||||
|
|
||||||
import useApiClient from './useApiClient';
|
|
||||||
|
|
||||||
const apiClient = useApiClient();
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const logs = ref<BuildLog[] | undefined>();
|
|
||||||
const proc = ref<BuildProc>();
|
|
||||||
let stream: EventSource | undefined;
|
|
||||||
|
|
||||||
function onLogsUpdate(data: BuildLog) {
|
|
||||||
if (data.proc === proc.value?.name) {
|
|
||||||
logs.value = [...(logs.value || []), data];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unload() {
|
|
||||||
if (stream) {
|
|
||||||
stream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load(owner: string, repo: string, build: number, _proc: BuildProc) {
|
|
||||||
unload();
|
|
||||||
|
|
||||||
proc.value = _proc;
|
|
||||||
logs.value = [];
|
|
||||||
|
|
||||||
// we do not have logs for skipped jobs
|
|
||||||
if (_proc.state === 'skipped' || _proc.state === 'killed') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_proc.error) {
|
|
||||||
logs.value = undefined;
|
|
||||||
} else if (isProcFinished(_proc)) {
|
|
||||||
logs.value = await apiClient.getLogs(owner, repo, build, _proc.pid);
|
|
||||||
} else if (isProcRunning(_proc)) {
|
|
||||||
// load stream of parent process (which receives all child processes logs)
|
|
||||||
stream = apiClient.streamLogs(owner, repo, build, _proc.ppid, onLogsUpdate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { logs, load, unload };
|
|
||||||
};
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex w-full mb-4 justify-center">
|
<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>
|
</div>
|
||||||
<BuildList :builds="builds" :repo="repo" />
|
<BuildList :builds="builds" :repo="repo" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="p-0 flex flex-col flex-grow">
|
<div class="flex flex-col flex-grow">
|
||||||
<div class="flex w-full min-h-0 flex-grow">
|
<div class="flex w-full min-h-0 flex-grow">
|
||||||
<BuildProcList v-model:selected-proc-id="selectedProcId" :build="build" />
|
<BuildProcList v-model:selected-proc-id="selectedProcId" :build="build" />
|
||||||
|
|
||||||
<div class="flex flex-grow relative">
|
<div class="flex flex-grow relative">
|
||||||
<div v-if="build.error" class="flex flex-col p-4">
|
<div v-if="error" class="flex flex-col p-4">
|
||||||
<span class="text-red-400 font-bold text-xl mb-2">{{ $t('repo.build.execution_error') }}</span>
|
<span class="text-red-400 font-bold text-xl mb-2">{{ $t('repo.build.execution_error') }}</span>
|
||||||
<span class="text-red-400">{{ build.error }}</span>
|
<span class="text-red-400">{{ error }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="build.status === 'blocked'" class="flex flex-col flex-grow justify-center items-center">
|
<div v-else-if="build.status === 'blocked'" class="flex flex-col flex-grow justify-center items-center">
|
||||||
|
@ -56,6 +56,7 @@ import useApiClient from '~/compositions/useApiClient';
|
||||||
import { useAsyncAction } from '~/compositions/useAsyncAction';
|
import { useAsyncAction } from '~/compositions/useAsyncAction';
|
||||||
import useNotifications from '~/compositions/useNotifications';
|
import useNotifications from '~/compositions/useNotifications';
|
||||||
import { Build, BuildProc, Repo, RepoPermissions } from '~/lib/api/types';
|
import { Build, BuildProc, Repo, RepoPermissions } from '~/lib/api/types';
|
||||||
|
import { findProc } from '~/utils/helpers';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Build',
|
name: 'Build',
|
||||||
|
@ -132,6 +133,9 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedProc = computed(() => findProc(build.value.procs || [], selectedProcId.value || -1));
|
||||||
|
const error = computed(() => build.value?.error || selectedProc.value?.error);
|
||||||
|
|
||||||
const { doSubmit: approveBuild, isLoading: isApprovingBuild } = useAsyncAction(async () => {
|
const { doSubmit: approveBuild, isLoading: isApprovingBuild } = useAsyncAction(async () => {
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
throw new Error('Unexpected: Repo is undefined');
|
throw new Error('Unexpected: Repo is undefined');
|
||||||
|
@ -154,6 +158,7 @@ export default defineComponent({
|
||||||
repoPermissions,
|
repoPermissions,
|
||||||
selectedProcId,
|
selectedProcId,
|
||||||
build,
|
build,
|
||||||
|
error,
|
||||||
isApprovingBuild,
|
isApprovingBuild,
|
||||||
isDecliningBuild,
|
isDecliningBuild,
|
||||||
approveBuild,
|
approveBuild,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<template v-if="build && repo">
|
<template v-if="build && repo">
|
||||||
<FluidContainer class="flex flex-col min-w-0 border-b dark:border-gray-600 !pb-0 mb-4">
|
<FluidContainer class="flex flex-col min-w-0 dark:border-gray-600">
|
||||||
<div class="flex mb-2 items-center <md:flex-wrap">
|
<div class="flex mb-2 items-center <md:flex-wrap">
|
||||||
<IconButton icon="back" class="flex-shrink-0" @click="goBack" />
|
<IconButton icon="back" class="flex-shrink-0" @click="goBack" />
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
<Tabs v-model="activeTab" disable-hash-mode class="order-2 md:order-none">
|
<Tabs v-model="activeTab" disable-hash-mode class="order-2 md:order-none">
|
||||||
<Tab id="tasks" :title="$t('repo.build.tasks')" />
|
<Tab id="tasks" :title="$t('repo.build.tasks')" />
|
||||||
<Tab id="config" :title="$t('repo.build.config')" />
|
<Tab id="config" :title="$t('repo.build.config')" />
|
||||||
<Tab id="changed-files" :title="$t('repo.build.files', [build.changed_files?.length || 0])" />
|
<Tab id="changed-files" :title="$t('repo.build.files', { files: build.changed_files?.length || 0 })" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div class="flex justify-between gap-x-4 text-gray-500 flex-shrink-0 pb-2 md:p-0 mx-auto md:mr-0">
|
<div class="flex justify-between gap-x-4 text-gray-500 flex-shrink-0 pb-2 md:p-0 mx-auto md:mr-0">
|
||||||
|
|
|
@ -3242,6 +3242,21 @@ wrappy@1:
|
||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||||
|
|
||||||
|
xterm-addon-fit@0.5.0:
|
||||||
|
version "0.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
|
||||||
|
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
|
||||||
|
|
||||||
|
xterm-addon-web-links@0.5.1:
|
||||||
|
version "0.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.5.1.tgz#73bfa3ed567af98fba947f638bd12093ee2a0bc6"
|
||||||
|
integrity sha512-dBjbOIrCNmxAcUQkkSrKj9BM6yLpmqUpZ9SOCUuZe/sznPl4d8OBZQClK7VcdZ0vf0+5i5Fce2rUUrew/XTZTg==
|
||||||
|
|
||||||
|
xterm@4.17.0:
|
||||||
|
version "4.17.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.17.0.tgz#e48ba6eeb83e118ec163f5512c3cfe9dbbf3f838"
|
||||||
|
integrity sha512-WGXlIHvLvZKtwMdFaL6kUwp+c9abd2Pcakp/GmuefBuOtGCu9fP9tBDPKyL/A17N+5tt44EYk3YsBbvkPBubMw==
|
||||||
|
|
||||||
yallist@^4.0.0:
|
yallist@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||||
|
|
Loading…
Reference in a new issue