diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 1ea927e74..1f3fc46b3 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -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!", diff --git a/web/src/assets/locales/lv.json b/web/src/assets/locales/lv.json index 7b8d3a1db..798028f30 100644 --- a/web/src/assets/locales/lv.json +++ b/web/src/assets/locales/lv.json @@ -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!", diff --git a/web/src/components/atomic/Button.vue b/web/src/components/atomic/Button.vue index 64a86697e..7728deb47 100644 --- a/web/src/components/atomic/Button.vue +++ b/web/src/components/atomic/Button.vue @@ -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, diff --git a/web/src/components/atomic/Icon.vue b/web/src/components/atomic/Icon.vue index 54c190136..6d05b830f 100644 --- a/web/src/components/atomic/Icon.vue +++ b/web/src/components/atomic/Icon.vue @@ -36,6 +36,7 @@ +
@@ -80,7 +81,8 @@ export type IconNames = | 'chevron-right' | 'turn-off' | 'close' - | 'edit'; + | 'edit' + | 'download'; export default defineComponent({ name: 'Icon', diff --git a/web/src/components/repo/build/BuildLog.vue b/web/src/components/repo/build/BuildLog.vue index b5bca6780..a09583407 100644 --- a/web/src/components/repo/build/BuildLog.vue +++ b/web/src/components/repo/build/BuildLog.vue @@ -8,7 +8,21 @@
-
+
+ +
@@ -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>('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 }; }, }); diff --git a/web/src/compositions/useNotifications.ts b/web/src/compositions/useNotifications.ts index 9b8edefeb..eac86b24a 100644 --- a/web/src/compositions/useNotifications.ts +++ b/web/src/compositions/useNotifications.ts @@ -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 });