aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--options/locale/locale_en-US.ini4
-rw-r--r--templates/repo/actions/view.tmpl3
-rw-r--r--web_src/css/base.css13
-rw-r--r--web_src/css/themes/theme-arc-green.css11
-rw-r--r--web_src/js/components/RepoActionView.vue217
-rw-r--r--web_src/js/svg.js4
-rw-r--r--web_src/js/utils.js2
7 files changed, 222 insertions, 32 deletions
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index e4fb50bbcb..bf6e4b7524 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -125,6 +125,10 @@ concept_user_individual = Individual
concept_code_repository = Repository
concept_user_organization = Organization
+show_timestamps = Show timestamps
+show_log_seconds = Show seconds
+show_full_screen = Show full screen
+
[aria]
navbar = Navigation Bar
footer = Footer
diff --git a/templates/repo/actions/view.tmpl b/templates/repo/actions/view.tmpl
index 3a3a069cbc..297232fca0 100644
--- a/templates/repo/actions/view.tmpl
+++ b/templates/repo/actions/view.tmpl
@@ -19,6 +19,9 @@
data-locale-status-skipped="{{.locale.Tr "actions.status.skipped"}}"
data-locale-status-blocked="{{.locale.Tr "actions.status.blocked"}}"
data-locale-artifacts-title="{{$.locale.Tr "artifacts"}}"
+ data-locale-show-timestamps="{{.locale.Tr "show_timestamps"}}"
+ data-locale-show-log-seconds="{{.locale.Tr "show_log_seconds"}}"
+ data-locale-show-full-screen="{{.locale.Tr "show_full_screen"}}"
>
</div>
</div>
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 204a9f0ce9..9bee0e4dcb 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -74,12 +74,15 @@
--color-secondary-alpha-90: #dededee1;
--color-secondary-hover: var(--color-secondary-dark-1);
--color-secondary-active: var(--color-secondary-dark-2);
- /* console colors */
- --color-console-fg: #ffffff;
- --color-console-bg: #252a2f;
- --color-console-border: #ffffff16;
+ /* console colors - used for actions console and console files */
+ --color-console-fg: #eeeff2;
+ --color-console-fg-subtle: #959cab;
+ --color-console-bg: #262936;
+ --color-console-border: #383c47;
--color-console-hover-bg: #ffffff16;
- --color-console-active-bg: #353a3f;
+ --color-console-active-bg: #454a57;
+ --color-console-menu-bg: #383c47;
+ --color-console-menu-border: #5c6374;
/* named colors */
--color-red: #db2828;
--color-orange: #f2711c;
diff --git a/web_src/css/themes/theme-arc-green.css b/web_src/css/themes/theme-arc-green.css
index 3c2a81470f..f6321c9ed7 100644
--- a/web_src/css/themes/theme-arc-green.css
+++ b/web_src/css/themes/theme-arc-green.css
@@ -60,12 +60,15 @@
--color-secondary-alpha-90: #525767e1;
--color-secondary-hover: var(--color-secondary-light-1);
--color-secondary-active: var(--color-secondary-light-2);
- /* console colors */
- --color-console-fg: #ffffff;
+ /* console colors - used for actions console and console files */
+ --color-console-fg: #eeeff2;
+ --color-console-fg-subtle: #959cab;
--color-console-bg: #262936;
- --color-console-border: #ffffff16;
+ --color-console-border: #383c47;
--color-console-hover-bg: #ffffff16;
- --color-console-active-bg: #383c47;
+ --color-console-active-bg: #454a57;
+ --color-console-menu-bg: #383c47;
+ --color-console-menu-border: #5c6374;
/* named colors */
--color-red: #cc4848;
--color-orange: #cc580c;
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 168dacfb4d..704ffa0706 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -60,14 +60,38 @@
<div class="action-view-right">
<div class="job-info-header">
- <h3 class="job-info-header-title">
- {{ currentJob.title }}
- </h3>
- <p class="job-info-header-detail">
- {{ currentJob.detail }}
- </p>
+ <div class="job-info-header-left">
+ <h3 class="job-info-header-title">
+ {{ currentJob.title }}
+ </h3>
+ <p class="job-info-header-detail">
+ {{ currentJob.detail }}
+ </p>
+ </div>
+ <div class="job-info-header-right">
+ <div class="ui top right pointing dropdown custom jump item" @click.stop="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
+ <button class="ui button button-ghost gt-p-3">
+ <SvgIcon name="octicon-gear" :size="18"/>
+ </button>
+ <div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
+ <a class="item" @click="toggleTimeDisplay('seconds')">
+ <i class="icon"><SvgIcon v-show="timeVisible['log-time-seconds']" name="octicon-check"/></i>
+ {{ locale.showLogSeconds }}
+ </a>
+ <a class="item" @click="toggleTimeDisplay('stamp')">
+ <i class="icon"><SvgIcon v-show="timeVisible['log-time-stamp']" name="octicon-check"/></i>
+ {{ locale.showTimeStamps }}
+ </a>
+ <div class="divider"/>
+ <a class="item" @click="toggleFullScreen()">
+ <i class="icon"><SvgIcon v-show="isFullScreen" name="octicon-check"/></i>
+ {{ locale.showFullScreen }}
+ </a>
+ </div>
+ </div>
+ </div>
</div>
- <div class="job-step-container">
+ <div class="job-step-container" ref="steps">
<div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i">
<div class="job-step-summary" @click.stop="toggleStepLogs(i)" :class="currentJobStepsStates[i].expanded ? 'selected' : ''">
<!-- If the job is done and the job step log is loaded for the first time, show the loading icon
@@ -81,7 +105,8 @@
<span class="step-summary-duration">{{ jobStep.duration }}</span>
</div>
- <!-- the log elements could be a lot, do not use v-if to destroy/reconstruct the DOM -->
+ <!-- the log elements could be a lot, do not use v-if to destroy/reconstruct the DOM,
+ use native DOM elements for "log line" to improve performance, Vue is not suitable for managing so many reactive elements. -->
<div class="job-step-logs" ref="logs" v-show="currentJobStepsStates[i].expanded"/>
</div>
</div>
@@ -95,6 +120,8 @@ import {SvgIcon} from '../svg.js';
import ActionRunStatus from './ActionRunStatus.vue';
import {createApp} from 'vue';
import AnsiToHTML from 'ansi-to-html';
+import {toggleElem} from '../utils/dom.js';
+import {getCurrentLocale} from '../utils.js';
const {csrfToken} = window.config;
@@ -121,6 +148,12 @@ const sfc = {
currentJobStepsStates: [],
artifacts: [],
onHoverRerunIndex: -1,
+ menuVisible: false,
+ isFullScreen: false,
+ timeVisible: {
+ 'log-time-stamp': false,
+ 'log-time-seconds': false,
+ },
// provided by backend
run: {
@@ -173,6 +206,11 @@ const sfc = {
// load job data and then auto-reload periodically
this.loadJob();
this.intervalID = setInterval(this.loadJob, 1000);
+ document.body.addEventListener('click', this.closeDropdown);
+ },
+
+ beforeUnmount() {
+ document.body.removeEventListener('click', this.closeDropdown);
},
unmounted() {
@@ -240,7 +278,7 @@ const sfc = {
this.fetchPost(`${this.run.link}/approve`);
},
- createLogLine(line) {
+ createLogLine(line, startTime) {
const div = document.createElement('div');
div.classList.add('job-log-line');
div._jobLogTime = line.timestamp;
@@ -250,21 +288,35 @@ const sfc = {
lineNumber.textContent = line.index;
div.append(lineNumber);
- // TODO: Support displaying time optionally
-
- const logMessage = document.createElement('div');
+ // for "Show timestamps"
+ const logTimeStamp = document.createElement('span');
+ logTimeStamp.className = 'log-time-stamp';
+ const date = new Date(parseFloat(line.timestamp * 1000));
+ const timeStamp = date.toLocaleString(getCurrentLocale(), {timeZoneName: 'short'});
+ logTimeStamp.textContent = timeStamp;
+ toggleElem(logTimeStamp, this.timeVisible['log-time-stamp']);
+ // for "Show seconds"
+ const logTimeSeconds = document.createElement('span');
+ logTimeSeconds.className = 'log-time-seconds';
+ const seconds = Math.floor(parseFloat(line.timestamp) - parseFloat(startTime));
+ logTimeSeconds.textContent = `${seconds}s`;
+ toggleElem(logTimeSeconds, this.timeVisible['log-time-seconds']);
+
+ const logMessage = document.createElement('span');
logMessage.className = 'log-msg';
logMessage.innerHTML = ansiLogToHTML(line.message);
+ div.append(logTimeStamp);
div.append(logMessage);
+ div.append(logTimeSeconds);
return div;
},
- appendLogs(stepIndex, logLines) {
+ appendLogs(stepIndex, logLines, startTime) {
for (const line of logLines) {
// TODO: group support: ##[group]GroupTitle , ##[endgroup]
const el = this.getLogsContainer(stepIndex);
- el.append(this.createLogLine(line));
+ el.append(this.createLogLine(line, startTime));
}
},
@@ -309,7 +361,7 @@ const sfc = {
for (const logs of response.logs.stepsLog) {
// save the cursor, it will be passed to backend next time
this.currentJobStepsStates[logs.step].cursor = logs.cursor;
- this.appendLogs(logs.step, logs.lines);
+ this.appendLogs(logs.step, logs.lines, logs.started);
}
if (this.run.done && this.intervalID) {
@@ -335,6 +387,46 @@ const sfc = {
isDone(status) {
return ['success', 'skipped', 'failure', 'cancelled'].includes(status);
+ },
+
+ closeDropdown() {
+ if (this.menuVisible) this.menuVisible = false;
+ },
+
+ // show at most one of log seconds and timestamp (can be both invisible)
+ toggleTimeDisplay(type) {
+ const toToggleTypes = [];
+ const other = type === 'seconds' ? 'stamp' : 'seconds';
+ this.timeVisible[`log-time-${type}`] = !this.timeVisible[`log-time-${type}`];
+ toToggleTypes.push(type);
+ if (this.timeVisible[`log-time-${type}`] && this.timeVisible[`log-time-${other}`]) {
+ this.timeVisible[`log-time-${other}`] = false;
+ toToggleTypes.push(other);
+ }
+ for (const toToggle of toToggleTypes) {
+ for (const el of this.$refs.steps.querySelectorAll(`.log-time-${toToggle}`)) {
+ toggleElem(el, this.timeVisible[`log-time-${toToggle}`]);
+ }
+ }
+ },
+
+ toggleFullScreen() {
+ this.isFullScreen = !this.isFullScreen;
+ const fullScreenEl = document.querySelector('.action-view-right');
+ const outerEl = document.querySelector('.full.height');
+ const actionBodyEl = document.querySelector('.action-view-body');
+ const headerEl = document.querySelector('.ui.main.menu');
+ const contentEl = document.querySelector('.page-content.repository');
+ const footerEl = document.querySelector('.page-footer');
+ toggleElem(headerEl, !this.isFullScreen);
+ toggleElem(contentEl, !this.isFullScreen);
+ toggleElem(footerEl, !this.isFullScreen);
+ // move .action-view-right to new parent
+ if (this.isFullScreen) {
+ outerEl.append(fullScreenEl);
+ } else {
+ actionBodyEl.append(fullScreenEl);
+ }
}
},
};
@@ -360,6 +452,9 @@ export function initRepositoryActionView() {
rerun: el.getAttribute('data-locale-rerun'),
artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
rerun_all: el.getAttribute('data-locale-rerun-all'),
+ showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
+ showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
+ showFullScreen: el.getAttribute('data-locale-show-full-screen'),
status: {
unknown: el.getAttribute('data-locale-status-unknown'),
waiting: el.getAttribute('data-locale-status-waiting'),
@@ -369,7 +464,7 @@ export function initRepositoryActionView() {
cancelled: el.getAttribute('data-locale-status-cancelled'),
skipped: el.getAttribute('data-locale-status-skipped'),
blocked: el.getAttribute('data-locale-status-blocked'),
- }
+ },
}
});
view.mount(el);
@@ -567,21 +662,95 @@ export function ansiLogToHTML(line) {
.action-view-right {
flex: 1;
- color: var(--color-secondary-dark-3);
+ color: var(--color-console-fg-subtle);
max-height: 100%;
width: 70%;
display: flex;
flex-direction: column;
}
+/* begin fomantic button overrides */
+
+.action-view-right .ui.button,
+.action-view-right .ui.button:focus {
+ background: transparent;
+ color: var(--color-console-fg-subtle);
+}
+
+.action-view-right .ui.button:hover {
+ background: var(--color-console-hover-bg);
+ color: var(--color-console-fg);
+}
+
+.action-view-right .ui.button:active {
+ background: var(--color-console-active-bg);
+ color: var(--color-console-fg);
+}
+
+/* end fomantic button overrides */
+
+/* begin fomantic dropdown menu overrides */
+
+.action-view-right .ui.dropdown .menu {
+ background: var(--color-console-menu-bg);
+ border-color: var(--color-console-menu-border);
+}
+
+.action-view-right .ui.dropdown .menu > .item {
+ color: var(--color-console-fg);
+}
+
+.action-view-right .ui.dropdown .menu > .item:hover {
+ color: var(--color-console-fg);
+ background: var(--color-console-hover-bg);
+}
+
+.action-view-right .ui.dropdown .menu > .item:active {
+ color: var(--color-console-fg);
+ background: var(--color-console-active-bg);
+}
+
+.action-view-right .ui.dropdown .menu > .divider {
+ border-top-color: var(--color-console-menu-border);
+}
+
+.action-view-right .ui.pointing.dropdown > .menu:not(.hidden)::after {
+ background: var(--color-console-menu-bg);
+ box-shadow: -1px -1px 0 0 var(--color-console-menu-border);
+}
+
+/* end fomantic dropdown menu overrides */
+
+/* selectors here are intentionally exact to only match fullscreen */
+
+.full.height > .action-view-right {
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ border-radius: 0;
+}
+
+.full.height > .action-view-right > .job-info-header {
+ border-radius: 0;
+}
+
+.full.height > .action-view-right > .job-step-container {
+ height: calc(100% - 60px);
+ border-radius: 0;
+}
+
.job-info-header {
- padding: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 12px;
border-bottom: 1px solid var(--color-console-border);
background-color: var(--color-console-bg);
position: sticky;
top: 0;
border-radius: var(--border-radius) var(--border-radius) 0 0;
height: 60px;
+ z-index: 1;
}
.job-info-header .job-info-header-title {
@@ -591,7 +760,7 @@ export function ansiLogToHTML(line) {
}
.job-info-header .job-info-header-detail {
- color: var(--color-secondary-dark-3);
+ color: var(--color-console-fg-subtle);
font-size: 12px;
}
@@ -676,14 +845,20 @@ export function ansiLogToHTML(line) {
background-color: var(--color-console-hover-bg);
}
-.job-step-section .job-step-logs .job-log-line .line-num {
+/* class names 'log-time-seconds' and 'log-time-stamp' are used in the method toggleTimeDisplay */
+.job-log-line .line-num, .log-time-seconds {
width: 48px;
color: var(--color-grey-light);
text-align: right;
user-select: none;
}
-.job-step-section .job-step-logs .job-log-line .log-time {
+.log-time-seconds {
+ padding-right: 2px;
+}
+
+.job-log-line .log-time,
+.log-time-stamp {
color: var(--color-grey-light);
margin-left: 10px;
white-space: nowrap;
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 49376c1643..1ea0eb8347 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -26,6 +26,7 @@ import octiconEye from '../../public/img/svg/octicon-eye.svg';
import octiconFile from '../../public/img/svg/octicon-file.svg';
import octiconFileDirectoryFill from '../../public/img/svg/octicon-file-directory-fill.svg';
import octiconFilter from '../../public/img/svg/octicon-filter.svg';
+import octiconGear from '../../public/img/svg/octicon-gear.svg';
import octiconGitBranch from '../../public/img/svg/octicon-git-branch.svg';
import octiconGitMerge from '../../public/img/svg/octicon-git-merge.svg';
import octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg';
@@ -94,6 +95,7 @@ const svgs = {
'octicon-file': octiconFile,
'octicon-file-directory-fill': octiconFileDirectoryFill,
'octicon-filter': octiconFilter,
+ 'octicon-gear': octiconGear,
'octicon-git-branch': octiconGitBranch,
'octicon-git-merge': octiconGitMerge,
'octicon-git-pull-request': octiconGitPullRequest,
@@ -132,7 +134,7 @@ const svgs = {
'octicon-tag': octiconTag,
'octicon-triangle-down': octiconTriangleDown,
'octicon-x': octiconX,
- 'octicon-x-circle-fill': octiconXCircleFill
+ 'octicon-x-circle-fill': octiconXCircleFill,
};
// TODO: use a more general approach to access SVG icons.
diff --git a/web_src/js/utils.js b/web_src/js/utils.js
index 2a2d6df0b4..6bee4f0836 100644
--- a/web_src/js/utils.js
+++ b/web_src/js/utils.js
@@ -60,7 +60,7 @@ export function parseUrl(str) {
}
// return current locale chosen by user
-function getCurrentLocale() {
+export function getCurrentLocale() {
return document.documentElement.lang;
}