Implement actions (#21937)
Close #13539. Co-authored by: @lunny @appleboy @fuxiaohei and others. Related projects: - https://gitea.com/gitea/actions-proto-def - https://gitea.com/gitea/actions-proto-go - https://gitea.com/gitea/act - https://gitea.com/gitea/act_runner ### Summary The target of this PR is to bring a basic implementation of "Actions", an internal CI/CD system of Gitea. That means even though it has been merged, the state of the feature is **EXPERIMENTAL**, and please note that: - It is disabled by default; - It shouldn't be used in a production environment currently; - It shouldn't be used in a public Gitea instance currently; - Breaking changes may be made before it's stable. **Please comment on #13539 if you have any different product design ideas**, all decisions reached there will be adopted here. But in this PR, we don't talk about **naming, feature-creep or alternatives**. ### ⚠️ Breaking `gitea-actions` will become a reserved user name. If a user with the name already exists in the database, it is recommended to rename it. ### Some important reviews - What is `DEFAULT_ACTIONS_URL` in `app.ini` for? - https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954 - Why the api for runners is not under the normal `/api/v1` prefix? - https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592 - Why DBFS? - https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178 - Why ignore events triggered by `gitea-actions` bot? - https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103 - Why there's no permission control for actions? - https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868 ### What it looks like <details> #### Manage runners <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png"> #### List runs <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png"> #### View logs <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png"> </details> ### How to try it <details> #### 1. Start Gitea Clone this branch and [install from source](https://docs.gitea.io/en-us/install-from-source). Add additional configurations in `app.ini` to enable Actions: ```ini [actions] ENABLED = true ``` Start it. If all is well, you'll see the management page of runners: <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png"> #### 2. Start runner Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow the [README](https://gitea.com/gitea/act_runner/src/branch/main/README.md) to start it. If all is well, you'll see a new runner has been added: <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png"> #### 3. Enable actions for a repo Create a new repo or open an existing one, check the `Actions` checkbox in settings and submit. <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png"> <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png"> If all is well, you'll see a new tab "Actions": <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png"> #### 4. Upload workflow files Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can follow the [quickstart](https://docs.github.com/en/actions/quickstart) of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions in most cases, you can use the same demo: ```yaml name: GitHub Actions Demo run-name: ${{ github.actor }} is testing out GitHub Actions 🚀 on: [push] jobs: Explore-GitHub-Actions: runs-on: ubuntu-latest steps: - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." - name: Check out repository code uses: actions/checkout@v3 - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner." - name: List files in the repository run: | ls ${{ github.workspace }} - run: echo "🍏 This job's status is ${{ job.status }}." ``` If all is well, you'll see a new run in `Actions` tab: <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png"> #### 5. Check the logs of jobs Click a run and you'll see the logs: <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png"> #### 6. Go on You can try more examples in [the documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) of GitHub Actions, then you might find a lot of bugs. Come on, PRs are welcome. </details> See also: [Feature Preview: Gitea Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/) --------- Co-authored-by: a1012112796 <1012112796@qq.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: ChristopherHX <christopher.homberger@web.de> Co-authored-by: John Olheiser <john.olheiser@gmail.com>
This commit is contained in:
parent
b5b3e0714e
commit
4011821c94
117 changed files with 7545 additions and 128 deletions
398
web_src/js/components/RepoActionView.vue
Normal file
398
web_src/js/components/RepoActionView.vue
Normal file
|
@ -0,0 +1,398 @@
|
|||
<template>
|
||||
<div class="action-view-container">
|
||||
<div class="action-view-header">
|
||||
<div class="action-info-summary">
|
||||
{{ run.title }}
|
||||
<button class="run_cancel" @click="cancelRun()" v-if="run.canCancel">
|
||||
<i class="stop circle outline icon"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-view-body">
|
||||
<div class="action-view-left">
|
||||
<div class="job-group-section">
|
||||
<div class="job-brief-list">
|
||||
<a class="job-brief-item" v-for="(job, index) in run.jobs" :key="job.id" :href="run.htmlurl+'/jobs/'+index">
|
||||
<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-blocked" class="ui text yellow" v-else-if="job.status === 'blocked'"/>
|
||||
<SvgIcon name="octicon-meter" class="ui text yellow" class-name="job-status-rotate" v-else-if="job.status === 'running'"/>
|
||||
<SvgIcon name="octicon-x-circle-fill" class="red" v-else/>
|
||||
{{ job.name }}
|
||||
<button class="job-brief-rerun" @click="rerunJob(index)" v-if="job.canRerun">
|
||||
<SvgIcon name="octicon-sync" class="ui text black"/>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-view-right">
|
||||
<div class="job-info-header">
|
||||
<div class="job-info-header-title">
|
||||
{{ currentJob.title }}
|
||||
</div>
|
||||
<div class="job-info-header-detail">
|
||||
{{ currentJob.detail }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-step-container">
|
||||
<div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i">
|
||||
<div class="job-step-summary" @click.stop="toggleStepLogs(i)">
|
||||
<SvgIcon name="octicon-chevron-down" class="mr-3" v-show="currentJobStepsStates[i].expanded"/>
|
||||
<SvgIcon name="octicon-chevron-right" class="mr-3" v-show="!currentJobStepsStates[i].expanded"/>
|
||||
|
||||
<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-blocked" class="ui text yellow mr-3" v-else-if="jobStep.status === 'blocked'"/>
|
||||
<SvgIcon name="octicon-meter" class="ui text yellow mr-3" class-name="job-status-rotate" v-else-if="jobStep.status === 'running'"/>
|
||||
<SvgIcon name="octicon-x-circle-fill" class="red mr-3 " v-else/>
|
||||
|
||||
<span class="step-summary-msg">{{ jobStep.summary }}</span>
|
||||
<span class="step-summary-dur">{{ jobStep.duration }}</span>
|
||||
</div>
|
||||
|
||||
<!-- the log elements could be a lot, do not use v-if to destroy/reconstruct the DOM -->
|
||||
<div class="job-step-logs" ref="logs" v-show="currentJobStepsStates[i].expanded"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {SvgIcon} from '../svg.js';
|
||||
import {createApp} from 'vue';
|
||||
import AnsiToHTML from 'ansi-to-html';
|
||||
|
||||
const {csrfToken} = window.config;
|
||||
|
||||
const sfc = {
|
||||
name: 'RepoActionView',
|
||||
components: {
|
||||
SvgIcon,
|
||||
},
|
||||
props: {
|
||||
runIndex: String,
|
||||
jobIndex: String,
|
||||
actionsURL: String,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
ansiToHTML: new AnsiToHTML({escapeXML: true}),
|
||||
|
||||
// internal state
|
||||
loading: false,
|
||||
intervalID: null,
|
||||
currentJobStepsStates: [],
|
||||
|
||||
// provided by backend
|
||||
run: {
|
||||
htmlurl: '',
|
||||
title: '',
|
||||
canCancel: false,
|
||||
done: false,
|
||||
jobs: [
|
||||
// {
|
||||
// id: 0,
|
||||
// name: '',
|
||||
// status: '',
|
||||
// canRerun: false,
|
||||
// },
|
||||
],
|
||||
},
|
||||
currentJob: {
|
||||
title: '',
|
||||
detail: '',
|
||||
steps: [
|
||||
// {
|
||||
// summary: '',
|
||||
// duration: '',
|
||||
// status: '',
|
||||
// }
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// load job data and then auto-reload periodically
|
||||
this.loadJob();
|
||||
this.intervalID = setInterval(this.loadJob, 1000);
|
||||
},
|
||||
|
||||
methods: {
|
||||
// get the active container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
|
||||
getLogsContainer(idx) {
|
||||
const el = this.$refs.logs[idx];
|
||||
return el._stepLogsActiveContainer ?? el;
|
||||
},
|
||||
// begin a log group
|
||||
beginLogGroup(idx) {
|
||||
const el = this.$refs.logs[idx];
|
||||
|
||||
const elJobLogGroup = document.createElement('div');
|
||||
elJobLogGroup.classList.add('job-log-group');
|
||||
|
||||
const elJobLogGroupSummary = document.createElement('div');
|
||||
elJobLogGroupSummary.classList.add('job-log-group-summary');
|
||||
|
||||
const elJobLogList = document.createElement('div');
|
||||
elJobLogList.classList.add('job-log-list');
|
||||
|
||||
elJobLogGroup.appendChild(elJobLogGroupSummary);
|
||||
elJobLogGroup.appendChild(elJobLogList);
|
||||
el._stepLogsActiveContainer = elJobLogList;
|
||||
},
|
||||
// end a log group
|
||||
endLogGroup(idx) {
|
||||
const el = this.$refs.logs[idx];
|
||||
el._stepLogsActiveContainer = null;
|
||||
},
|
||||
|
||||
// show/hide the step logs for a step
|
||||
toggleStepLogs(idx) {
|
||||
this.currentJobStepsStates[idx].expanded = !this.currentJobStepsStates[idx].expanded;
|
||||
if (this.currentJobStepsStates[idx].expanded) {
|
||||
this.loadJob(); // try to load the data immediately instead of waiting for next timer interval
|
||||
}
|
||||
},
|
||||
// rerun a job
|
||||
rerunJob(idx) {
|
||||
this.fetch(`${this.run.htmlurl}/jobs/${idx}/rerun`);
|
||||
},
|
||||
// cancel a run
|
||||
cancelRun() {
|
||||
this.fetch(`${this.run.htmlurl}/cancel`);
|
||||
},
|
||||
|
||||
createLogLine(line) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('job-log-line');
|
||||
div._jobLogTime = line.timestamp;
|
||||
|
||||
const lineNumber = document.createElement('div');
|
||||
lineNumber.className = 'line-num';
|
||||
lineNumber.innerText = line.index;
|
||||
div.appendChild(lineNumber);
|
||||
|
||||
// TODO: Support displaying time optionally
|
||||
|
||||
const logMessage = document.createElement('div');
|
||||
logMessage.className = 'log-msg';
|
||||
logMessage.innerHTML = this.ansiToHTML.toHtml(line.message);
|
||||
div.appendChild(logMessage);
|
||||
|
||||
return div;
|
||||
},
|
||||
|
||||
appendLogs(stepIndex, logLines) {
|
||||
for (const line of logLines) {
|
||||
// TODO: group support: ##[group]GroupTitle , ##[endgroup]
|
||||
const el = this.getLogsContainer(stepIndex);
|
||||
el.append(this.createLogLine(line));
|
||||
}
|
||||
},
|
||||
|
||||
async fetchJob() {
|
||||
const logCursors = this.currentJobStepsStates.map((it, idx) => {
|
||||
// cursor is used to indicate the last position of the logs
|
||||
// it's only used by backend, frontend just reads it and passes it back, it and can be any type.
|
||||
// for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc
|
||||
return {step: idx, cursor: it.cursor, expanded: it.expanded};
|
||||
});
|
||||
const resp = await this.fetch(
|
||||
`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`,
|
||||
JSON.stringify({logCursors}),
|
||||
);
|
||||
return await resp.json();
|
||||
},
|
||||
|
||||
async loadJob() {
|
||||
if (this.loading) return;
|
||||
try {
|
||||
this.loading = true;
|
||||
|
||||
const response = await this.fetchJob();
|
||||
|
||||
// save the state to Vue data, then the UI will be updated
|
||||
this.run = response.state.run;
|
||||
this.currentJob = response.state.currentJob;
|
||||
|
||||
// sync the currentJobStepsStates to store the job step states
|
||||
for (let i = 0; i < this.currentJob.steps.length; i++) {
|
||||
if (!this.currentJobStepsStates[i]) {
|
||||
this.currentJobStepsStates[i] = {cursor: null, expanded: false};
|
||||
}
|
||||
}
|
||||
// append logs to the UI
|
||||
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);
|
||||
}
|
||||
|
||||
if (this.run.done && this.intervalID) {
|
||||
clearInterval(this.intervalID);
|
||||
this.intervalID = null;
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
fetch(url, body) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Csrf-Token': csrfToken,
|
||||
},
|
||||
body,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default sfc;
|
||||
|
||||
export function initRepositoryActionView() {
|
||||
const el = document.getElementById('repo-action-view');
|
||||
if (!el) return;
|
||||
|
||||
const view = createApp(sfc, {
|
||||
runIndex: el.getAttribute('data-run-index'),
|
||||
jobIndex: el.getAttribute('data-job-index'),
|
||||
actionsURL: el.getAttribute('data-actions-url'),
|
||||
});
|
||||
view.mount(el);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
// some elements are not managed by vue, so we need to use _actions.less in addition.
|
||||
|
||||
.action-view-body {
|
||||
display: flex;
|
||||
height: calc(100vh - 266px); // fine tune this value to make the main view has full height
|
||||
}
|
||||
|
||||
// ================
|
||||
// action view header
|
||||
|
||||
.action-view-header {
|
||||
margin: 0 20px 20px 20px;
|
||||
button.run_cancel {
|
||||
border: none;
|
||||
color: var(--color-red);
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition:transform 0.2s;
|
||||
};
|
||||
button.run_cancel:hover{
|
||||
transform:scale(130%);
|
||||
};
|
||||
}
|
||||
|
||||
.action-info-summary {
|
||||
font-size: 150%;
|
||||
height: 20px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
// ================
|
||||
// action view left
|
||||
|
||||
.action-view-left {
|
||||
width: 30%;
|
||||
max-width: 400px;
|
||||
overflow-y: scroll;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.job-group-section {
|
||||
.job-group-summary {
|
||||
margin: 5px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.job-brief-list {
|
||||
a.job-brief-item {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
padding: 10px;
|
||||
background: var(--color-info-bg);
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
button.job-brief-rerun {
|
||||
float: right;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition:transform 0.2s;
|
||||
};
|
||||
button.job-brief-rerun:hover{
|
||||
transform:scale(130%);
|
||||
};
|
||||
}
|
||||
a.job-brief-item:hover {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================
|
||||
// action view right
|
||||
|
||||
.action-view-right {
|
||||
flex: 1;
|
||||
background-color: var(--color-console-bg);
|
||||
color: var(--color-console-fg);
|
||||
max-height: 100%;
|
||||
margin-right: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.job-info-header {
|
||||
.job-info-header-title {
|
||||
font-size: 150%;
|
||||
padding: 10px;
|
||||
}
|
||||
.job-info-header-detail {
|
||||
padding: 0 10px 10px;
|
||||
border-bottom: 1px solid var(--color-grey);
|
||||
}
|
||||
}
|
||||
|
||||
.job-step-container {
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.job-step-summary {
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
|
||||
.step-summary-msg {
|
||||
flex: 1;
|
||||
}
|
||||
.step-summary-dur {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
.job-step-summary:hover {
|
||||
background-color: var(--color-black-light);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -90,6 +90,7 @@ import {initRepoCommentForm, initRepository} from './features/repo-legacy.js';
|
|||
import {initFormattingReplacements} from './features/formatting.js';
|
||||
import {initMcaptcha} from './features/mcaptcha.js';
|
||||
import {initCopyContent} from './features/copycontent.js';
|
||||
import {initRepositoryActionView} from './components/RepoActionView.vue';
|
||||
|
||||
// Run time-critical code as soon as possible. This is safe to do because this
|
||||
// script appears at the end of <body> and rendered HTML is accessible at that point.
|
||||
|
@ -187,6 +188,7 @@ $(document).ready(() => {
|
|||
initRepoTopicBar();
|
||||
initRepoWikiForm();
|
||||
initRepository();
|
||||
initRepositoryActionView();
|
||||
|
||||
initCommitStatuses();
|
||||
initMcaptcha();
|
||||
|
|
|
@ -25,8 +25,16 @@ import octiconSidebarCollapse from '../../public/img/svg/octicon-sidebar-collaps
|
|||
import octiconSidebarExpand from '../../public/img/svg/octicon-sidebar-expand.svg';
|
||||
import octiconTriangleDown from '../../public/img/svg/octicon-triangle-down.svg';
|
||||
import octiconX from '../../public/img/svg/octicon-x.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';
|
||||
import octiconMeter from '../../public/img/svg/octicon-meter.svg';
|
||||
import octiconBlocked from '../../public/img/svg/octicon-blocked.svg';
|
||||
import octiconSync from '../../public/img/svg/octicon-sync.svg';
|
||||
|
||||
export const svgs = {
|
||||
'octicon-blocked': octiconBlocked,
|
||||
'octicon-check-circle-fill': octiconCheckCircleFill,
|
||||
'octicon-chevron-down': octiconChevronDown,
|
||||
'octicon-chevron-right': octiconChevronRight,
|
||||
'octicon-clock': octiconClock,
|
||||
|
@ -44,6 +52,7 @@ export const svgs = {
|
|||
'octicon-kebab-horizontal': octiconKebabHorizontal,
|
||||
'octicon-link': octiconLink,
|
||||
'octicon-lock': octiconLock,
|
||||
'octicon-meter': octiconMeter,
|
||||
'octicon-milestone': octiconMilestone,
|
||||
'octicon-mirror': octiconMirror,
|
||||
'octicon-project': octiconProject,
|
||||
|
@ -52,8 +61,11 @@ export const svgs = {
|
|||
'octicon-repo-template': octiconRepoTemplate,
|
||||
'octicon-sidebar-collapse': octiconSidebarCollapse,
|
||||
'octicon-sidebar-expand': octiconSidebarExpand,
|
||||
'octicon-skip': octiconSkip,
|
||||
'octicon-sync': octiconSync,
|
||||
'octicon-triangle-down': octiconTriangleDown,
|
||||
'octicon-x': octiconX,
|
||||
'octicon-x-circle-fill': octiconXCircleFill,
|
||||
};
|
||||
|
||||
const parser = new DOMParser();
|
||||
|
|
43
web_src/less/_actions.less
Normal file
43
web_src/less/_actions.less
Normal file
|
@ -0,0 +1,43 @@
|
|||
@import "variables.less";
|
||||
|
||||
// TODO: the parent element's full height doesn't work well now
|
||||
body > div.full.height {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.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-logs {
|
||||
font-family: monospace;
|
||||
.job-log-line {
|
||||
display: flex;
|
||||
.line-num {
|
||||
width: 48px;
|
||||
color: var(--color-grey-light);
|
||||
text-align: right;
|
||||
}
|
||||
.log-time {
|
||||
color: var(--color-grey-light);
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.log-msg {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
white-space: break-spaces;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: group support
|
||||
}
|
||||
}
|
|
@ -1797,7 +1797,7 @@ footer {
|
|||
.ui {
|
||||
&.left,
|
||||
&.right {
|
||||
line-height: 40px;
|
||||
line-height: 39px; // there is a border-top on the footer, so make the line-height 1px less
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2619,7 +2619,8 @@
|
|||
}
|
||||
|
||||
&.webhooks .list > .item:not(:first-child),
|
||||
&.githooks .list > .item:not(:first-child) {
|
||||
&.githooks .list > .item:not(:first-child),
|
||||
&.runners .list > .item:not(:first-child) {
|
||||
padding: .25rem 1rem;
|
||||
margin: 12px -1rem -1rem;
|
||||
}
|
||||
|
|
45
web_src/less/_runner.less
Normal file
45
web_src/less/_runner.less
Normal file
|
@ -0,0 +1,45 @@
|
|||
@import "variables.less";
|
||||
|
||||
.runner-container {
|
||||
padding-bottom: 30px;
|
||||
.runner-ops > a {
|
||||
margin-left: .5em;
|
||||
}
|
||||
.runner-ops-delete {
|
||||
color: var(--color-red-light);
|
||||
}
|
||||
.runner-basic-info .dib {
|
||||
margin-right: 1em;
|
||||
}
|
||||
.runner-status-online {
|
||||
.ui.label;
|
||||
background-color: var(--color-green);
|
||||
color: var(--color-white);
|
||||
}
|
||||
.runner-new-text {
|
||||
color: var(--color-white);
|
||||
}
|
||||
#runner-new:hover .runner-new-text {
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
.runner-new-menu {
|
||||
width: 300px;
|
||||
}
|
||||
.task-status-success {
|
||||
background-color: var(--color-green);
|
||||
color: var(--color-white);
|
||||
}
|
||||
.task-status-failure {
|
||||
background-color: var(--color-red-light);
|
||||
color: var(--color-white);
|
||||
}
|
||||
.task-status-running {
|
||||
background-color: var(--color-blue);
|
||||
color: var(--color-white);
|
||||
}
|
||||
.task-status-cancelled,
|
||||
.task-status-blocked {
|
||||
background-color: var(--color-yellow);
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
|
@ -37,5 +37,7 @@
|
|||
@import "_explore";
|
||||
@import "_review";
|
||||
@import "_package";
|
||||
@import "_runner";
|
||||
@import "_actions";
|
||||
|
||||
@import "./helpers.less";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue