|
| 1 | +<template> |
| 2 | +<div class="droneci-builds-wrapper" v-if="builds"> |
| 3 | + <div |
| 4 | + class="build-row" |
| 5 | + v-for="build in builds" :key="build.id" |
| 6 | + v-tooltip="infoTooltip(build)" |
| 7 | + > |
| 8 | + <div class="status"> |
| 9 | + <p :class="build.build.status">{{ build.build.status | formatStatus }}</p> |
| 10 | + <span v-if="build.build.status == 'running'"> |
| 11 | + {{ build.build.started*1000 | formatTimeAgo }} ago |
| 12 | + </span> |
| 13 | + <span v-else-if="build.build.status != 'pending' "> |
| 14 | + {{ formatBuildDuration(build) }} |
| 15 | + </span> |
| 16 | + <span v-else> |
| 17 | + {{ build.build.created*1000 | formatTimeAgo }} ago |
| 18 | + </span> |
| 19 | + </div> |
| 20 | + <div class="info"> |
| 21 | + <div class="build-name"> |
| 22 | + {{ build.name }} |
| 23 | + <a |
| 24 | + class="droneci-build-number" |
| 25 | + :href="build.baseurl + '/' + build.slug + '/' +build.build.number" |
| 26 | + target="_blank" |
| 27 | + >{{ build.build.number }}</a> |
| 28 | + </div> |
| 29 | + <div class="build-desc"> |
| 30 | + <span class="droneci-extra"> |
| 31 | + <template v-if="build.build.event == 'pull_request'"> |
| 32 | + <a |
| 33 | + :href="build.build.link" |
| 34 | + target="_blank" |
| 35 | + class="droneci-extra-info" |
| 36 | + >#{{ formatPrId(build.build.link) }}</a> to |
| 37 | + </template> |
| 38 | + <template v-else-if="build.build.event == 'push'"> |
| 39 | + <a |
| 40 | + :href="build.build.link" |
| 41 | + target="_blank" |
| 42 | + class="droneci-extra-info" |
| 43 | + >push</a> to |
| 44 | + </template> |
| 45 | + <a |
| 46 | + :href="build.git_http_url" |
| 47 | + target="_blank" |
| 48 | + class="droneci-extra-info" |
| 49 | + > |
| 50 | + {{ build.build.target }} |
| 51 | + </a> |
| 52 | + </span> |
| 53 | + </div> |
| 54 | + </div> |
| 55 | + </div> |
| 56 | +</div> |
| 57 | +</template> |
| 58 | + |
| 59 | +<script> |
| 60 | +import WidgetMixin from '@/mixins/WidgetMixin'; |
| 61 | +import { getTimeAgo, getTimeDifference, timestampToDateTime } from '@/utils/MiscHelpers'; |
| 62 | +
|
| 63 | +export default { |
| 64 | + mixins: [WidgetMixin], |
| 65 | + components: {}, |
| 66 | + data() { |
| 67 | + return { |
| 68 | + builds: null, |
| 69 | + }; |
| 70 | + }, |
| 71 | + filters: { |
| 72 | + formatStatus(status) { |
| 73 | + let symbol = ''; |
| 74 | + if (status === 'success') symbol = '✔'; |
| 75 | + if (status === 'failure' || status === 'error' || status === 'killed') symbol = '✘'; |
| 76 | + if (status === 'running') symbol = '❖'; |
| 77 | + if (status === 'skipped') symbol = '↠'; |
| 78 | + return `${symbol}`; |
| 79 | + }, |
| 80 | + formatDate(timestamp) { |
| 81 | + return timestampToDateTime(timestamp); |
| 82 | + }, |
| 83 | + formatTimeAgo(timestamp) { |
| 84 | + return getTimeAgo(timestamp); |
| 85 | + }, |
| 86 | + }, |
| 87 | + computed: { |
| 88 | + /* API endpoint, either for self-hosted or managed instance */ |
| 89 | + endpointBuilds() { |
| 90 | + if (!this.options.host) this.error('drone.ci Host is required'); |
| 91 | + return `${this.options.host}/api/user/builds`; |
| 92 | + }, |
| 93 | + endpointRepoInfo() { |
| 94 | + if (!this.options.host) this.error('drone.ci Host is required'); |
| 95 | + return `${this.options.host}/api/repos/${this.options.repo}`; |
| 96 | + }, |
| 97 | + endpointRepoBuilds() { |
| 98 | + if (!this.options.host) this.error('drone.ci Host is required'); |
| 99 | + return `${this.options.host}/api/repos/${this.options.repo}/builds`; |
| 100 | + }, |
| 101 | + repo() { |
| 102 | + if (this.options.repo) return this.options.repo; |
| 103 | + return false; |
| 104 | + }, |
| 105 | + apiKey() { |
| 106 | + if (!this.options.apiKey) { |
| 107 | + this.error('An API key is required, please see the docs for more info'); |
| 108 | + } |
| 109 | + return this.options.apiKey; |
| 110 | + }, |
| 111 | + }, |
| 112 | + methods: { |
| 113 | + /* Fetch new data, configured by updateInterval */ |
| 114 | + update() { |
| 115 | + this.startLoading(); |
| 116 | + this.fetchData(); |
| 117 | + this.finishLoading(); |
| 118 | + }, |
| 119 | + /* Make GET request to Drone CI API endpoint */ |
| 120 | + fetchData() { |
| 121 | + const authHeaders = { Authorization: `Bearer ${this.apiKey}` }; |
| 122 | + if (this.repo !== false) { |
| 123 | + this.makeRequest(this.endpointRepoInfo, authHeaders).then( |
| 124 | + (repoInfo) => { |
| 125 | + this.makeRequest(this.endpointRepoBuilds, authHeaders).then( |
| 126 | + (buildInfo) => { |
| 127 | + this.processRepoBuilds(repoInfo, buildInfo); |
| 128 | + }, |
| 129 | + ); |
| 130 | + }, |
| 131 | + ); |
| 132 | + } else { |
| 133 | + this.makeRequest(this.endpointBuilds, authHeaders).then( |
| 134 | + (response) => { this.processBuilds(response); }, |
| 135 | + ); |
| 136 | + } |
| 137 | + }, |
| 138 | + /* Assign data variables to the returned data */ |
| 139 | + processBuilds(data) { |
| 140 | + const results = data.slice(0, this.options.limit) |
| 141 | + .map((obj) => ({ ...obj, baseurl: this.options.host })); |
| 142 | + this.builds = results; |
| 143 | + }, |
| 144 | + processRepoBuilds(repo, builds) { |
| 145 | + const results = builds.slice(0, this.options.limit) |
| 146 | + .map((obj) => ({ build: { ...obj }, baseurl: this.options.host, ...repo })); |
| 147 | + this.builds = results; |
| 148 | + }, |
| 149 | + infoTooltip(build) { |
| 150 | + const content = `<b>Trigger:</b> ${build.build.event} by ${build.build.trigger}<br>` |
| 151 | + + `<b>Repo:</b> ${build.slug}<br>` |
| 152 | + + `<b>Branch:</b> ${build.build.target}<br>`; |
| 153 | + return { |
| 154 | + content, html: true, trigger: 'hover focus', delay: 250, classes: 'build-info-tt', |
| 155 | + }; |
| 156 | + }, |
| 157 | + formatPrId(link) { |
| 158 | + return link.split('/').pop(); |
| 159 | + }, |
| 160 | + formatBuildDuration(build) { |
| 161 | + return getTimeDifference(build.build.started * 1000, build.build.finished * 1000); |
| 162 | + }, |
| 163 | + }, |
| 164 | +}; |
| 165 | +</script> |
| 166 | + |
| 167 | +<style scoped lang="scss"> |
| 168 | +.droneci-builds-wrapper { |
| 169 | + color: var(--widget-text-color); |
| 170 | + .build-row { |
| 171 | + display: grid; |
| 172 | + grid-template-columns: 1fr 2.5fr; |
| 173 | + justify-content: left; |
| 174 | + align-items: center; |
| 175 | + padding: 0.25rem 0; |
| 176 | + .status { |
| 177 | + font-size: 1rem; |
| 178 | + font-weight: bold; |
| 179 | + p { |
| 180 | + margin: 0; |
| 181 | + color: var(--info); |
| 182 | + &.success { color: var(--success); } |
| 183 | + &.failure { color: var(--danger); } |
| 184 | + &.error { color: var(--danger); } |
| 185 | + &.running { color: var(--neutral); } |
| 186 | + } |
| 187 | + span { |
| 188 | + font-size: 0.75rem; |
| 189 | + color: var(--secondary); |
| 190 | + } |
| 191 | + } |
| 192 | + .info { |
| 193 | + div.build-name { |
| 194 | + margin: 0.25rem 0; |
| 195 | + font-weight: bold; |
| 196 | + color: var(--widget-text-color); |
| 197 | + a, a:hover, a:visited, a:active { |
| 198 | + color: inherit; |
| 199 | + text-decoration: none; |
| 200 | + } |
| 201 | + .droneci-build-number::before { |
| 202 | + content: "#"; |
| 203 | + } |
| 204 | + } |
| 205 | + div.build-desc { |
| 206 | + margin: 0; |
| 207 | + font-size: 0.85rem; |
| 208 | + color: var(--widget-text-color); |
| 209 | + opacity: var(--dimming-factor); |
| 210 | + a, a:hover, a:visited, a:active { |
| 211 | + color: inherit; |
| 212 | + text-decoration: none; |
| 213 | + } |
| 214 | + .droneci-extra { |
| 215 | + .droneci-extra-info { |
| 216 | + margin: 0.25em; |
| 217 | + padding: 0em 0.25em; |
| 218 | + background: var(--item-background); |
| 219 | + border: 1px solid var(--primary); |
| 220 | + border-radius: 5px; |
| 221 | + } |
| 222 | + } |
| 223 | +
|
| 224 | + } |
| 225 | + } |
| 226 | + &:not(:last-child) { |
| 227 | + border-bottom: 1px dashed var(--widget-text-color); |
| 228 | + } |
| 229 | + } |
| 230 | +} |
| 231 | +
|
| 232 | +</style> |
| 233 | + |
| 234 | +<style lang="scss"> |
| 235 | +.build-info-tt { |
| 236 | + min-width: 20rem; |
| 237 | +} |
| 238 | +</style> |
0 commit comments