diff options
author | Marek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl> | 2023-03-01 12:27:28 +0100 |
---|---|---|
committer | Marek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl> | 2023-03-03 13:46:02 +0100 |
commit | 70fa03898ee412e30b6b87cf961004bf16ccaef4 (patch) | |
tree | 10cca3196bd5db69ee643316365a00f1276dba04 /chained-ci-vue | |
parent | 0399d9842c2a5670e4ee21d45343d2ac168eee2d (diff) |
[GATING] Add configuration for Azure3 gating in the fork of chained-ci
This change includes:
* moving submodules of chained-ci-roles and chained-ci-vue as static folders
to the repo (they were quite old and not updated for some time)
* create azure access artifacts
* add config for azure3 gating pipeline
Issue-ID: INT-2207
Signed-off-by: Marek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl>
Change-Id: Idb475c166d78f10ed4204153ab634110aa9093f6
Diffstat (limited to 'chained-ci-vue')
-rw-r--r-- | chained-ci-vue/LICENSE | 201 | ||||
-rw-r--r-- | chained-ci-vue/README.md | 4 | ||||
-rw-r--r-- | chained-ci-vue/favicon.png | bin | 0 -> 5898 bytes | |||
-rw-r--r-- | chained-ci-vue/index.html | 238 | ||||
-rwxr-xr-x | chained-ci-vue/init.sh | 68 | ||||
-rw-r--r-- | chained-ci-vue/js/config.js | 16 | ||||
-rw-r--r-- | chained-ci-vue/js/index.js | 250 | ||||
-rw-r--r-- | chained-ci-vue/js/lib.js | 558 | ||||
-rw-r--r-- | chained-ci-vue/js/visibility.LICENSE | 3 | ||||
-rw-r--r-- | chained-ci-vue/js/visibility.core.js | 189 | ||||
-rw-r--r-- | chained-ci-vue/js/visibility.timers.js | 161 | ||||
-rw-r--r-- | chained-ci-vue/logo.svg | 130 | ||||
-rw-r--r-- | chained-ci-vue/style.css | 219 |
13 files changed, 2037 insertions, 0 deletions
diff --git a/chained-ci-vue/LICENSE b/chained-ci-vue/LICENSE new file mode 100644 index 0000000..3be62f8 --- /dev/null +++ b/chained-ci-vue/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Orange-OpenSource / lfn / ci_cd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/chained-ci-vue/README.md b/chained-ci-vue/README.md new file mode 100644 index 0000000..e4baa04 --- /dev/null +++ b/chained-ci-vue/README.md @@ -0,0 +1,4 @@ +Chained-ci-vue +================ + +Submodule for a better visualization of the chained-ci diff --git a/chained-ci-vue/favicon.png b/chained-ci-vue/favicon.png Binary files differnew file mode 100644 index 0000000..6e07930 --- /dev/null +++ b/chained-ci-vue/favicon.png diff --git a/chained-ci-vue/index.html b/chained-ci-vue/index.html new file mode 100644 index 0000000..abc92bc --- /dev/null +++ b/chained-ci-vue/index.html @@ -0,0 +1,238 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + + <title>Pipelines</title> + + <!-- VUE JS development version, includes helpful console warnings --> + <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> + + <!-- My scripts --> + <script src="js/config.js"></script> + <script src="js/lib.js"></script> + + <!-- Visisbilityjs --> + <script src="js/visibility.core.js"></script> + <script src="js/visibility.timers.js"></script> + + <!-- CSS --> + <link rel="icon" href="favicon.png"> + <link rel="stylesheet" href="style.css"> + <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css"> + <link rel="stylesheet" href="https://www.w3schools.com/lib/w3-theme-blue-grey.css"> + <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU" crossorigin="anonymous"> + </head> + + <body> + <div class="main"> + <header class="w3-container w3-theme w3-card" id="header"> + <h1 v-on:click="update($event)" + class='header w3-center'>{{ project.name }} UI</h1> + </header> + + <script type="text/x-template" id="modal-template"> + <transition name="modal"> + <div class="modal-mask" v-on:click="closeModal($event, $emit)"> + <div class="modal-wrapper"> + <div class="modal-container"> + <div class="modal-header"><slot name="header"></slot></div> + <div class="modal-body"><slot name="body"></slot></div> + <div class="modal-footer"> + <slot name="footer"> + </slot> + </div> + </div> + </div> + </div> + </transition> + </script> + + <section id="auth"> + <div class="w3-card-4" v-if="!globalAccessGranted"> + <div class="w3-container"> + <h2>Please set your gitlab<span v-if="privateTokens.length > 1">s</span> + private token<span v-if="privateTokens.length > 1">s</span> + </h2> + </div> + <form @submit="checkForm" class="w3-container"> + <div v-for="token in privateTokens"> + <label> + <a v-bind:href="'https://'+token.target+gitlabProfileToken">{{ token.target }}</a></label> + <label v-if="token.msg">[ {{ token.msg }} ]</label> + <div class="w3-xlarge statusIcon" + v-bind:class="[ token.icon ]"></div> + <input + v-model="token.value" + class="w3-input" + v-bind:id="token.target" + type="password"> + </input> + </div> + <button type="submit" class="w3-btn">Validate</button> + </form> + <div> + <div> + <div>this is required and can be generated on your user profile like: + <a v-bind="{ href: gitlabProfileToken }">{{gitlabProfileToken}}</a> + (Only API option is needed)</div> + </div> + </div> + </div> + </section> + + <section class="w3-ul w3-border-top" id="pipelines"> + <div v-if="accessGranted"> + <div class="tools w3-theme-l5"> + <div class='tool_sc w3-theme-l4 w3-opacity'> + <b>Scenario filter:</b> + <input v-model="pipelineFilter" placeholder="filter"> + </div> + <div class='tool_timer w3-theme-l4 w3-opacity'> + <b>Next update: </b>{{ timer }} / {{ actualRefresh }} + <i v-if="! optimizedRefresh"> + (Please set filter or optimize it to have a better update time) + </i> + </div> + <div class='tool_new w3-theme-l4'> + <a v-bind="{ href: newPipelineUrl }" target='_blank'> + <div class='fab fa-gitlab w3-text-orange w3-large w3-statusIcon'></div> + New pipeline + </a> + </div> + </div> + <div v-for="id in sortedPipelinesIds"> + <div class='pipeline w3-theme-l5'> + <div class='pipeline_header w3-center w3-theme-l4 w3-display-container '> + <a v-bind="{ href: pipelines[id].url }" target='_blank'> + <div class='pipeline_statusIcon w3-xxlarge statusIcon w3-padding w3-display-middle' + v-bind:class="[ pipelines[id].statusIcon ]"></div></a> + <div class='pipeline_scenario w3-opacity'>{{ pipelines[id].scenario }}</div> + <div class='pipeline_branch'>{{ pipelines[id].branch }}</div> + <div class='pipeline_date'>{{ pipelines[id].date }}</div> + <div class='pipeline_time'>{{ pipelines[id].time }}</div> + <div class='pipeline_duration'>{{ Math.round(pipelines[id].details.duration/60) }} min</div> + <div class='pipeline_user_icon w3-padding'> + <img v-bind="{ src:pipelines[id].userAvatar, alt:pipelines[id].user}"></img> + </div> + </div> + <div class='pipeline_stages w3-theme-l4'> + <div v-for="stage in pipelines[id].stages"> + <div class='stage'> + <div class='stage_name w3-opacity'>{{ stage.name }}</div> + <div v-for='job in stage.jobs'> + <div class='w3-round w3-theme-l5 w3-btn w3-padding-small w3-block'> + <div v-if="!job.internal"> + <div class='job'> + <div class='job_statusIcon w3-large statusIcon' + v-bind:class="[ job.statusIcon ]" + @mouseover="mouseOverJob(job)" + @mouseleave="mouseLeaveJob(job)" + v-on:click="jobAction(job)" + ></div> + <div class='job_name' + v-on:click="jobDetails($event, job)"> + {{ job.shortname }}</div> + </div> + </div> + <div v-if="job.internal"> + <div class='job'> + <div class='job_statusIcon w3-large statusIcon' + v-bind:class="[ job.statusIcon ]" + @mouseover="mouseOverJob(job)" + @mouseleave="mouseLeaveJob(job)" + v-on:click="jobAction(job)"></div> + <div class='job_name'> + <a v-bind="{ href: job.web_url }" target='_blank'> + {{ job.shortname }}</a> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div> + <div class='pipeline w3-theme-l5'> + <div class='pipeline_loader w3-theme-l2 w3-center w3-opacity' + v-on:click="loadMore()"> + Load more pipelines + </div> + </div> + </div> + </div> + </section> + + + + <section id='alert'> + <div class='masq'> + <modal v-if="showModal" @close="showModal = false"> + <h3 slot="header"> + {{ title }} + </h3> + <div slot="body"> + {{ message }} + </div> + </modal> + </div> + </section> + + + <section id='task_details'> + <div class='masq'> + <modal v-if="showModal" @close="showModal = false"> + <h3 slot="header"> + <div class='job_statusIcon w3-large statusIcon' + v-bind:class="[ pipeline.statusIcon ]" + @mouseover="mouseOverPipeline(pipeline)" + @mouseleave="mouseLeavePipeline(pipeline)" + v-on:click="jobAction(pipeline)"></div> + <a v-bind="{ href: pipeline.url }" target='_blank'> + Pipeline {{ pipeline.name }} {{ pipeline.id }} + </a> + <a v-bind="{ href: pipeline.console }" target='_blank'> + <div class='fa fa-terminal w3-theme-l2 w3-large w3-statusIcon'></div> + </a> + + </h3> + <div slot="body"> + <div v-if="showWaiting"> + <div class='w3-xxlarge fa fa-sync w3-text-blue-gray statusIcon'>Loading, please wait...</div> + </div> + <div v-if="showPipeline"> + <div v-for="stage in pipeline.stages"> + <div class='stage'> + <div class='stage_name w3-opacity'>{{ stage.name }}</div> + <div v-for='job in stage.jobs'> + <div class='w3-round w3-theme-l5 w3-btn w3-block'> + <a v-bind="{ href: job.web_url }" target='_blank'> + <div class='job'> + <div class='job_statusIcon w3-large statusIcon' + v-bind:class="[ job.statusIcon ]"></div> + <div class='job_name'>{{ job.name }}</div> + </div> + </a> + </div> + </div> + </div> + </div> + </div> + <div v-if="chainedCiFailure"> + <div>The pipeline was probably not triggered, check console: + <a v-bind="{ href: pipeline.console }" target='_blank'> + <div class='fa fa-terminal w3-theme-l2 w3-large w3-statusIcon'></div> + </a> + </div> + </div> + </div> + </modal> + </div> + </section> + </div> + <script src="js/index.js"></script> + </body> +</html> diff --git a/chained-ci-vue/init.sh b/chained-ci-vue/init.sh new file mode 100755 index 0000000..ff308e8 --- /dev/null +++ b/chained-ci-vue/init.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env sh + +export RUN_SCRIPT=${0} +export INV_FOLDER=${1} +export ROOT_FOLDER=${PWD} + +mkdir ${ROOT_FOLDER}/public +mkdir -p ${ROOT_FOLDER}/public/${INV_FOLDER}/host_vars +mkdir -p ${ROOT_FOLDER}/public/${INV_FOLDER}/group_vars + +yaml2json(){ + SRC=$1 + DEST="public/${SRC%.*}.json"; + echo "convert ${SRC} to ${DEST}" + yq '.' ${SRC} > ${DEST} +} + +updateConf(){ + ITEM=$1 + VALUE=$(echo $2| sed 's/:/\\:/g') + echo "set $ITEM to $VALUE" + sed -i -e "s|^var $ITEM = .*$|var $ITEM = \"$VALUE\";|" ${ROOT_FOLDER}/public/js/config.js +} + +updateConfObj(){ + ITEM=$1 + VALUE=$(echo $2| sed 's/:/\\:/g') + echo "set $ITEM to $VALUE" + sed -i -e "s|^var $ITEM = .*$|var $ITEM = $VALUE;|" ${ROOT_FOLDER}/public/js/config.js +} + +## Install Yq +pip install --user yq +include_splitted=$( yq -r '.include' ${CI_CONFIG_PATH}) +# Convert chained_ci files to json +if [ -z "${include_splitted}" ]; then + # Monolitic gitlab ci, doing nothing + echo "no need to merge splitted files" +else + # Non monolitic gitlab ci, add the yaml part into main one + for part_file in $(yq -r '.include[]' ${CI_CONFIG_PATH}); do + sed 's/---//' $part_file >> ${CI_CONFIG_PATH} + done +fi +yaml2json ${CI_CONFIG_PATH} + +for sc in ${INV_FOLDER}/host_vars/*.yml; do + yaml2json $sc +done +yaml2json ${INV_FOLDER}/group_vars/all.yml + +# Copy site +cp -rf chained-ci-vue/js public/ +cp -rf chained-ci-vue/index.html public/ +cp -rf chained-ci-vue/style.css public/ + +# Generate config +updateConf gitlabUrl ${CI_PROJECT_URL%"$CI_PROJECT_PATH"} +updateConf chainedCiProjectId ${CI_PROJECT_ID} +updateConf scenarioFolder "${INV_FOLDER}/" +updateConf chainedCiUrl ${CI_PROJECT_URL} +updateConf gitlabCiFilename "${CI_CONFIG_PATH%.*}.json" +updateConf pipelines_size ${pipeline_size:-10} +updateConf rootUrl "${CI_PROJECT_NAME}/" + +# get all gitlab used +tokenTargets=$(jq -r '.gitlab.git_projects | map(try(.url | split("/")| .[2]))| sort | unique | @csv' ${ROOT_FOLDER}/public/${INV_FOLDER}/group_vars/all.json) +updateConfObj tokenTargets "[${tokenTargets}]" diff --git a/chained-ci-vue/js/config.js b/chained-ci-vue/js/config.js new file mode 100644 index 0000000..f71ff70 --- /dev/null +++ b/chained-ci-vue/js/config.js @@ -0,0 +1,16 @@ +// Generated by init.sh +var gitlabUrl = ; +var chainedCiProjectId = ; +var scenarioFolder = ; +var chainedCiUrl = ; +var rootUrl = ; +var gitlabCiFilename = ; +var pipelines_size = ; +var updateTimer = 60; +var tokenTargets = []; +var optimizedRefreshLevel = 5; + +// tool url +var gitlabCiFile = gitlabCiFilename; +var gitlabApi = gitlabUrl+'api/v4/projects/'; +var gitlabProfileToken = '/profile/personal_access_tokens'; diff --git a/chained-ci-vue/js/index.js b/chained-ci-vue/js/index.js new file mode 100644 index 0000000..447a27b --- /dev/null +++ b/chained-ci-vue/js/index.js @@ -0,0 +1,250 @@ +// Init token we need to fetch +var privateTokens = []; +tokenTargets.forEach(function(target){ + privateTokens.push({'target': target, + 'value': '', + 'msg': '', + 'icon': '', + 'accessGranted': false}) +}) +pipelineRefreshId = -1; +// Start the authentication +authenticate(); + + +/** + * VUE for authentication form + * + * @data {list} privateTokens list of token objects + * @data {list} gitlabProfileToken list of gitlab token to ask + * + * @computed {dict} tokensByTarget dict of tokens by target + * @computed {dict} globalAccessGranted remove the form if all token are verified + * + * @methods {function} check the form by starting validateTokens + */ +var headerVue = new Vue({ + el: '#header', + data: {project: {}} +}); + +/** + * VUE for pipelines + * + * @data {bool} loading lock var to avoid concurrent load + * @data {dict} pipelines dict of pipelines + * @data {list} pipelinesIds list of pipelines + * @data {bool} accessGranted show the pipelines + * @data {int} pages page indication + * + * @computed {list} sortedPipelinesIds list of reverse pipelines ids + * + * @methods {function} job_details load job details on click + * @methods {function} handleScroll load new pipelines on scroll to bottom + */ +var pipelinesVue = new Vue({ + el: '#pipelines', + data: { + loading: false, + newPipelineUrl: '', + pipelines: {}, + pipelinesIds: [], + accessGranted: false, + pipelineFilter: '', + pages: 1, + timer: updateTimer, + actualRefresh: updateTimer * 2, + optimizedRefresh: false, + }, + computed: { + sortedPipelinesIds: function() { + filteredList = []; + var pipelines = this.pipelines; + var filter = this.pipelineFilter; + this.pipelinesIds.sort().reverse().forEach( + function(pipelineId){ + if(pipelines[pipelineId].scenario.includes(filter)){ + filteredList.push(pipelineId) + } + } + ); + this.optimizedRefresh = (filteredList.length <= optimizedRefreshLevel); + if(this.optimizedRefresh){ + updateLoop(updateTimer/2); + }else{ + updateLoop(updateTimer); + } + return filteredList; + } + }, + methods:{ + mouseOverJob: function(job){ + iconMouseOver(job); + }, + mouseLeaveJob: function(job){ + iconMouseLeave(job); + }, + jobAction: function(job){ + jobActionSwitch(job.status, job.id) + }, + jobDetails: function(event, job){ + taskDetailsVue.showModal = true; + taskDetailsVue.showWaiting = true; + taskDetailsVue.showPipeline = false; + loadSubPipeline(job.id, job.name); + }, + loadMore: function() { + if (!pipelinesVue.loading){ + pipelinesVue.loading = true; + pipelinesVue.pages += 1; + loadPipelines(pipelinesVue.pages); + } + }, + } +}); + +// Vue.directive('scroll', { +// inserted: function(el, binding) { +// let f = function(evt) { +// if (binding.value(evt, el)) {} +// }; +// window.addEventListener('scroll', f); +// }, +// }); + +/** + * VUE for authentication form + * + * @data {list} privateTokens list of token objects + * @data {list} gitlabProfileToken list of gitlab token to ask + * + * @computed {dict} tokensByTarget dict of tokens by target + * @computed {dict} globalAccessGranted remove the form if all token are verified + * + * @methods {function} check the form by starting validateTokens + */ +var authVue = new Vue({ + el: '#auth', + data: {privateTokens: privateTokens, + gitlabProfileToken: gitlabProfileToken}, + methods:{ + checkForm: function (e) { + validateTokens(this.privateTokens); + e.preventDefault(); + } + }, + computed: { + tokensByTarget: function() { + tokens = {} + this.privateTokens.forEach(function(token){ + tokens[token.target] = token.value + }); + return tokens + }, + globalAccessGranted: function() { + granted = true; + this.privateTokens.forEach(function(token){ + granted = (granted && token.accessGranted) + }); + if (granted){ + localStorage.setItem("chained_ci_tokens", JSON.stringify(this.privateTokens)); + load() + } + pipelinesVue.accessGranted = granted + return granted; + } + } +}); + +/** + * VUE for the detail of a job (show the sub pipeline) + * + * @data {bool} showModal show the modal vue + * @data {bool} showPipeline show the pipeline + * @data {bool} showWaiting show the waiting message + * @data {bool} chainedCiFailure prompt a message of chained ci failed + * @data {dict} pipeline pipeline data + */ +var taskDetailsVue = new Vue({ + el: '#task_details', + data: { + showModal: false, + showPipeline: false, + showWaiting: false, + chainedCiFailure: false, + pipeline: { + 'name': '', + 'url': '', + 'id': '', + 'status': '', + 'statusIcon': '', + 'console': '', + 'stages': [], + 'parentTaskId': '', + 'parentTaskName': '', + } + }, + methods:{ + mouseOverPipeline: function(pipeline){ + iconMouseOver(pipeline); + }, + mouseLeavePipeline: function(pipeline){ + iconMouseLeave(pipeline); + }, + jobAction: function(pipeline){ + console.log(pipeline) + this.showModal = false; + this.showPipeline = false; + jobActionSwitch(pipeline.status, pipeline.parentTaskId) + } + } +}); + +/** + * VUE for alert + * + * @data {bool} showModal show the modal vue + * @data {bool} showPipeline show the pipeline + * @data {bool} showWaiting show the waiting message + * @data {bool} chainedCiFailure prompt a message of chained ci failed + * @data {dict} pipeline pipeline data + */ +var alertVue = new Vue({ + el: '#alert', + data: { + showModal: false, + title: '', + message: '', + } +}); + +// Modal template +Vue.component('modal', { + template: '#modal-template', + methods:{ + closeModal: function(event, emit){ + if(event.target.className == 'modal-wrapper'){ + // emit('close') + alertVue.showModal = false + alertVue.title = '' + alertVue.message = '' + taskDetailsVue.showModal = false + taskDetailsVue.showPipeline = false + taskDetailsVue.showWaiting = false + taskDetailsVue.chainedCiFailure = false + taskDetailsVue.pipeline = { + 'name': '', + 'url': '', + 'id': '', + 'status': '', + 'statusIcon': '', + 'console': '', + 'stages': [], + 'parentTaskId': '', + 'parentTaskName': '', + } + updatePipelines + } + }, + }, +}) diff --git a/chained-ci-vue/js/lib.js b/chained-ci-vue/js/lib.js new file mode 100644 index 0000000..8ce3e11 --- /dev/null +++ b/chained-ci-vue/js/lib.js @@ -0,0 +1,558 @@ +/** + * Different functions to load pipelines and jobs from gitlab + * + * Description. (use period) + * + * @link https://gitlab.com/Orange-OpenSource/lfn/ci_cd/chained-ci-vue/blob/master/js/lib.js + * @file lib.js + * @author David Blaisonneau + */ + + + /** + * Load the pipelines + */ +function load(){ + // Load gitlab-ci and save it to configCi var + getJson(gitlabCiFile, function(resp) { + configCi = resp; + // Get Gitlab project name + loadTitle(); + // Load last pipelines + pipelinesVue.loading = true; + loadPipelines(0); + + setInterval(function (){ + if(pipelinesVue.timer > 0 ){ + pipelinesVue.timer = pipelinesVue.timer - 1; + } + }, 1000); + updateLoop(updateTimer) + // setInterval(() => { + // console.log("set refresh to "+ (updateTimer * 1000)+ "ms"); + // updatePipelines(); + // }, updateTimer * 1000); + }); +} + +function updateLoop(timer){ + // The refresh time has changed, update loop + if(timer != pipelinesVue.actualRefresh){ + // If we had a Visibility loop, stop it + if(pipelineRefreshId >= 0){ + console.log("stop actual refresh loop ["+pipelineRefreshId+"]") + Visibility.stop(pipelineRefreshId); + } + pipelinesVue.timer = timer + pipelinesVue.actualRefresh = timer; + console.log("set refresh to "+ timer + "s for next update"); + pipelineRefreshId = Visibility.every(timer * 1000, + function (){ + console.log("Update pipelines, then sleep for "+ timer +" seconds") + // Update the pipelines + updatePipelines(); + // Update the timer + pipelinesVue.timer = timer + }); + } + + +} + +/** + * Sort jobs by stage + * + * Get the whole list of jobs in a pipeline and return a list of stages dict + * containing the name of the stage and the list of jobs in this stage + * + * @params {list} jobs List of jobs sent by the /jobs API + * + * @return {list} List of {'name': 'stage name', 'jobs': []} + */ +function jobsByStages(jobs){ + stages = {}; + stagesList = stages2List(jobs) + stagesList.forEach(function(stage){ + stages[stage]={'name': stage, 'jobs': []}}) + jobs.forEach(function(job){ + job.statusIcon = getIcon(job.status); + stages[job.stage].jobs.push(job) + }); + jobsByStagesList = [] + // return a list order by stage step + stagesList.forEach(function(stage){ + jobsByStagesList.push(stages[stage]) + }); + return jobsByStagesList +} + +/** + * Get stages list from jobs list + * + * Get a list of stages used by the jobs + * + * @params {list} jobs List of jobs sent by the /jobs API + * + * @return {list} List of stage names + */ +function stages2List(jobs){ + stagesList = [] + jobs.forEach(function(job) { + if(stagesList.indexOf(job.stage) < 0){ + stagesList.push(job.stage) + } + }) + return stagesList +} + +/** + * Get an icon class + * + * Get an icon class name from a string + * + * @params {str} type name of the icon to get + * + * @return {str} icon class + */ +function getIcon(type){ + switch(type){ + case 'failed': + return 'fa fa-times-circle w3-text-red' + break; + case 'success': + return 'fa fa-check-circle w3-text-green' + break; + case 'running': + return 'fa fa-play-circle w3-text-blue' + break; + case 'waiting': + return 'fa fa-pause-circle w3-text-orange' + break; + case 'skipped': + return 'fa fa-dot-circle w3-text-blue-gray' + break; + case 'created': + return 'fa fa-circle-notch w3-text-blue-gray' + break; + case 'canceled': + return 'fa fa-stop-circle w3-text-blue-gray' + break; + case 'retry': + return 'fa fa-plus-circle w3-text-orange' + break; + case 'stop': + return 'fa fa-stop-circle w3-text-orange' + break; + default: + return 'fa fa-question-circle' + } +} + + +/** + * Wrapper to call gitlab api + * + * @params {str} project gitlab project id + * @params {str} call api function called + * @params {function} reqOnload function to call on load + * @params {str} api base gitlab api to call + * @params {str} method HTTP Method + */ +function gitlabCall(project, call, reqOnload, + api = gitlabApi, method = 'GET'){ + var requestURL = api+project+'/'+call; + getJson(requestURL, reqOnload, getToken(api), method); +} + +/** + * Get a JSON from an api + * + * GET a json from an url and run a function on it + * + * @params {str} requestURL gitlab project id + * @params {function} reqOnload function to call on load + * @params {str} token PRIVATE-TOKEN to add if needed (default: null) + * @params {str} method HTTP Method + */ +function getJson(requestURL, reqOnload, token = null, method = 'GET'){ + var request = new XMLHttpRequest(); + request.open(method, requestURL); + if (token){ + request.setRequestHeader('PRIVATE-TOKEN', token); + }; + request.responseType = 'json'; + request.send(); + request.onload = function() { + reqOnload(request.response); + } +} + +/** + * Get the token of a gitlab API + * + * Get API token from auth vector depending of the url to call + * + * @params {string} url the url to call + */ +function getToken(url){ + target = url.split('/')[2] + return authVue.tokensByTarget[target] +} + +/** + * Validate all API tokens + * + * Call gitlab API with a token to check it + * - Set VUEJS privateTokens.globalAccessGranted to boolean result of all + * authentications + * - Update privateTokens list to update the vue + * + * @params {list} tokens list of tokens from the form + */ +function validateTokens(tokens){ + tokens.forEach(function(token){ + if(token.value){ + getJson( + requestURL = 'https://'+ token.target + + '/api/v4/projects/?per_page=1', + function(resp){ + globalSuccess = true; + success = (resp.length == 1) + privateTokens.forEach(function(globalToken){ + if(token.target == globalToken.target){ + globalToken.accessGranted = success; + if(success){ + globalToken.icon = getIcon('success'); + }else{ + globalToken.icon = getIcon('failed'); + } + } + globalSuccess = (globalSuccess && globalToken.accessGranted) + }) + privateTokens.globalAccessGranted = globalSuccess + }, + token.value); + } + }); +} + +/** + * Authenticate at startup + * + * Recover saved tokens in localStorage and start token validation + */ +function authenticate(){ + // Try to authenticate with local token stored + if (typeof(Storage) !== "undefined") { + savedTokens = {} + savedTokensList = JSON.parse(localStorage.getItem("chained_ci_tokens")); + if(savedTokensList){ + savedTokensList.forEach(function(token){ + savedTokens[token.target] = token.value; + }) + privateTokens.forEach(function(token){ + if(token.target in savedTokens){ + token.value = savedTokens[token.target] + } + }); + validateTokens(privateTokens); + } + } else { + authVue.error = "No local storage, must authenticate again" + } +} + +/** + * Just load the page title from chained-ci project name + */ +function loadTitle(){ + gitlabCall(chainedCiProjectId, '', function(resp) { + headerVue.project = resp; + pipelinesVue.newPipelineUrl = resp.web_url + '/pipelines/new'; + }); +} + +/** + * Load pipelines of the project + * + * Call gitlab API to get the pipelines and prepare them + * - set global pipelines info + * - load pipeline jobs + * - for each job: + * - get the scenario names + * - clean jobs names + * - set if a job is a sub pipeline + * + * @params {string} page the page of the api to call (to load them by smal bulks) + */ +function loadPipelines(page = 1, size = pipelines_size){ + gitlabCall(chainedCiProjectId, 'pipelines?page='+page+'&per_page='+size, function(resp) { + console.log('load page '+page+' with size '+ size) + previous_pipelinesIds = Object.keys(pipelinesVue.pipelines); + previous_sorted_pipelinesIds = pipelinesVue.sortedPipelinesIds; + pipelines = resp; + res = {} + // Add more info to pipelines + pipelines.forEach(function(pipeline) { + console.log(pipeline) + load_it = false + if (previous_pipelinesIds.indexOf(pipeline.id.toString()) < 0 ){ + console.log("new pipeline " + pipeline.id); + load_it = true; + }else if (previous_sorted_pipelinesIds.indexOf(pipeline.id.toString()) >= 0 ){ + console.log("sorted pipeline " + pipeline.id); + load_it = true; + }else{ + console.log("filtered existing pipeline " + pipeline.id + ", pass"); + } + if(load_it){ + res[pipeline.id] = {}; + res[pipeline.id].id = pipeline.id; + res[pipeline.id].status = pipeline.status; + res[pipeline.id].statusIcon = getIcon(pipeline.status); + res[pipeline.id].scenario = ''; + res[pipeline.id].branch = pipeline.ref; + res[pipeline.id].details = {}; + res[pipeline.id].stages = []; + res[pipeline.id].url = pipeline.web_url; + // Add details + gitlabCall(chainedCiProjectId, 'pipelines/'+pipeline.id, function(resp) { + res[pipeline.id].details = resp; + dt = resp.started_at.split('T'); + res[pipeline.id].date = dt[0]; + res[pipeline.id].time = dt[1].split('.')[0]; + res[pipeline.id].user = resp.user.name; + res[pipeline.id].userAvatar = resp.user.avatar_url; + }); + // Add jobs + gitlabCall(chainedCiProjectId, 'pipelines/'+pipeline.id+'/jobs', function(resp) { + jobs = resp; + // get scenario name + names = [] + jobs.forEach(function(job){ + if (job.name in configCi){ + if ('variables' in configCi[job.name]){ + if ('pod' in configCi[job.name].variables){ + name = configCi[job.name].variables.pod; + if(!names.includes(name)){names.push(name);} + } + } + } + }); + if(names.length){ + res[pipeline.id].scenario = names.join(' + ') + }else{ + res[pipeline.id].scenario = 'Internal' + } + + // test if it trig another pipeline + jobs.forEach(function(job){ + if (job.name in configCi){ + job.internal = (configCi[job.name].script[0].search('run-ci') < 0) + }else{ + job.internal = true; + } + // clean jobs names and remove the scenario name in it + if(job.name.search(res[pipeline.id].scenario)>=0){ + job.shortname = job.name.split(':').slice(0,-1).join(':') + }else{ + job.shortname = job.name + } + }); + + res[pipeline.id].stages = jobsByStages(jobs); + }); + }else{ + console.log("push previous values") + res[pipeline.id] = pipelinesVue.pipelines[pipeline.id] + } + }); + pipelinesVue.pipelines = Object.assign({}, pipelinesVue.pipelines, res) + pipelinesVue.pipelinesIds = Object.keys(pipelinesVue.pipelines); + pipelinesVue.loading = false; + }); +} + + +/** + * Update pipeline + * + * This function is trigged by a setInterval() and refresh all pipelines + * + */ +function updatePipelines(){ + // Update subpipline + if(taskDetailsVue.pipeline.status == 'running' ){ + console.log('update task') + loadSubPipeline(taskDetailsVue.pipeline.parentTaskId, + taskDetailsVue.pipeline.parentTaskName) + } + // Update all piplines + loadPipelines(0, pipelinesVue.pages * pipelines_size) +} + + +/** + * Run an action on a pipeline job + * + * Call gitlab API to get run an action on a pipeline job + * - set global pipelines info + * - load pipeline jobs + * - for each job: + * - get the scenario names + * - clean jobs names + * - set if a job is a sub pipeline + * + * @params {string} action the action to run, in ['cancel', 'retry'] + * @params {int} jobId the job ID + */ +function jobAction(action, jobId){ + gitlabCall( + chainedCiProjectId, + 'jobs/'+jobId+'/'+action, + function(resp) { + alertVue.showModal = true; + alertVue.title = 'Action '+ action +' on job '+ jobId; + alertVue.message = 'Status: ' + resp.status; + console.log(resp) + + }, + gitlabApi, + 'POST' + ) +} + +/** + * Load a sub pipeline + * + * Load the a pipeline trigged by chained ci + * - Call the job logs and recover the subpipeline url + * - Load the pipeline info + * - Load the pipeline jobs + * + * @params {string} jobId The job ID inside a chained ci pipeline + * @params {string} jobName The job Name inside a chained ci pipeline + */ +function loadSubPipeline(jobId, originalJobName){ + + // Get project URL from static config + pod = configCi[originalJobName].variables.pod; + jobName = originalJobName.split(":")[0] + // Load the config of this scenario + getJson(scenarioFolder+'/host_vars/'+pod+'.json', function(scenario) { + project = scenario.scenario_steps[jobName].project; + // Load top config + getJson(scenarioFolder+'/group_vars/all.json', function(all) { + subprojectApi = all.gitlab.git_projects[project].api; + subprojectUrl = all.gitlab.git_projects[project].url; + // console.log(project_url); + + // Load the job log and search for the pipeline string + var request = new XMLHttpRequest(); + requestURL = gitlabApi+chainedCiProjectId+'/jobs/'+jobId+'/trace'; + request.open('GET', requestURL); + request.setRequestHeader('PRIVATE-TOKEN', getToken(gitlabApi) ); + request.send(); + request.onload = function() { + log = request.response; + regex = '\\* ' + subprojectUrl +'/pipelines/\\d+'; + regex = regex.replace(/\//g,'\\/'); + regex = regex.replace(/\:/g,'\\:'); + regex = regex.replace(/\./g,'\\.'); + regex = regex.replace(/\-/g,'\\-'); + filter = new RegExp(regex, 'm'); + m = log.match(filter); + if (m){ + subpipelineId = m[0].split('/').slice(-1)[0]; + // List subpipeline jobs + gitlabCall( + '', + 'pipelines/'+ subpipelineId, + function(pipeline) { + taskDetailsVue.pipeline.name = project; + taskDetailsVue.pipeline.id = subpipelineId; + taskDetailsVue.pipeline.parentTaskId = jobId; + taskDetailsVue.pipeline.parentTaskName = originalJobName; + taskDetailsVue.pipeline.chainedCiFailure = false; + taskDetailsVue.pipeline.status = pipeline.status; + taskDetailsVue.pipeline.statusIcon = getIcon(pipeline.status); + taskDetailsVue.pipeline.url = subprojectUrl+'/pipelines/'+subpipelineId; + taskDetailsVue.pipeline.console = chainedCiUrl+'/-/jobs/'+jobId; + }, + subprojectApi); + gitlabCall( + '', + 'pipelines/'+ subpipelineId +'/jobs', + function(jobs) { + jobs.forEach(function(job){ + if(job.name.search('triggered')>=0){ + job.name = job.name.split(':').slice(0,-1).join(':') + } + }); + stages = jobsByStages(jobs); + // console.log(stages); + taskDetailsVue.pipeline.stages = stages; + taskDetailsVue.showWaiting = false; + taskDetailsVue.showPipeline = true; + taskDetailsVue.chainedCiFailure = false; + }, + subprojectApi); + }else{ + taskDetailsVue.showWaiting = false; + taskDetailsVue.showPipeline = false; + taskDetailsVue.chainedCiFailure = true; + taskDetailsVue.pipeline.name = project; + taskDetailsVue.pipeline.status = 'failed'; + taskDetailsVue.pipeline.statusIcon = getIcon('failed'); + taskDetailsVue.pipeline.url = chainedCiUrl+'/-/jobs/'+jobId; + taskDetailsVue.pipeline.console = chainedCiUrl+'/-/jobs/'+jobId; + } + } + }); + }); +} + +/** + * Change icon on mouse over + * + * @params {object} target The target object + */ +function iconMouseOver(target){ + switch(target.status){ + case 'failed': + case 'success': + target.statusIcon = getIcon('retry'); + break; + case 'running': + target.statusIcon = getIcon('stop'); + break; + } +} + +/** + * Change icon on mouse leave + * + * @params {object} target The target object + */ +function iconMouseLeave(target){ + target.statusIcon = getIcon(target.status); +} + + +/** + * Action depending on job status + * + * @params {string} status The job status + * @params {int} target The job id + */ +function jobActionSwitch(status, jobId){ + switch(status){ + case 'failed': + case 'success': + jobAction('retry', jobId) + break; + case 'running': + jobAction('cancel', jobId) + break; + } +} diff --git a/chained-ci-vue/js/visibility.LICENSE b/chained-ci-vue/js/visibility.LICENSE new file mode 100644 index 0000000..9ef56c8 --- /dev/null +++ b/chained-ci-vue/js/visibility.LICENSE @@ -0,0 +1,3 @@ +source: https://github.com/ai/visibilityjs +LICENSE: MIT - https://github.com/ai/visibilityjs/blob/master/LICENSE +author: Andrey Sitnik - https://github.com/ai diff --git a/chained-ci-vue/js/visibility.core.js b/chained-ci-vue/js/visibility.core.js new file mode 100644 index 0000000..6dda095 --- /dev/null +++ b/chained-ci-vue/js/visibility.core.js @@ -0,0 +1,189 @@ +;(function (global) { + var lastId = -1; + + // Visibility.js allow you to know, that your web page is in the background + // tab and thus not visible to the user. This library is wrap under + // Page Visibility API. It fix problems with different vendor prefixes and + // add high-level useful functions. + var self = { + + // Call callback only when page become to visible for user or + // call it now if page is visible now or Page Visibility API + // doesn’t supported. + // + // Return false if API isn’t supported, true if page is already visible + // or listener ID (you can use it in `unbind` method) if page isn’t + // visible now. + // + // Visibility.onVisible(function () { + // startIntroAnimation(); + // }); + onVisible: function (callback) { + var support = self.isSupported(); + if ( !support || !self.hidden() ) { + callback(); + return support; + } + + var listener = self.change(function (e, state) { + if ( !self.hidden() ) { + self.unbind(listener); + callback(); + } + }); + return listener; + }, + + // Call callback when visibility will be changed. First argument for + // callback will be original event object, second will be visibility + // state name. + // + // Return listener ID to unbind listener by `unbind` method. + // + // If Page Visibility API doesn’t supported method will be return false + // and callback never will be called. + // + // Visibility.change(function(e, state) { + // Statistics.visibilityChange(state); + // }); + // + // It is just proxy to `visibilitychange` event, but use vendor prefix. + change: function (callback) { + if ( !self.isSupported() ) { + return false; + } + lastId += 1; + var number = lastId; + self._callbacks[number] = callback; + self._listen(); + return number; + }, + + // Remove `change` listener by it ID. + // + // var id = Visibility.change(function(e, state) { + // firstChangeCallback(); + // Visibility.unbind(id); + // }); + unbind: function (id) { + delete self._callbacks[id]; + }, + + // Call `callback` in any state, expect “prerender”. If current state + // is “prerender” it will wait until state will be changed. + // If Page Visibility API doesn’t supported, it will call `callback` + // immediately. + // + // Return false if API isn’t supported, true if page is already after + // prerendering or listener ID (you can use it in `unbind` method) + // if page is prerended now. + // + // Visibility.afterPrerendering(function () { + // Statistics.countVisitor(); + // }); + afterPrerendering: function (callback) { + var support = self.isSupported(); + var prerender = 'prerender'; + + if ( !support || prerender != self.state() ) { + callback(); + return support; + } + + var listener = self.change(function (e, state) { + if ( prerender != state ) { + self.unbind(listener); + callback(); + } + }); + return listener; + }, + + // Return true if page now isn’t visible to user. + // + // if ( !Visibility.hidden() ) { + // VideoPlayer.play(); + // } + // + // It is just proxy to `document.hidden`, but use vendor prefix. + hidden: function () { + return !!(self._doc.hidden || self._doc.webkitHidden); + }, + + // Return visibility state: 'visible', 'hidden' or 'prerender'. + // + // if ( 'prerender' == Visibility.state() ) { + // Statistics.pageIsPrerendering(); + // } + // + // Don’t use `Visibility.state()` to detect, is page visible, because + // visibility states can extend in next API versions. + // Use more simpler and general `Visibility.hidden()` for this cases. + // + // It is just proxy to `document.visibilityState`, but use + // vendor prefix. + state: function () { + return self._doc.visibilityState || + self._doc.webkitVisibilityState || + 'visible'; + }, + + // Return true if browser support Page Visibility API. + // refs: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API + // + // if ( Visibility.isSupported() ) { + // Statistics.startTrackingVisibility(); + // Visibility.change(function(e, state)) { + // Statistics.trackVisibility(state); + // }); + // } + isSupported: function () { + return self._doc.hidden !== undefined || self._doc.webkitHidden !== undefined; + }, + + // Link to document object to change it in tests. + _doc: document || {}, + + // Callbacks from `change` method, that wait visibility changes. + _callbacks: { }, + + // Listener for `visibilitychange` event. + _change: function(event) { + var state = self.state(); + + for ( var i in self._callbacks ) { + self._callbacks[i].call(self._doc, event, state); + } + }, + + // Set listener for `visibilitychange` event. + _listen: function () { + if ( self._init ) { + return; + } + + var event = 'visibilitychange'; + if ( self._doc.webkitVisibilityState ) { + event = 'webkit' + event; + } + + var listener = function () { + self._change.apply(self, arguments); + }; + if ( self._doc.addEventListener ) { + self._doc.addEventListener(event, listener); + } else { + self._doc.attachEvent(event, listener); + } + self._init = true; + } + + }; + + if ( typeof(module) != 'undefined' && module.exports ) { + module.exports = self; + } else { + global.Visibility = self; + } + +})(this); diff --git a/chained-ci-vue/js/visibility.timers.js b/chained-ci-vue/js/visibility.timers.js new file mode 100644 index 0000000..546c24e --- /dev/null +++ b/chained-ci-vue/js/visibility.timers.js @@ -0,0 +1,161 @@ +;(function (window) { + var lastTimer = -1; + + var install = function (Visibility) { + + // Run callback every `interval` milliseconds if page is visible and + // every `hiddenInterval` milliseconds if page is hidden. + // + // Visibility.every(60 * 1000, 5 * 60 * 1000, function () { + // checkNewMails(); + // }); + // + // You can skip `hiddenInterval` and callback will be called only if + // page is visible. + // + // Visibility.every(1000, function () { + // updateCountdown(); + // }); + // + // It is analog of `setInterval(callback, interval)` but use visibility + // state. + // + // It return timer ID, that you can use in `Visibility.stop(id)` to stop + // timer (`clearInterval` analog). + // Warning: timer ID is different from interval ID from `setInterval`, + // so don’t use it in `clearInterval`. + // + // On change state from hidden to visible timers will be execute. + Visibility.every = function (interval, hiddenInterval, callback) { + Visibility._time(); + + if ( !callback ) { + callback = hiddenInterval; + hiddenInterval = null; + } + + lastTimer += 1; + var number = lastTimer; + + Visibility._timers[number] = { + visible: interval, + hidden: hiddenInterval, + callback: callback + }; + Visibility._run(number, false); + + if ( Visibility.isSupported() ) { + Visibility._listen(); + } + return number; + }; + + // Stop timer from `every` method by it ID (`every` method return it). + // + // slideshow = Visibility.every(5 * 1000, function () { + // changeSlide(); + // }); + // $('.stopSlideshow').click(function () { + // Visibility.stop(slideshow); + // }); + Visibility.stop = function(id) { + if ( !Visibility._timers[id] ) { + return false; + } + Visibility._stop(id); + delete Visibility._timers[id]; + return true; + }; + + // Callbacks and intervals added by `every` method. + Visibility._timers = { }; + + // Initialize variables on page loading. + Visibility._time = function () { + if ( Visibility._timed ) { + return; + } + Visibility._timed = true; + Visibility._wasHidden = Visibility.hidden(); + + Visibility.change(function () { + Visibility._stopRun(); + Visibility._wasHidden = Visibility.hidden(); + }); + }; + + // Try to run timer from every method by it’s ID. It will be use + // `interval` or `hiddenInterval` depending on visibility state. + // If page is hidden and `hiddenInterval` is null, + // it will not run timer. + // + // Argument `runNow` say, that timers must be execute now too. + Visibility._run = function (id, runNow) { + var interval, + timer = Visibility._timers[id]; + + if ( Visibility.hidden() ) { + if ( null === timer.hidden ) { + return; + } + interval = timer.hidden; + } else { + interval = timer.visible; + } + + var runner = function () { + timer.last = new Date(); + timer.callback.call(window); + } + + if ( runNow ) { + var now = new Date(); + var last = now - timer.last ; + + if ( interval > last ) { + timer.delay = setTimeout(function () { + timer.id = setInterval(runner, interval); + runner(); + }, interval - last); + } else { + timer.id = setInterval(runner, interval); + runner(); + } + + } else { + timer.id = setInterval(runner, interval); + } + }; + + // Stop timer from `every` method by it’s ID. + Visibility._stop = function (id) { + var timer = Visibility._timers[id]; + clearInterval(timer.id); + clearTimeout(timer.delay); + delete timer.id; + delete timer.delay; + }; + + // Listener for `visibilitychange` event. + Visibility._stopRun = function (event) { + var isHidden = Visibility.hidden(), + wasHidden = Visibility._wasHidden; + + if ( (isHidden && !wasHidden) || (!isHidden && wasHidden) ) { + for ( var i in Visibility._timers ) { + Visibility._stop(i); + Visibility._run(i, !isHidden); + } + } + }; + + return Visibility; + } + + if ( typeof(module) != 'undefined' && module.exports ) { + module.exports = install(require('./visibility.core')); + } else { + install(window.Visibility || require('./visibility.core')) + } + +})(window); diff --git a/chained-ci-vue/logo.svg b/chained-ci-vue/logo.svg new file mode 100644 index 0000000..1eb9231 --- /dev/null +++ b/chained-ci-vue/logo.svg @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="19.586046mm" + height="19.586046mm" + viewBox="0 0 19.586046 19.586046" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="logo.svg" + inkscape:export-filename="/home/edby8475/Dev/chained-ci-vue/logo.png" + inkscape:export-xdpi="98" + inkscape:export-ydpi="98"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="6.5333333" + inkscape:cx="7.8188803" + inkscape:cy="30.617961" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:window-width="2560" + inkscape:window-height="1403" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Calque 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-76.099242,-143.22085)"> + <circle + style="opacity:1;vector-effect:none;fill:#abc837;fill-opacity:1;stroke:#4d4d4d;stroke-width:0.50260705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path921" + cx="85.892265" + cy="153.01387" + r="9.5417194" /> + <g + id="g933"> + <path + sodipodi:nodetypes="cccc" + inkscape:connector-curvature="0" + id="path919" + d="m 87.755086,149.65335 h -3.722685 c 3.722685,0 0.03786,6.84943 3.737003,6.84401 v 0" + style="fill:none;stroke:#f2f2f2;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <g + transform="translate(-2.1166667)" + id="g862"> + <circle + style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#44aa00;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path848" + cx="83.371094" + cy="149.57428" + r="2.7487407" /> + <path + style="fill:none;stroke:#44aa00;stroke-width:0.62900001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 82.386279,149.28284 0.715902,0.81612 1.267143,-1.24567" + id="path850" + inkscape:connector-curvature="0" /> + </g> + <g + transform="translate(-0.03559777,6.879167)" + id="g858"> + <circle + r="2.7487407" + cy="149.57428" + cx="90.565697" + id="circle852" + style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#2a7fff;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <path + style="opacity:1;vector-effect:none;fill:#2a7fff;fill-opacity:1;stroke:none;stroke-width:0.36824697;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="circle854" + sodipodi:type="arc" + sodipodi:cx="90.415558" + sodipodi:cy="149.57428" + sodipodi:rx="1.9128479" + sodipodi:ry="1.9128479" + sodipodi:start="4.7106229" + sodipodi:end="2.5736704" + d="m 90.41218,147.66143 a 1.9128479,1.9128479 0 0 1 1.881627,1.55068 1.9128479,1.9128479 0 0 1 -1.17015,2.13913 1.9128479,1.9128479 0 0 1 -2.320669,-0.74807 l 1.61257,-1.02889 z" /> + </g> + <g + transform="translate(7.1590052)" + id="g868"> + <circle + r="2.7487407" + cy="149.57428" + cx="83.371094" + id="circle864" + style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#44aa00;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path866" + d="m 82.386279,149.28284 0.715902,0.81612 1.267143,-1.24567" + style="fill:none;stroke:#44aa00;stroke-width:0.62900001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + </g> + </g> +</svg> diff --git a/chained-ci-vue/style.css b/chained-ci-vue/style.css new file mode 100644 index 0000000..d2c7f8b --- /dev/null +++ b/chained-ci-vue/style.css @@ -0,0 +1,219 @@ +a { + text-decoration: none; +} + +html, body { + margin: 5px; + padding: 0; +} + +.main{ + margin: auto; + width: 70%; + min-width: 1200px; +} + +.tools { + margin: 10px auto; +} +.tools > div { + padding: 0.3em; + margin: 1em; +}.tools { + display: grid; + text-align: center; + grid-template-columns: repeat(8, 1fr ); + grid-gap: 5px; +} +.tool_sc { + grid-column: 1 / 3; +} +.tool_timer { + grid-column: 3 / 8; +} +.tool_new { + grid-column: 8; +} + + +.pipeline { + margin: 10px auto; +} +.pipeline > div { + padding: 1em; + margin: 1em; +}.pipeline { + display: grid; + grid-template-columns: repeat( 6, 1fr ); + grid-gap: 5px; +} + + +.pipeline_header { + grid-column: 1; + grid-row: 1; +}.pipeline_header { + display: grid; + grid-template-columns: repeat( 2, 1fr ); + grid-template-rows: repeat(5, 3fr); + grid-gap: 5px; +} + +.pipeline_user_icon { + grid-column: 1 ; + grid-row: 4 / 6; +} + +.pipeline_statusIcon { + grid-column: 2 ; + grid-row: 4 / 6; +} img{ + width: 36px; + height: 36px; + border-radius: 50%; +} + +.pipeline_scenario { + font-weight: bold; + grid-column: 1 / 3; + grid-row:1; +} +.pipeline_branch { + grid-column: 1; + grid-row:2; +} +.pipeline_date { + grid-column: 1; + grid-row:3; +} +.pipeline_time { + grid-column: 2; + grid-row:3; +} + +.pipeline_duration { + grid-column: 2; + grid-row: 2; +} + +.pipeline_stages { + grid-column: 2 / 7; + grid-row: 1; +} + +.pipeline_loader { + grid-column: 1 / 7; + grid-row: 1; +} + +.stage { +} +.stage > div { + padding: 1px; + width: 100%; +}.stage { + float:left; + display: table; +} + +.stage_name{ + display: inline-block; + text-align: center; + padding: 2px; +} + +.job{ + display: inline-block; +} > div { +}.job{ + display: grid; + grid-template-columns: repeat(6, 1fr); + grid-gap: 0px; +} + +.job_statusIcon{ + grid-column: 1; + grid-row: 1; +} + +.job_name{ + padding: 2px; + grid-column: 2 / 7; + grid-row: 1; +} + +.statusIcon{ + padding: 2px; +} + + + + + + + + + +.modal-mask { + position: fixed; + z-index: 9998; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, .5); + display: table; + transition: opacity .3s ease; +} + +.modal-wrapper { + display: table-cell; + vertical-align: middle; +} + +.modal-container { + width: 60%; + min-height: 600px;; + margin: 0px auto; + padding: 20px 30px; + background-color: #fff; + border-radius: 2px; + box-shadow: 0 2px 8px rgba(0, 0, 0, .33); + transition: all .3s ease; +} + +.modal-header h3 { + margin-top: 0; + color: #42b983; +} + +.modal-body { + margin: 20px 0; +} + +.modal-default-button { + float: right; +} + +/* + * The following styles are auto-applied to elements with + * transition="modal" when their visibility is toggled + * by Vue.js. + * + * You can easily play with the modal transition by editing + * these styles. + */ + +.modal-enter { + opacity: 0; +} + +.modal-leave-active { + opacity: 0; +} + +.modal-enter .modal-container, +.modal-leave-active .modal-container { + -webkit-transform: scale(1.1); + transform: scale(1.1); +} |