aboutsummaryrefslogtreecommitdiffstats
path: root/chained-ci-vue/js
diff options
context:
space:
mode:
Diffstat (limited to 'chained-ci-vue/js')
-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
6 files changed, 1177 insertions, 0 deletions
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);