* change initializeScript path * Merge branch 'add-dockerfile-method-and-start-stop-container' of https… * 更新了容器镜像方式的构建、安装和使用方法,但是devcontainer功能还有问题 * fix run postCreateCommand bug * sh文件方式管理启动脚本 * add restart command and fix bug * add dockerfile method to create container and save container .restart …
584 lines
15 KiB
Vue
584 lines
15 KiB
Vue
<script>
|
|
import {SvgIcon} from '../svg.js';
|
|
import ActionRunStatus from './ActionRunStatus.vue';
|
|
import {createApp} from 'vue';
|
|
import {toggleElem} from '../utils/dom.js';
|
|
import {formatDatetime} from '../utils/time.js';
|
|
import {renderAnsi} from '../render/ansi.js';
|
|
import {GET, POST, DELETE} from '../modules/fetch.js';
|
|
|
|
const sfc = {
|
|
name: 'RepoDevcontainerView',
|
|
components: {
|
|
SvgIcon,
|
|
ActionRunStatus,
|
|
},
|
|
props: {
|
|
outputLink: String,
|
|
|
|
},
|
|
data() {
|
|
return {
|
|
// internal state
|
|
loading: false,
|
|
intervalID: null,
|
|
currentJobStepsStates: [],
|
|
currentDevcontainer: {
|
|
title: 'Devcontainer Info',
|
|
detail: 'No Devcontainer Created yet',
|
|
steps: [
|
|
// {
|
|
// summary: '',
|
|
// duration: '',
|
|
// status: '',
|
|
// logs:{
|
|
//
|
|
// },
|
|
// }
|
|
],
|
|
},
|
|
};
|
|
},
|
|
|
|
async mounted() {
|
|
// load job data and then auto-reload periodically
|
|
// need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener
|
|
|
|
await this.loadJob();
|
|
this.intervalID = setInterval(this.loadJob, 3000);
|
|
},
|
|
unmounted() {
|
|
// clear the interval timer when the component is unmounted
|
|
// even our page is rendered once, not spa style
|
|
if (this.intervalID) {
|
|
clearInterval(this.intervalID);
|
|
this.intervalID = null;
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
|
|
// show/hide the step logs for a step
|
|
toggleStepLogs(idx) {
|
|
this.currentJobStepsStates[idx].expanded = !this.currentJobStepsStates[idx].expanded;
|
|
},
|
|
async fetchJob() {
|
|
|
|
const resp = await GET(this.outputLink);
|
|
return await resp.json();
|
|
},
|
|
|
|
async loadJob() {
|
|
if (this.loading) return;
|
|
try {
|
|
this.loading = true;
|
|
|
|
let job;
|
|
try {
|
|
job = await this.fetchJob();
|
|
if(!job){
|
|
clearInterval(this.intervalID);
|
|
this.intervalID = null;
|
|
return;
|
|
}
|
|
|
|
} catch (err) {
|
|
if (err instanceof TypeError) return; // avoid network error while unloading page
|
|
throw err;
|
|
}
|
|
if(this.currentDevcontainer.detail == "running" && job.currentDevcontainer.detail == "created"){
|
|
location.reload();
|
|
}
|
|
|
|
if((this.currentDevcontainer.detail == "created" || this.currentDevcontainer.detail == "running") && job.currentDevcontainer.detail == "success"){
|
|
location.reload();
|
|
}
|
|
// save the state to Vue data, then the UI will be updated
|
|
this.currentDevcontainer = job.currentDevcontainer;
|
|
|
|
// sync the currentJobStepsStates to store the job step states
|
|
if (this.currentDevcontainer.steps != null){
|
|
for (let i = 0; i < this.currentDevcontainer.steps.length; i++) {
|
|
if (!this.currentJobStepsStates[i]) {
|
|
// initial states for job steps
|
|
this.currentJobStepsStates[i] = {expanded: false};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.isDone(this.currentDevcontainer.detail) && this.intervalID) {
|
|
clearInterval(this.intervalID);
|
|
this.intervalID = null;
|
|
}
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
isDone(status) {
|
|
return ['success', 'finished', 'skipped', 'failure', 'cancelled'].includes(status);
|
|
},
|
|
|
|
isExpandable(status) {
|
|
return ['success', 'finished', 'running', 'failure', 'cancelled'].includes(status);
|
|
},
|
|
|
|
},
|
|
};
|
|
|
|
export default sfc;
|
|
|
|
export function initRepositoryDevcontainerView() {
|
|
const el = document.querySelector('#repo-devcontainer-view');
|
|
if (!el) return;
|
|
|
|
// TODO: the parent element's full height doesn't work well now,
|
|
// but we can not pollute the global style at the moment, only fix the height problem for pages with this component
|
|
const parentFullHeight = document.querySelector('body > div.full.height');
|
|
if (parentFullHeight) parentFullHeight.style.paddingBottom = '0';
|
|
const view = createApp(sfc, {
|
|
outputLink: el.getAttribute('data-outputLink')
|
|
})
|
|
view.mount(el);
|
|
}
|
|
</script>
|
|
<template>
|
|
<div class="ui container action-view-container">
|
|
<div class="action-view-body">
|
|
<div class="action-view-right">
|
|
<div class="job-info-header">
|
|
<div class="job-info-header-left gt-ellipsis">
|
|
<h3 class="job-info-header-title gt-ellipsis">
|
|
{{ currentDevcontainer.title }}
|
|
</h3>
|
|
<p class="job-info-header-detail">
|
|
{{ currentDevcontainer.detail }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="job-step-container" ref="steps" v-if="currentDevcontainer.steps && currentDevcontainer.steps.length">
|
|
<div class="job-step-section" v-for="(jobStep, i) in currentDevcontainer.steps" :key="i">
|
|
<div class="job-step-summary" @click.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']">
|
|
<!-- If the job is done and the job step log is loaded for the first time, show the loading icon
|
|
currentJobStepsStates[i].cursor === null means the log is loaded for the first time
|
|
-->
|
|
<SvgIcon v-if="currentJobStepsStates[i].expanded && currentJobStepsStates[i].cursor === null" name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
|
|
<SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" :class="['tw-mr-2', !isExpandable(jobStep.status) && 'tw-invisible']"/>
|
|
<ActionRunStatus :status="jobStep.status" class="tw-mr-2"/>
|
|
|
|
<span class="step-summary-msg gt-ellipsis">{{ jobStep.summary }}</span>
|
|
<span class="step-summary-duration">{{ jobStep.duration }}</span>
|
|
</div>
|
|
|
|
<!-- 使用 v-for 渲染 job-log-line -->
|
|
<div class="job-step-logs" ref="logs" v-show="currentJobStepsStates[i].expanded">
|
|
<div class="job-log-line" v-for="(line, index) in jobStep.logs" :key="index" :id="`jobstep-${i}-${line.index}`">
|
|
<a class="line-num muted" :href="`#jobstep-${i}-${line.index}`">{{ line.index }}</a>
|
|
<span class="log-msg">{{ line.message }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<style scoped>
|
|
.action-view-body {
|
|
padding-top: 12px;
|
|
padding-bottom: 12px;
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* ================ */
|
|
/* action view header */
|
|
|
|
.action-view-header {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.action-info-summary {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
}
|
|
|
|
.action-info-summary-title {
|
|
display: flex;
|
|
}
|
|
|
|
.action-info-summary-title-text {
|
|
font-size: 20px;
|
|
margin: 0 0 0 8px;
|
|
flex: 1;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.action-commit-summary {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 5px;
|
|
margin-left: 28px;
|
|
}
|
|
|
|
@media (max-width: 767.98px) {
|
|
.action-commit-summary {
|
|
margin-left: 0;
|
|
margin-top: 8px;
|
|
}
|
|
}
|
|
|
|
/* ================ */
|
|
/* action view left */
|
|
|
|
.action-view-left {
|
|
width: 30%;
|
|
max-width: 400px;
|
|
position: sticky;
|
|
top: 12px;
|
|
max-height: 100vh;
|
|
overflow-y: auto;
|
|
background: var(--color-body);
|
|
z-index: 2; /* above .job-info-header */
|
|
}
|
|
|
|
@media (max-width: 767.98px) {
|
|
.action-view-left {
|
|
position: static; /* can not sticky because multiple jobs would overlap into right view */
|
|
}
|
|
}
|
|
|
|
.job-artifacts-title {
|
|
font-size: 18px;
|
|
margin-top: 16px;
|
|
padding: 16px 10px 0 20px;
|
|
border-top: 1px solid var(--color-secondary);
|
|
}
|
|
|
|
.job-artifacts-item {
|
|
margin: 5px 0;
|
|
padding: 6px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.job-artifacts-list {
|
|
padding-left: 12px;
|
|
list-style: none;
|
|
}
|
|
|
|
.job-artifacts-icon {
|
|
padding-right: 3px;
|
|
}
|
|
|
|
.job-brief-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.job-brief-item {
|
|
padding: 10px;
|
|
border-radius: var(--border-radius);
|
|
text-decoration: none;
|
|
display: flex;
|
|
flex-wrap: nowrap;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.job-brief-item:hover {
|
|
background-color: var(--color-hover);
|
|
}
|
|
|
|
.job-brief-item.selected {
|
|
font-weight: var(--font-weight-bold);
|
|
background-color: var(--color-active);
|
|
}
|
|
|
|
.job-brief-item:first-of-type {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.job-brief-item .job-brief-rerun {
|
|
cursor: pointer;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.job-brief-item .job-brief-rerun:hover {
|
|
transform: scale(130%);
|
|
}
|
|
|
|
.job-brief-item .job-brief-item-left {
|
|
display: flex;
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
.job-brief-item .job-brief-item-left span {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.job-brief-item .job-brief-item-left .job-brief-name {
|
|
display: block;
|
|
width: 70%;
|
|
}
|
|
|
|
.job-brief-item .job-brief-item-right {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
/* ================ */
|
|
/* action view right */
|
|
|
|
.action-view-right {
|
|
flex: 1;
|
|
color: var(--color-console-fg-subtle);
|
|
max-height: 100%;
|
|
width: 70%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: 1px solid var(--color-console-border);
|
|
border-radius: var(--border-radius);
|
|
background: var(--color-console-bg);
|
|
align-self: flex-start;
|
|
}
|
|
|
|
/* 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 */
|
|
|
|
.job-info-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0 12px;
|
|
position: sticky;
|
|
top: 0;
|
|
height: 60px;
|
|
z-index: 1; /* above .job-step-container */
|
|
background: var(--color-console-bg);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.job-info-header:has(+ .job-step-container) {
|
|
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
|
}
|
|
|
|
.job-info-header .job-info-header-title {
|
|
color: var(--color-console-fg);
|
|
font-size: 16px;
|
|
margin: 0;
|
|
}
|
|
|
|
.job-info-header .job-info-header-detail {
|
|
color: var(--color-console-fg-subtle);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.job-info-header-left {
|
|
flex: 1;
|
|
}
|
|
|
|
.job-step-container {
|
|
max-height: 100%;
|
|
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
|
border-top: 1px solid var(--color-console-border);
|
|
z-index: 0;
|
|
}
|
|
|
|
.job-step-container .job-step-summary {
|
|
padding: 5px 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
border-radius: var(--border-radius);
|
|
}
|
|
|
|
.job-step-container .job-step-summary.step-expandable {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.job-step-container .job-step-summary.step-expandable:hover {
|
|
color: var(--color-console-fg);
|
|
background: var(--color-console-hover-bg);
|
|
}
|
|
|
|
.job-step-container .job-step-summary .step-summary-msg {
|
|
flex: 1;
|
|
}
|
|
|
|
.job-step-container .job-step-summary .step-summary-duration {
|
|
margin-left: 16px;
|
|
}
|
|
|
|
.job-step-container .job-step-summary.selected {
|
|
color: var(--color-console-fg);
|
|
background-color: var(--color-console-active-bg);
|
|
position: sticky;
|
|
top: 60px;
|
|
}
|
|
|
|
@media (max-width: 767.98px) {
|
|
.action-view-body {
|
|
flex-direction: column;
|
|
}
|
|
.action-view-left, .action-view-right {
|
|
width: 100%;
|
|
}
|
|
.action-view-left {
|
|
max-width: none;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style>
|
|
/* some elements are not managed by vue, so we need to use global style */
|
|
.job-status-rotate {
|
|
animation: job-status-rotate-keyframes 1s linear infinite;
|
|
}
|
|
|
|
@keyframes job-status-rotate-keyframes {
|
|
100% {
|
|
transform: rotate(-360deg);
|
|
}
|
|
}
|
|
|
|
.job-step-section {
|
|
margin: 10px;
|
|
}
|
|
|
|
.job-step-section .job-step-logs {
|
|
font-family: var(--fonts-monospace);
|
|
margin: 8px 0;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.job-step-section .job-step-logs .job-log-line {
|
|
display: flex;
|
|
}
|
|
|
|
.job-log-line:hover,
|
|
.job-log-line:target {
|
|
background-color: var(--color-console-hover-bg);
|
|
}
|
|
|
|
.job-log-line:target {
|
|
scroll-margin-top: 95px;
|
|
}
|
|
|
|
/* 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-text-light-3);
|
|
text-align: right;
|
|
user-select: none;
|
|
}
|
|
|
|
.job-log-line:target > .line-num {
|
|
color: var(--color-primary);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.log-time-seconds {
|
|
padding-right: 2px;
|
|
}
|
|
|
|
.job-log-line .log-time,
|
|
.log-time-stamp {
|
|
color: var(--color-text-light-3);
|
|
margin-left: 10px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.job-step-section .job-step-logs .job-log-line .log-msg {
|
|
flex: 1;
|
|
word-break: break-all;
|
|
white-space: break-spaces;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* TODO: group support
|
|
|
|
.job-log-group {
|
|
|
|
}
|
|
.job-log-group-summary {
|
|
|
|
}
|
|
.job-log-list {
|
|
|
|
} */
|
|
</style>
|