aboutsummaryrefslogtreecommitdiffstats
path: root/chained-ci-vue
diff options
context:
space:
mode:
authorMarek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl>2023-03-01 12:27:28 +0100
committerMarek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl>2023-03-03 13:46:02 +0100
commit70fa03898ee412e30b6b87cf961004bf16ccaef4 (patch)
tree10cca3196bd5db69ee643316365a00f1276dba04 /chained-ci-vue
parent0399d9842c2a5670e4ee21d45343d2ac168eee2d (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/LICENSE201
-rw-r--r--chained-ci-vue/README.md4
-rw-r--r--chained-ci-vue/favicon.pngbin0 -> 5898 bytes
-rw-r--r--chained-ci-vue/index.html238
-rwxr-xr-xchained-ci-vue/init.sh68
-rw-r--r--chained-ci-vue/js/config.js16
-rw-r--r--chained-ci-vue/js/index.js250
-rw-r--r--chained-ci-vue/js/lib.js558
-rw-r--r--chained-ci-vue/js/visibility.LICENSE3
-rw-r--r--chained-ci-vue/js/visibility.core.js189
-rw-r--r--chained-ci-vue/js/visibility.timers.js161
-rw-r--r--chained-ci-vue/logo.svg130
-rw-r--r--chained-ci-vue/style.css219
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
new file mode 100644
index 0000000..6e07930
--- /dev/null
+++ b/chained-ci-vue/favicon.png
Binary files differ
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:&nbsp;</b>{{ timer }}&nbsp;/&nbsp;{{ 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);
+}