summaryrefslogtreecommitdiffstats
path: root/web_src
diff options
context:
space:
mode:
authorJason Song <i@wolfogre.com>2023-01-31 09:45:19 +0800
committerGitHub <noreply@github.com>2023-01-31 09:45:19 +0800
commit4011821c946e8db032be86266dd9364ccb204118 (patch)
treea8a1cf1b8f088df583f316c8233bc18a89881099 /web_src
parentb5b3e0714e624cea3ce4d5368aa1266f7639d0eb (diff)
downloadgitea-4011821c946e8db032be86266dd9364ccb204118.tar.gz
gitea-4011821c946e8db032be86266dd9364ccb204118.zip
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>
Diffstat (limited to 'web_src')
-rw-r--r--web_src/js/components/RepoActionView.vue398
-rw-r--r--web_src/js/index.js2
-rw-r--r--web_src/js/svg.js12
-rw-r--r--web_src/less/_actions.less43
-rw-r--r--web_src/less/_base.less2
-rw-r--r--web_src/less/_repository.less3
-rw-r--r--web_src/less/_runner.less45
-rw-r--r--web_src/less/index.less2
8 files changed, 505 insertions, 2 deletions
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
new file mode 100644
index 0000000000..673e5b0d8c
--- /dev/null
+++ b/web_src/js/components/RepoActionView.vue
@@ -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>
+
diff --git a/web_src/js/index.js b/web_src/js/index.js
index a866184203..14483f3fa2 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -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();
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 0f08f64e80..2132ad3120 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -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();
diff --git a/web_src/less/_actions.less b/web_src/less/_actions.less
new file mode 100644
index 0000000000..1acad06a65
--- /dev/null
+++ b/web_src/less/_actions.less
@@ -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
+ }
+}
diff --git a/web_src/less/_base.less b/web_src/less/_base.less
index 697bc0ee74..26fc83785b 100644
--- a/web_src/less/_base.less
+++ b/web_src/less/_base.less
@@ -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
}
}
}
diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less
index 7aa42b1f07..5d30d0d81a 100644
--- a/web_src/less/_repository.less
+++ b/web_src/less/_repository.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;
}
diff --git a/web_src/less/_runner.less b/web_src/less/_runner.less
new file mode 100644
index 0000000000..74d917fc87
--- /dev/null
+++ b/web_src/less/_runner.less
@@ -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);
+ }
+}
diff --git a/web_src/less/index.less b/web_src/less/index.less
index 185bf7ca31..29cff15c54 100644
--- a/web_src/less/index.less
+++ b/web_src/less/index.less
@@ -37,5 +37,7 @@
@import "_explore";
@import "_review";
@import "_package";
+@import "_runner";
+@import "_actions";
@import "./helpers.less";