From 1ea5c8b0ff5ca9f81a06b195a7dff870d784cb02 Mon Sep 17 00:00:00 2001 From: HesterG Date: Wed, 31 May 2023 04:38:55 +0800 Subject: [PATCH] Add show timestamp/seconds and fullscreen options to action page (#24876) Part of #24728 - The timestamp shows local time and is parsed by `date.toLocaleString`; - "show seconds" and "show timestamps" are mutually exclusive, and they can be both hidden. https://github.com/go-gitea/gitea/assets/17645053/89531e54-37b7-4400-a6a0-bb3cc69eb6f5 Update for timestamp format: Screen Shot 2023-05-25 at 09 07 47 --------- Co-authored-by: silverwind Co-authored-by: wxiaoguang --- options/locale/locale_en-US.ini | 4 + templates/repo/actions/view.tmpl | 3 + web_src/css/base.css | 13 +- web_src/css/themes/theme-arc-green.css | 11 +- web_src/js/components/RepoActionView.vue | 215 ++++++++++++++++++++--- web_src/js/svg.js | 4 +- web_src/js/utils.js | 2 +- 7 files changed, 221 insertions(+), 31 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"}}" > 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 @@
-

- {{ currentJob.title }} -

-

- {{ currentJob.detail }} -

+
+

+ {{ currentJob.title }} +

+

+ {{ currentJob.detail }} +

+
+ -
+
+
@@ -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 + // 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('div'); + 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; }