diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json
index 1ea927e74d..1f3fc46b36 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 7b8d3a1db4..798028f302 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 64a86697e3..7728deb476 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 54c1901368..6d05b830f7 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 b5bca6780c..a09583407b 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 9b8edefeb8..eac86b24a5 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 });
]