mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-23 02:11:01 +00:00
Add pipeline log output download (#1023)
This commit is contained in:
parent
73ee12a6c3
commit
f654e1f316
6 changed files with 84 additions and 8 deletions
|
@ -178,13 +178,15 @@
|
|||
"exit_code": "exit code {exitCode}",
|
||||
"loading": "Loading ...",
|
||||
"pipeline": "Pipeline #{buildId}",
|
||||
"log_download_error": "There was an error while downloading the log file",
|
||||
|
||||
"actions": {
|
||||
"cancel": "Cancel",
|
||||
"restart": "Restart",
|
||||
"canceled": "This step has been canceled.",
|
||||
"cancel_success": "Pipeline canceled",
|
||||
"restart_success": "Pipeline restarted"
|
||||
"restart_success": "Pipeline restarted",
|
||||
"log_download": "Download"
|
||||
},
|
||||
"protected": {
|
||||
"awaits": "This pipeline is awaiting approval by some maintainer!",
|
||||
|
|
|
@ -178,13 +178,15 @@
|
|||
"exit_code": "iziešanas kods {exitCode}",
|
||||
"loading": "Notiek ielāde...",
|
||||
"pipeline": "Konvejerdarbs #{buildId}",
|
||||
"log_download_error": "Veicot žurnālfaila lejupielādi notika kļūda",
|
||||
|
||||
"actions": {
|
||||
"cancel": "Atcelt",
|
||||
"restart": "Pārstartēt",
|
||||
"canceled": "Šis solis tika atcelts.",
|
||||
"cancel_success": "Konvejerdarbs atcelts",
|
||||
"restart_success": "Konvejerdarbs pārstartēts"
|
||||
"restart_success": "Konvejerdarbs pārstartēts",
|
||||
"log_download": "Lejupielādēt"
|
||||
},
|
||||
"protected": {
|
||||
"awaits": "Šim konvejerdarbam ir nepieciešams apstiprinājums no atbildīgajām personām!",
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
color === 'red',
|
||||
...passedClasses,
|
||||
}"
|
||||
:title="title"
|
||||
:disabled="disabled"
|
||||
@click="doClick"
|
||||
>
|
||||
|
@ -69,6 +70,11 @@ export default defineComponent({
|
|||
default: null,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
<i-mdi-chevron-right v-else-if="name === 'chevron-right'" class="h-6 w-6" />
|
||||
<i-carbon-close-outline v-else-if="name === 'close'" class="h-6 w-6" />
|
||||
<i-ic-baseline-edit v-else-if="name === 'edit'" class="h-6 w-6" />
|
||||
<i-ic-baseline-download v-else-if="name === 'download'" class="h-6 w-6" />
|
||||
<div v-else-if="name === 'blank'" class="h-6 w-6" />
|
||||
</template>
|
||||
|
||||
|
@ -80,7 +81,8 @@ export type IconNames =
|
|||
| 'chevron-right'
|
||||
| 'turn-off'
|
||||
| 'close'
|
||||
| 'edit';
|
||||
| 'edit'
|
||||
| 'download';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Icon',
|
||||
|
|
|
@ -8,7 +8,21 @@
|
|||
<Icon name="close" class="ml-auto" />
|
||||
</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
|
||||
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"
|
||||
@mouseover="showActions = true"
|
||||
@mouseleave="showActions = false"
|
||||
>
|
||||
<div v-show="showActions" class="absolute top-0 right-0 z-50 mt-2 mr-4 hidden md:flex">
|
||||
<Button
|
||||
v-if="proc?.end_time !== undefined"
|
||||
:is-loading="downloadInProgress"
|
||||
:title="$t('repo.build.actions.log_download')"
|
||||
start-icon="download"
|
||||
@click="download"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="loadedLogs" class="w-full flex-grow p-2">
|
||||
<div id="terminal" class="w-full h-full" />
|
||||
</div>
|
||||
|
@ -47,20 +61,23 @@ import {
|
|||
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';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BuildLog',
|
||||
|
||||
components: { Icon },
|
||||
components: { Icon, Button },
|
||||
|
||||
props: {
|
||||
build: {
|
||||
|
@ -82,6 +99,8 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setup(props) {
|
||||
const notifications = useNotifications();
|
||||
const i18n = useI18n();
|
||||
const build = toRef(props, 'build');
|
||||
const procId = toRef(props, 'procId');
|
||||
const repo = inject<Ref<Repo>>('repo');
|
||||
|
@ -103,6 +122,41 @@ export default defineComponent({
|
|||
const fitAddon = ref(new FitAddon());
|
||||
const loadedLogs = ref(true);
|
||||
const autoScroll = ref(true); // TODO
|
||||
const showActions = ref(false);
|
||||
const downloadInProgress = ref(false);
|
||||
|
||||
async function download() {
|
||||
if (!repo?.value || !build.value || !proc.value) {
|
||||
throw new Error('The reposiotry, build or proc was undefined');
|
||||
}
|
||||
let logs;
|
||||
try {
|
||||
downloadInProgress.value = true;
|
||||
logs = await apiClient.getLogs(repo.value.owner, repo.value.name, build.value.number, proc.value.pid);
|
||||
} catch (e) {
|
||||
notifications.notifyError(e, i18n.t('repo.build.log_download_error'));
|
||||
return;
|
||||
} finally {
|
||||
downloadInProgress.value = false;
|
||||
}
|
||||
const fileURL = window.URL.createObjectURL(
|
||||
new Blob([logs.map((line) => line.out).join('')], {
|
||||
type: 'text/plain',
|
||||
}),
|
||||
);
|
||||
const fileLink = document.createElement('a');
|
||||
|
||||
fileLink.href = fileURL;
|
||||
fileLink.setAttribute(
|
||||
'download',
|
||||
`${repo.value.owner}-${repo.value.name}-${build.value.number}-${proc.value.name}.log`,
|
||||
);
|
||||
document.body.appendChild(fileLink);
|
||||
|
||||
fileLink.click();
|
||||
document.body.removeChild(fileLink);
|
||||
window.URL.revokeObjectURL(fileURL);
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
if (loadedProcSlug.value === procSlug.value) {
|
||||
|
@ -231,7 +285,7 @@ export default defineComponent({
|
|||
window.removeEventListener('resize', resize);
|
||||
});
|
||||
|
||||
return { proc, loadedLogs };
|
||||
return { proc, loadedLogs, showActions, download, downloadInProgress };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
import Notifications, { notify } from '@kyvg/vue3-notification';
|
||||
import Notifications, { NotificationsOptions, notify } from '@kyvg/vue3-notification';
|
||||
|
||||
export const notifications = Notifications;
|
||||
|
||||
export default () => ({ notify });
|
||||
function notifyError(err: unknown, args: NotificationsOptions | string = {}): void {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
|
||||
const mArgs = typeof args === 'string' ? { title: args } : args;
|
||||
const title = mArgs?.title || (err as Error)?.message || `${err}`;
|
||||
|
||||
notify({ type: 'error', ...mArgs, title });
|
||||
}
|
||||
|
||||
export default () => ({ notify, notifyError });
|
||||
|
|
Loading…
Reference in a new issue