view logs

This commit is contained in:
wxiaoguang 2022-09-30 01:09:56 +08:00 committed by Jason Song
parent 4f2330623e
commit 7ba22b740f
2 changed files with 109 additions and 53 deletions

View File

@ -9,7 +9,11 @@
</div> </div>
<div class="job-brief-list"> <div class="job-brief-list">
<a class="job-brief-item" v-for="job in jobGroup.jobs" :key="job.id"> <a class="job-brief-item" v-for="job in jobGroup.jobs" :key="job.id">
<SvgIcon name="octicon-check-circle-fill"/> {{ job.name }} <SvgIcon name="octicon-check-circle-fill" class="green" v-if="job.status === 'success'"/>
<SvgIcon name="octicon-skip" class="ui text grey" v-else-if="job.status === 'skipped'"/>
<SvgIcon name="octicon-clock" class="ui text yellow" v-else-if="job.status === 'waiting'"/>
<SvgIcon name="octicon-x-circle-fill" class="red" v-else/>
{{ job.name }}
</a> </a>
</div> </div>
</div> </div>
@ -24,19 +28,23 @@
{{ currentJobInfo.detail }} {{ currentJobInfo.detail }}
</div> </div>
</div> </div>
<div class="job-stage-container"> <div class="job-step-container">
<div class="job-stage-section" v-for="(jobStage, i) in currentJobStages" :key="i"> <div class="job-step-section" v-for="(jobStep, i) in currentJobSteps" :key="i">
<div class="job-stage-summary" @click.stop="toggleStageLogs(i)"> <div class="job-step-summary" @click.stop="toggleStepLogs(i)">
<SvgIcon name="octicon-chevron-down" v-show="currentJobStagesStates[i].expanded"/> <SvgIcon name="octicon-chevron-down" class="mr-3" v-show="currentJobStepsStates[i].expanded"/>
<SvgIcon name="octicon-chevron-right" v-show="!currentJobStagesStates[i].expanded"/> <SvgIcon name="octicon-chevron-right" class="mr-3" v-show="!currentJobStepsStates[i].expanded"/>
<SvgIcon name="octicon-check-circle-fill"/> <SvgIcon name="octicon-check-circle-fill" class="green mr-3 " v-if="jobStep.status === 'success'"/>
<SvgIcon name="octicon-skip" class="ui text grey mr-3 " v-else-if="jobStep.status === 'skipped'"/>
<SvgIcon name="octicon-clock" class="ui text yellow mr-3 " v-else-if="jobStep.status === 'waiting'"/>
<SvgIcon name="octicon-x-circle-fill" class="red mr-3 " v-else/>
{{ jobStage.summary }} <span class="step-summary-msg">{{ jobStep.summary }}</span>
<span class="step-summary-dur">{{ Math.round(jobStep.duration/1000) }}s</span>
</div> </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 -->
<div class="job-stage-logs" ref="elJobStageLogs" v-show="currentJobStagesStates[i].expanded"> <div class="job-step-logs" ref="elJobStepLogs" v-show="currentJobStepsStates[i].expanded">
<!-- <!--
<div class="job-log-group"> <div class="job-log-group">
<div class="job-log-group-summary"></div> <div class="job-log-group-summary"></div>
@ -70,13 +78,13 @@ const sfc = {
return { return {
// internal state // internal state
loading: false, loading: false,
currentJobStagesStates: [], currentJobStepsStates: [],
// provided by backend // provided by backend
buildInfo: {}, buildInfo: {},
allJobGroups: [], allJobGroups: [],
currentJobInfo: {}, currentJobInfo: {},
currentJobStages: [], currentJobSteps: [],
}; };
}, },
@ -87,12 +95,12 @@ const sfc = {
}, },
methods: { methods: {
stageLogsGetActiveContainer(idx) { stepLogsGetActiveContainer(idx) {
const el = this.$refs.elJobStageLogs[idx]; const el = this.$refs.elJobStepLogs[idx];
return el._stageLogsActiveContainer ?? el; return el._stepLogsActiveContainer ?? el;
}, },
stageLogsGroupBegin(idx) { stepLogsGroupBegin(idx) {
const el = this.$refs.elJobStageLogs[idx]; const el = this.$refs.elJobStepLogs[idx];
const elJobLogGroup = document.createElement('div'); const elJobLogGroup = document.createElement('div');
elJobLogGroup.classList.add('job-log-group'); elJobLogGroup.classList.add('job-log-group');
@ -105,30 +113,42 @@ const sfc = {
elJobLogGroup.appendChild(elJobLogGroupSummary); elJobLogGroup.appendChild(elJobLogGroupSummary);
elJobLogGroup.appendChild(elJobLogList); elJobLogGroup.appendChild(elJobLogList);
el._stageLogsActiveContainer = elJobLogList; el._stepLogsActiveContainer = elJobLogList;
}, },
stageLogsGroupEnd(idx) { stepLogsGroupEnd(idx) {
const el = this.$refs.elJobStageLogs[idx]; const el = this.$refs.elJobStepLogs[idx];
el._stageLogsActiveContainer = null; el._stepLogsActiveContainer = null;
}, },
toggleStageLogs(idx) { toggleStepLogs(idx) {
this.currentJobStagesStates[idx].expanded = !this.currentJobStagesStates[idx].expanded; this.currentJobStepsStates[idx].expanded = !this.currentJobStepsStates[idx].expanded;
}, },
createLogLine(msg, time) { createLogLine(line) {
const el = document.createElement('div'); const el = document.createElement('div');
el.classList.add('job-log-line'); el.classList.add('job-log-line');
el.innerText = msg; el._jobLogTime = line.t;
el._jobLogTime = time;
const elLineNum = document.createElement('line-num');
elLineNum.innerText = line.ln;
el.appendChild(elLineNum);
const elLogTime = document.createElement('log-time');
elLogTime.innerText = new Date(line.t).toUTCString();
el.appendChild(elLogTime);
const elLogMsg = document.createElement('log-msg');
elLogMsg.innerText = line.m;
el.appendChild(elLogMsg);
return el; return el;
}, },
appendLogs(stageIndex, logLines) { appendLogs(stepIndex, logLines) {
for (const line of logLines) { for (const line of logLines) {
// group: ##[group]GroupTItle , ##[endgroup] // TODO: group support: ##[group]GroupTitle , ##[endgroup]
const el = this.stageLogsGetActiveContainer(stageIndex); const el = this.stepLogsGetActiveContainer(stepIndex);
el.append(this.createLogLine(line.m, line.t)); el.append(this.createLogLine(line));
} }
}, },
@ -136,28 +156,35 @@ const sfc = {
const stateData = { const stateData = {
buildInfo: {title: 'The Demo Build'}, buildInfo: {title: 'The Demo Build'},
allJobGroups: [ allJobGroups: [
{summary: 'Job Group Foo', jobs: [{id: 1, name: 'Job A'}, {id: 2, name: 'Job B'}]}, {summary: 'Job Group Foo', jobs: [{id: 1, name: 'Job A', status: 'success'}, {id: 2, name: 'Job B', status: 'error'}]},
{summary: 'Job Group Bar', jobs: [{id: 3, name: 'Job X'}, {id: 4, name: 'Job Y'}]}, {summary: 'Job Group Bar', jobs: [{id: 3, name: 'Job X', status: 'skipped'}, {id: 4, name: 'Job Y', status: 'waiting'}]},
], ],
currentJobInfo: {title: 'the job title', detail: ' succeeded 3 hours ago in 11s'}, currentJobInfo: {title: 'the job title', detail: ' succeeded 3 hours ago in 11s'},
currentJobStages: [ currentJobSteps: [
{summary: 'Job Stage 1'}, {summary: 'Job Step 1', duration: 3000, status: 'success'},
{summary: 'Job Stage 2'}, {summary: 'Job Step 2', duration: 3000, status: 'error'},
{summary: 'Job Step 3', duration: 3000, status: 'skipped'},
{summary: 'Job Step 4', duration: 3000, status: 'waiting'},
], ],
}; };
const logsData = {streamingLogs: []}; const logsData = {streamingLogs: []};
for (const reqCursor of reqData.stageLogCursors) { for (const reqCursor of reqData.stepLogCursors) {
if (!reqCursor.expanded) continue; if (!reqCursor.expanded) continue;
// if (reqCursor.cursor > 100) continue; // if (reqCursor.cursor > 100) continue;
const stageIndex = reqCursor.stageIndex; const stepIndex = reqCursor.stepIndex;
let cursor = reqCursor.cursor; let cursor = reqCursor.cursor;
const lines = []; const lines = [];
for (let i = 0; i < 110; i++) { for (let i = 0; i < 110; i++) {
lines.push({m: `hello world ${Date.now()}, cursor: ${cursor}`, t: Date.now()}); lines.push({
ln: cursor+0, // line number
m: `hello world ${Date.now()}, cursor: ${cursor}`,
t: Date.now(),
d: 3000, // duration
});
cursor++; cursor++;
} }
logsData.streamingLogs.push({stageIndex, cursor, lines}); logsData.streamingLogs.push({stepIndex, cursor, lines});
} }
return {stateData, logsData}; return {stateData, logsData};
}, },
@ -172,8 +199,8 @@ const sfc = {
if (this.loading) return; if (this.loading) return;
this.loading = true; this.loading = true;
const stageLogCursors = this.currentJobStagesStates.map((it, idx) => {return {stageIndex: idx, cursor: it.cursor, expanded: it.expanded}}); const stepLogCursors = this.currentJobStepsStates.map((it, idx) => {return {stepIndex: idx, cursor: it.cursor, expanded: it.expanded}});
const reqData = {stageLogCursors}; const reqData = {stepLogCursors};
// const data = await this.fetchJobData(); // const data = await this.fetchJobData();
const data = this.fetchMockData(reqData); const data = this.fetchMockData(reqData);
@ -183,14 +210,14 @@ const sfc = {
for (const [key, value] of Object.entries(data.stateData)) { for (const [key, value] of Object.entries(data.stateData)) {
this[key] = value; this[key] = value;
} }
for (let i = 0; i < this.currentJobStages.length; i++) { for (let i = 0; i < this.currentJobSteps.length; i++) {
if (!this.currentJobStagesStates[i]) { if (!this.currentJobStepsStates[i]) {
this.$set(this.currentJobStagesStates, i, {cursor: null, expanded: false}); this.$set(this.currentJobStepsStates, i, {cursor: null, expanded: false});
} }
} }
for (const [_, logs] of data.logsData.streamingLogs.entries()) { for (const [_, logs] of data.logsData.streamingLogs.entries()) {
this.currentJobStagesStates[logs.stageIndex].cursor = logs.cursor; this.currentJobStepsStates[logs.stepIndex].cursor = logs.cursor;
this.appendLogs(logs.stageIndex, logs.lines); this.appendLogs(logs.stepIndex, logs.lines);
} }
} finally { } finally {
this.loading = false; this.loading = false;
@ -237,15 +264,18 @@ export function initRepositoryBuildView() {
.job-group-section { .job-group-section {
.job-group-summary { .job-group-summary {
margin: 5px 0;
padding: 10px;
} }
.job-brief-list { .job-brief-list {
a.job-brief-item { a.job-brief-item {
display: block; display: block;
margin: 5px 0; margin: 5px 0;
padding: 5px; padding: 10px;
background: #f8f8f8; background: #f8f8f8;
border-radius: 5px; border-radius: 5px;
text-decoration: none;
} }
} }
} }
@ -277,15 +307,23 @@ export function initRepositoryBuildView() {
} }
} }
.job-stage-container { .job-step-container {
max-height: 100%; max-height: 100%;
overflow: auto; overflow: auto;
.job-stage-summary { .job-step-summary {
cursor: pointer; cursor: pointer;
padding: 5px 0; padding: 5px 10px;
display: flex;
.step-summary-msg {
flex: 1;
}
.step-summary-dur {
margin-left: 16px;
}
} }
.job-stage-summary:hover { .job-step-summary:hover {
background-color: #333; background-color: #333;
} }
} }
@ -293,13 +331,27 @@ export function initRepositoryBuildView() {
<style lang="less"> <style lang="less">
// some elements are not managed by vue, so we need to use global style // some elements are not managed by vue, so we need to use global style
.job-stage-section { .job-step-section {
margin: 10px; margin: 10px;
.job-stage-logs { .job-step-logs {
.job-log-line { .job-log-line {
margin-left: 20px; display: flex;
line-num {
width: 48px;
color: #555;
text-align: right;
}
log-time {
color: #777;
margin-left: 16px;
}
log-msg {
flex: 1;
margin-left: 16px;
}
} }
// TODO: group support
.job-log-group { .job-log-group {
} }

View File

@ -25,6 +25,8 @@ import octiconFile from '../../public/img/svg/octicon-file.svg';
import octiconSidebarExpand from '../../public/img/svg/octicon-sidebar-expand.svg'; import octiconSidebarExpand from '../../public/img/svg/octicon-sidebar-expand.svg';
import octiconSidebarCollapse from '../../public/img/svg/octicon-sidebar-collapse.svg'; import octiconSidebarCollapse from '../../public/img/svg/octicon-sidebar-collapse.svg';
import octiconCheckCircleFill from '../../public/img/svg/octicon-check-circle-fill.svg'; import octiconCheckCircleFill from '../../public/img/svg/octicon-check-circle-fill.svg';
import octiconXCircleFill from '../../public/img/svg/octicon-x-circle-fill.svg';
import octiconSkip from '../../public/img/svg/octicon-skip.svg';
export const svgs = { export const svgs = {
@ -55,6 +57,8 @@ export const svgs = {
'octicon-diff-removed': octiconDiffRemoved, 'octicon-diff-removed': octiconDiffRemoved,
'octicon-diff-renamed': octiconDiffRenamed, 'octicon-diff-renamed': octiconDiffRenamed,
'octicon-check-circle-fill': octiconCheckCircleFill, 'octicon-check-circle-fill': octiconCheckCircleFill,
'octicon-x-circle-fill': octiconXCircleFill,
'octicon-skip': octiconSkip,
}; };