From 94171c0d3261b6de5bef9747700e42a8b84a1fdd Mon Sep 17 00:00:00 2001 From: azachar Date: Mon, 18 Dec 2017 18:14:18 +0100 Subject: [PATCH 01/31] feat(filtering): Open + Download Job's Trace logs opens: GitLab page for pipelines, pipeline or job downloads: trace log, all failed trace logs editor: support opening log with an ANSI editor that allows custom filtering key bindings: to open GitLab's pipelines page and the job selector --- icons/download.svg | 3 + keymaps/gitlab-integration.json | 6 + lib/gitlab-integration.coffee | 23 +++ lib/gitlab.coffee | 169 ++++++++++++++-- lib/job-selector-view.coffee | 85 ++++++++ lib/status-bar-view.coffee | 52 ++--- package-lock.json | 335 ++++++++++++++++++++++++++++++++ package.json | 5 +- styles/icons.less | 15 ++ 9 files changed, 650 insertions(+), 43 deletions(-) create mode 100644 icons/download.svg create mode 100644 keymaps/gitlab-integration.json create mode 100644 lib/job-selector-view.coffee create mode 100644 package-lock.json diff --git a/icons/download.svg b/icons/download.svg new file mode 100644 index 0000000..641c384 --- /dev/null +++ b/icons/download.svg @@ -0,0 +1,3 @@ + + + diff --git a/keymaps/gitlab-integration.json b/keymaps/gitlab-integration.json new file mode 100644 index 0000000..04c8455 --- /dev/null +++ b/keymaps/gitlab-integration.json @@ -0,0 +1,6 @@ +{ + "atom-workspace": { + "shift-ctrl-alt-g": "gitlab-integration:open-gitlab-ci-cd", + "ctrl-alt-g": "gitlab-integration:open-failed-job-selector" + } +} diff --git a/lib/gitlab-integration.coffee b/lib/gitlab-integration.coffee index c203789..3c01c50 100644 --- a/lib/gitlab-integration.coffee +++ b/lib/gitlab-integration.coffee @@ -12,6 +12,11 @@ class GitlabIntegration description: 'Token to access your Gitlab API' type: 'string' default: '' + artifactReportPath: + title: 'Artifact report path' + description: 'Usefull to open your report such as protractor-screenshoter' + type: 'string' + default: 'storage//index.html' period: title: 'Polling period (ms)' description: 'The interval at which gitlab will be polled' @@ -128,6 +133,24 @@ class GitlabIntegration "You likely forgot to configure your gitlab token", {dismissable: true} ) + if not atom.config.get('gitlab-integration.artifactReportPath') + atom.notifications.addInfo( + "You likely forgot to configure your gitlab artifact report path", + {dismissable: true} + ) + atom.commands.add 'atom-workspace', + 'gitlab-integration:open-gitlab-ci-cd': () => + if @statusBarView?.currentProject isnt "" + @gitlab.openGitlabCICD(@statusBarView.currentProject) + + atom.commands.add 'atom-workspace', + 'gitlab-integration:open-failed-job-selector': () => + if @statusBarView?.currentProject isnt "" + currentStages = @statusBarView?.stages[@statusBarView.currentProject] + failedStages = currentStages?.filter (stage) -> stage.status is 'failed' + if @statusBarView.currentProject and failedStages?.length > 0 + @gitlab.openJobSelector(@statusBarView.currentProject, failedStages[0]) + @handleProjects(atom.project.getDirectories()) @subscriptions.add atom.project.onDidChangePaths (paths) => @handleProjects(paths.map((path) => new Directory(path))) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index 34b8b9b..a9bf6a5 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -1,22 +1,19 @@ fetch = require 'isomorphic-fetch' log = require './log' +shell = require('electron').shell; +JobSelectorView = require './job-selector-view' class GitlabStatus constructor: (@view, @timeout=null, @projects={}, @pending=[], @jobs={}) -> @token = atom.config.get('gitlab-integration.token') + @artifactReportPath = atom.config.get('gitlab-integration.artifactReportPath') @period = atom.config.get('gitlab-integration.period') @updating = {} @watchTimeout = null + @view.setController(@) fetch: (host, q, paging=false) -> - log " -> fetch '#{q}' from '#{host}" - fetch( - "https://#{host}/api/v4/#{q}", { - headers: { - "PRIVATE-TOKEN": @token, - } - } - ).then((res) => + @load(host,q).then((res) => log " <- ", res if res.headers.get('X-Next-Page') if paging @@ -60,6 +57,16 @@ class GitlabStatus res.json() ) + load: (host, q) -> + log " -> fetch '#{q}' from '#{host}" + fetch( + "https://#{host}/api/v4/#{q}", { + headers: { + "PRIVATE-TOKEN": @token, + } + } + ) + watch: (host, projectPath, repos) -> projectPath = projectPath.toLowerCase() if not @projects[projectPath]? and not @updating[projectPath]? @@ -87,6 +94,129 @@ class GitlabStatus @view.unknown(projectPath) ) + printHeader: (job) -> + "[0K Log from branch #{job.ref} | job #{job.name} | # #{job.id} | pipeline #{job.pipeline.id} [0;m" + + loadJob: (host, project, job) -> + atom.notifications.addInfo( + "Downloading build log for job #{job.name} #{job.id}", + {dismissable: true} + ) + @load(host, "projects/#{project.id}/jobs/#{job.id}/trace", false) + .then( (res) -> + return res.text() + ) + .then( (text) -> + return text: text, job: job + ) + .catch((error) -> + atom.notifications.addWarning( + "Unable to load the build log due to #{error}", + {dismissable: true} + ) + console.error "cannot fetch the build log from projects/#{project.id}/jobs/#{job.id}/trace", error + ) + + openLog: (projectPath, job) -> + { host, project, repos } = @projects[projectPath] + @loadJob(host, project, job) + .then( (downloadedLog) => + atom.workspace.open(undefined, { + # split : 'right' + }).then (editor) => + # editor.setFileName("#{job.name}.#{job.ref}.#{job.id}") + editor.setGrammar(atom.grammars.grammarForScopeName('text.ansi')) + editor.insertText @printHeader downloadedLog.job + editor.insertNewline() + editor.insertNewline() + editor.insertText(downloadedLog.text) + ) + .catch((error) -> + console.error "cannot open editor for the build log of #{projectPath} and job #{job.id}", error + ) + + openFailedLogs: (projectPath, jobs) -> + @openLogs(projectPath, jobs.filter (job) -> job.status is 'failed') + + openFailedLogsInGroup: (projectPath, jobs, mainJob) -> + @openFailedLogs(projectPath, jobs.filter (job) -> job.name is mainJob.name) + + openLogs: (projectPath, jobs) -> + { host, project, repos } = @projects[projectPath] + + atom.notifications.addInfo( + "Downloading build logs for #{jobs.length} jobs", + {dismissable: true} + ) + + jobsToLoad = ( + @loadJob(host, project, aJob) for aJob in jobs + ) + + Promise.all(jobsToLoad) + .then (logs) => + logs?.sort (a, b) -> + return -1 if a.job.name < b.job.name + return 1 if a.job.name > b.job.name + + return -1 if a.job.id < b.job.id + return 1 if a.job.id > b.job.id + return 0 + + atom.workspace.open(undefined, { + # split : 'right' + }).then (editor) => + # editor.setFileName("#{job.name}.#{job.ref}.#{job.id}") + editor.setGrammar(atom.grammars.grammarForScopeName('text.ansi')) + for l in logs + editor.insertNewline() + editor.insertText @printHeader l.job + editor.insertNewline() + editor.insertText(l.text) + # atom.notifications.addInfo( + # "Build log #{l.job.name} (#{l.job.id}) downloaded", + # {dismissable: true} + # ) + .catch((error) -> + console.error "cannot open editor for build logs of jobs of #{projectPath}", error + ) + + openReport: (projectPath, job) -> + if not job.artifacts_file + atom.notifications.addWarning( + "No artifacts for job #{job.name}", + {dismissable: true} + ) + return + + path = @artifactReportPath.split('').join(job.name) + + if not path + atom.notifications.addWarning( + "Unknown path for artifact to download for #{job.name}", + {dismissable: true} + ) + return + + { host, project, repos } = @projects[projectPath] + shell.openExternal("https://#{host}/#{encodeURI(projectPath)}/-/jobs/#{job.id}/artifacts/raw/#{path}"); + + openGitlabCICD: (projectPath) -> + { host, project, repos } = @projects[projectPath] + return unless host + shell.openExternal("https://#{host}/#{projectPath}/pipelines"); + + openPipeline: (projectPath, stages) -> + if stages + { host, project, repos } = @projects[projectPath] + shell.openExternal("https://#{host}/#{projectPath}/pipelines/#{stages[0].pipeline}"); + else + @openGitlabCICD(projectPath) + + openJobSelector: (projectPath, stage) -> + selector = new JobSelectorView + selector.initialize(stage.jobs, @ , projectPath) + schedule: -> @timeout = setTimeout @update.bind(@), @period @@ -151,26 +281,27 @@ class GitlabStatus if not stage? stage = name: job.stage - status: 'success' + pipeline: pipeline.id + status: 'created' jobs: [] stages = stages.concat([stage]) stage.jobs = stage.jobs.concat([job]) return stages , []).map((stage) -> Object.assign(stage, { + firstFailedJob: stage.jobs + .filter( (job) -> job.status is 'failed' )?[0] + status: stage.jobs .sort((a, b) -> b.id - a.id) .reduce((status, job) -> - switch - when job.status is 'pending' then 'pending' - when job.status is 'created' then 'created' - when job.status is 'canceled' then 'canceled' - when job.status is 'running' then 'running' - when job.status is 'skipped' then 'skipped' - when job.status is 'failed' and - status is 'success' then 'failed' - else status - , 'success') + switch status + when 'failed' then 'failed' + when 'pending' then 'pending' + when 'manual' then 'manual' + when 'running' then 'running' + else job.status + , 'created') }) )) ).catch((error) => diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee new file mode 100644 index 0000000..07e522e --- /dev/null +++ b/lib/job-selector-view.coffee @@ -0,0 +1,85 @@ +{$, $$, SelectListView} = require 'atom-space-pen-views' + +class JobSelectorView extends SelectListView + initialize: (jobs, controller, projectPath) -> + super + + @controller = controller + @projectPath = projectPath + @addClass('overlay from-top') + + jobs?.sort (a, b) -> + return -1 if a.name < b.name + return 1 if a.name > b.name + return -1 if a.id < b.id + return 1 if a.id > b.id + return 0 + + @setItems jobs + + @panel ?= atom.workspace.addModalPanel(item: this) + @panel.show() + @focusFilterEditor() + @getFilterKey = -> 'name' + # @loadingArea.append $$ @extraContent + @handleEvents + # @loadingArea.show() + + extraContent: -> + @div class: 'input-block-item', => + @div class: 'btn-group', => + @button outlet: 'sortButton', class: 'btn', 'Sort' + @div class: 'btn-group', => + @button outlet: 'allButton', class: 'btn', 'All' + @div class: 'btn-group', => + @button outlet: 'selectedButton', class: 'btn', 'Selected' + + + handleEvents: -> + @sortButton.on 'click', => @sort() + @allButton.on 'click', => @all() + + sort: -> + @setItems @items.sort (a, b) -> + return -1 if a.name < b.name + return 1 if a.name > b.name + return 0 + + all: -> + @cancel() + @controller.openLogs(@projectPath, @items) + + selected: -> + @cancel() + @controller.openLogs(@projectPath, @getSelectedItems) + + + viewForItem: (job) -> + artifactIcon = if job.artifacts_file then "| " else "" + "
  • #{job.name} (#{job.id}) | ♨︎ #{Math.round(job.duration)}s #{artifactIcon} | #{job.finished_at} | #{job.runner.description} | #{job.user.name}
  • " + + confirmed: (job) => + @cancel() + if job.artifacts_file + atom.confirm + message: 'What to open?' + detailedMessage: "Do you want to open the log or the report or both or all logs of the group #{job.name}?" + buttons: + Group: => @controller.openFailedLogsInGroup(@projectPath, @items, job) + Log: => @controller.openLog(@projectPath, job) + Report: => @controller.openReport(@projectPath, job) + Both: => + @controller.openLog(@projectPath, job) + @controller.openReport(@projectPath, job) + else + atom.confirm + message: 'What to open?' + detailedMessage: "Do you want to open the log or all logs of the group #{job.name}?" + buttons: + Group: => @controller.openFailedLogsInGroup(@projectPath, @items, job) + Log: => @controller.openLog(@projectPath, job) + + cancelled: -> + @panel.hide() + +module.exports = JobSelectorView diff --git a/lib/status-bar-view.coffee b/lib/status-bar-view.coffee index a9aaff3..ec6e71f 100644 --- a/lib/status-bar-view.coffee +++ b/lib/status-bar-view.coffee @@ -9,6 +9,10 @@ class StatusBarView extends HTMLElement @stages = {} @statuses = {} @tooltips = [] + @controller = null + + setController: (controller) => + @controller = controller activate: => @displayed = false deactivate: => @@ -59,8 +63,10 @@ class StatusBarView extends HTMLElement @disposeTooltips() status = document.createElement('div') status.classList.add('inline-block') - icon = document.createElement('span') + icon = document.createElement('a') icon.classList.add('icon', 'icon-gitlab') + icon.onclick = (e) => + @controller.openGitlabCICD(project); @tooltips.push atom.tooltips.add icon, { title: "GitLab project #{project}" } @@ -85,10 +91,12 @@ class StatusBarView extends HTMLElement @disposeTooltips() status = document.createElement('div') status.classList.add('inline-block') - icon = document.createElement('span') + icon = document.createElement('a') icon.classList.add('icon', 'icon-gitlab') + icon.onclick = (e) => + @controller.openGitlabCICD(project); @tooltips.push atom.tooltips.add icon, { - title: "GitLab project #{project}" + title: "GitLab project #{project} #{stages[0]?.pipeline} on branch #{stages[0]?.jobs[0]?.ref}" } status.appendChild icon if stages.length is 0 @@ -99,29 +107,29 @@ class StatusBarView extends HTMLElement } status.appendChild e else + icon.onclick = (e) => + @controller.openPipeline(project, stages); + stages.forEach((stage) => - e = document.createElement('span') - switch - when stage.status is 'success' - e.classList.add('icon', 'gitlab-success') - when stage.status is 'failed' - e.classList.add('icon', 'gitlab-failed') - when stage.status is 'running' - e.classList.add('icon', 'gitlab-running') - when stage.status is 'pending' - e.classList.add('icon', 'gitlab-pending') - when stage.status is 'skipped' - e.classList.add('icon', 'gitlab-skipped') - when stage.status is 'canceled' - e.classList.add('icon', 'gitlab-canceled') - when stage.status is 'created' - e.classList.add('icon', 'gitlab-created') - when stage.status is 'manual' - e.classList.add('icon', 'gitlab-manual') + failedJobs = stage.jobs.filter( (job) -> job.status is 'failed' ) + + e = document.createElement('a') + e.classList.add('icon', "gitlab-#{stage.status}") + e.onclick = (e) => + @controller.openJobSelector(project, stage); @tooltips.push atom.tooltips.add e, { - title: "#{stage.name}: #{stage.status}" + title: "#{stage.name}: #{stage.status} | #{failedJobs.length} failed jobs out of #{stage.jobs.length} | Click to individually select a job's log to download." } status.appendChild e + if failedJobs.length > 0 + e = document.createElement('a') + e.classList.add('icon', "gitlab-artifact") + e.onclick = (e) => + @controller.openFailedLogs(project, stage.jobs); + @tooltips.push atom.tooltips.add e, { + title: "Download all failed logs (#{failedJobs.length}) from the stage #{stage.name}" + } + status.appendChild e ) @setchild(status) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..955e21d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,335 @@ +{ + "name": "gitlab-integration", + "version": "0.4.4", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "assertion-error": { + "version": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "atom-space-pen-views": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/atom-space-pen-views/-/atom-space-pen-views-2.2.0.tgz", + "integrity": "sha1-plsskg7QL3JAFPp9Plw9ePv1mZc=", + "requires": { + "fuzzaldrin": "2.1.0", + "space-pen": "5.1.2" + } + }, + "chai": { + "version": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", + "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "dev": true, + "requires": { + "assertion-error": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "deep-eql": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "type-detect": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz" + } + }, + "d": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz", + "integrity": "sha1-2hhMU10Y2O57oqoim5FACfrhEwk=", + "requires": { + "es5-ext": "0.10.37" + } + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "dev": true, + "requires": { + "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + } + }, + "deep-eql": { + "version": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz" + }, + "dependencies": { + "type-detect": { + "version": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } + }, + "deep-equal": { + "version": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "emissary": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/emissary/-/emissary-1.3.3.tgz", + "integrity": "sha1-phjZLWgrIy0xER3DYlpd9mF5lgY=", + "requires": { + "es6-weak-map": "0.1.4", + "mixto": "1.0.0", + "property-accessors": "1.1.3", + "underscore-plus": "1.6.6" + } + }, + "encoding": { + "version": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz" + } + }, + "es5-ext": { + "version": "0.10.37", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.37.tgz", + "integrity": "sha1-DudB0Ui4AGm6J9AgOTdWryV978M=", + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + }, + "dependencies": { + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "0.10.37" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37", + "es6-symbol": "3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37" + } + } + } + }, + "es6-iterator": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-0.1.3.tgz", + "integrity": "sha1-1vWLjE/EE8JJtLqhl2j45NfIlE4=", + "requires": { + "d": "0.1.1", + "es5-ext": "0.10.37", + "es6-symbol": "2.0.1" + } + }, + "es6-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-2.0.1.tgz", + "integrity": "sha1-dhtcZ8/U8dGK+yNPaR1nhoLLO/M=", + "requires": { + "d": "0.1.1", + "es5-ext": "0.10.37" + } + }, + "es6-weak-map": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-0.1.4.tgz", + "integrity": "sha1-cGzvnpmqI2undmwjnIueKG6n0ig=", + "requires": { + "d": "0.1.1", + "es5-ext": "0.10.37", + "es6-iterator": "0.1.3", + "es6-symbol": "2.0.1" + } + }, + "fuzzaldrin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fuzzaldrin/-/fuzzaldrin-2.1.0.tgz", + "integrity": "sha1-kCBMPi/appQbso0WZF1BgGOpDps=" + }, + "git-up": { + "version": "https://registry.npmjs.org/git-up/-/git-up-2.0.9.tgz", + "integrity": "sha1-IZv9J8gtrurYSVvrOG3Bjq5jY20=", + "requires": { + "is-ssh": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.0.tgz", + "parse-url": "https://registry.npmjs.org/parse-url/-/parse-url-1.3.11.tgz" + } + }, + "git-url-parse": { + "version": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-7.0.1.tgz", + "integrity": "sha1-Gj3/xuqp42NN7txY4g2eMxBtPUE=", + "requires": { + "git-up": "https://registry.npmjs.org/git-up/-/git-up-2.0.9.tgz" + } + }, + "grim": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/grim/-/grim-1.5.0.tgz", + "integrity": "sha1-sysI71Z88YUvgXWe2caLDXE5ajI=", + "requires": { + "emissary": "1.3.3" + } + }, + "iconv-lite": { + "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=" + }, + "is-ssh": { + "version": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.0.tgz", + "integrity": "sha1-6+oRaaJhTaOSpjdANmw84EnY3/Y=", + "requires": { + "protocols": "https://registry.npmjs.org/protocols/-/protocols-1.4.6.tgz" + } + }, + "is-stream": { + "version": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "isomorphic-fetch": { + "version": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "whatwg-fetch": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz" + } + }, + "jquery": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.1.4.tgz", + "integrity": "sha1-IoveaYoMYUMdwmMKahVPFYkNIxc=" + }, + "json-stringify-safe": { + "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mixto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mixto/-/mixto-1.0.0.tgz", + "integrity": "sha1-wyDvYbUvKJj1IuF9i7xtUG2EJbY=" + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nock": { + "version": "https://registry.npmjs.org/nock/-/nock-9.1.4.tgz", + "integrity": "sha1-XN2onF7/rtD0SEhvATW/ex578dw=", + "dev": true, + "requires": { + "chai": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "deep-equal": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "propagate": "https://registry.npmjs.org/propagate/-/propagate-0.4.0.tgz", + "qs": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "semver": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz" + } + }, + "node-fetch": { + "version": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha1-mA9vcthSEaU0fGsrwYxbhMPrR+8=", + "requires": { + "encoding": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "is-stream": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" + } + }, + "parse-url": { + "version": "https://registry.npmjs.org/parse-url/-/parse-url-1.3.11.tgz", + "integrity": "sha1-V8FUKKuKiSsfQ4aWRccR0OFEtVQ=", + "requires": { + "is-ssh": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.0.tgz", + "protocols": "https://registry.npmjs.org/protocols/-/protocols-1.4.6.tgz" + } + }, + "propagate": { + "version": "https://registry.npmjs.org/propagate/-/propagate-0.4.0.tgz", + "integrity": "sha1-8/zKCm/gZzanulcpZgaWF8EwtIE=", + "dev": true + }, + "property-accessors": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/property-accessors/-/property-accessors-1.1.3.tgz", + "integrity": "sha1-Hd6EAkYxhlkJ7zBwM2VoDF+SixU=", + "requires": { + "es6-weak-map": "0.1.4", + "mixto": "1.0.0" + } + }, + "protocols": { + "version": "https://registry.npmjs.org/protocols/-/protocols-1.4.6.tgz", + "integrity": "sha1-+LsmPqG1/Xp2BNJri+Ob13Z4v4o=" + }, + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=", + "dev": true + }, + "semver": { + "version": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=", + "dev": true + }, + "space-pen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/space-pen/-/space-pen-5.1.2.tgz", + "integrity": "sha1-Ivu+EOCwROe3pHsCPamdlLWE748=", + "requires": { + "grim": "1.5.0", + "jquery": "2.1.4", + "underscore-plus": "1.6.6" + } + }, + "type-detect": { + "version": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", + "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "dev": true + }, + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" + }, + "underscore-plus": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.6.6.tgz", + "integrity": "sha1-ZezeG9xEGjXYnmUP1w3PE65Dmn0=", + "requires": { + "underscore": "1.6.0" + } + }, + "whatwg-fetch": { + "version": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", + "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" + } + } +} diff --git a/package.json b/package.json index e94b638..48dd61a 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,9 @@ } }, "dependencies": { - "isomorphic-fetch": "^2.2.1", - "git-url-parse": "^7.0.0" + "atom-space-pen-views": "^2.2.0", + "git-url-parse": "^7.0.0", + "isomorphic-fetch": "^2.2.1" }, "devDependencies": { "nock": "^9.0.14" diff --git a/styles/icons.less b/styles/icons.less index f663c7f..7a987e2 100644 --- a/styles/icons.less +++ b/styles/icons.less @@ -20,6 +20,15 @@ border-style: solid; } +.gitlab-avatar { + width: 16px !important; + height: 16px !important; + vertical-align: -4px; + margin: 0; + padding: 0; + .gitlab-round; +} + .gitlab-created:before { .gitlab-icon(created); border-color: #c4c4c4; @@ -69,6 +78,12 @@ .gitlab-round; } +.gitlab-artifact:before { + .gitlab-icon(download); + border-color: #2e2e2e; + fill: #2e2e2e; +} + .gitlab-canceled:before { .gitlab-icon(canceled); border-color: #2e2e2e; From b048c3d2072d37aa9f114cf8482f3e53434b79ab Mon Sep 17 00:00:00 2001 From: azachar Date: Mon, 18 Dec 2017 19:10:38 +0100 Subject: [PATCH 02/31] doc(filtering): how to install additional packages --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 0390bf5..56deecf 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ Track gitlab pipelines state of your project. ![Screenshot](https://user-images.githubusercontent.com/1149069/28337289-7973ebcc-6c05-11e7-844d-c7a1e106317c.png) # Configuration + - This fork can be installed from + ``` + apm install https://github.com/azachar/gitlab-integration + ``` + Please install additional forks to benefit from some extra functionality. - Once installed, fill your Gitlab API token in the package's settings page - If you don't know what they are, please refer to https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html, @@ -21,6 +26,16 @@ Track gitlab pipelines state of your project. - `gitlab-integration` should display pipeline statuses in the status bar if it can correctly determine and reach the gitlab server where your project is hosted like shown above. - *In case any errors occurs, a message should be logged in Atom developer console.* +## Gitlab Build Log Filtering Support +- In order to benefit from extra log filtering to narrow down your error from your failed GitLab job's trace log, you need to install 2 additional packages to your atom. Currently, these packages are in experimental mode and still under the development (basically I forked them to gain quick functionality) +- Please install forks `language-ansi-styles` from `azachar` repo the same applies to `language-log` package. + ``` + apm install https://github.com/azachar/language-log + apm install https://github.com/azachar/language-ansi-styles + ``` + +- In order to open your job's reports directly in your browser instead of downloading them, please set up your GitLab installation or modify your browser by following https://gitlab.com/gitlab-org/gitlab-ce/issues/10982#note_50291868 + # Contributing Reporting issues and pull requests are more than welcome on this project. From cc6ae897d9db8383a6248fbb72eb70c0fc0386c6 Mon Sep 17 00:00:00 2001 From: azachar Date: Mon, 18 Dec 2017 23:29:21 +0100 Subject: [PATCH 03/31] fix(filtering): npe in job selector --- lib/job-selector-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index 07e522e..9c86230 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -56,7 +56,7 @@ class JobSelectorView extends SelectListView viewForItem: (job) -> artifactIcon = if job.artifacts_file then "| " else "" - "
  • #{job.name} (#{job.id}) | ♨︎ #{Math.round(job.duration)}s #{artifactIcon} | #{job.finished_at} | #{job.runner.description} | #{job.user.name}
  • " + "
  • #{job.name} (#{job.id}) | ♨︎ #{Math.round(job.duration)}s #{artifactIcon} | #{job.finished_at} | #{job.runner?.description} | #{job.user?.name}
  • " confirmed: (job) => @cancel() From 7c86b802cac6eabb4a5242800e9aa6b2bfda0ceb Mon Sep 17 00:00:00 2001 From: azachar Date: Mon, 18 Dec 2017 23:26:07 +0100 Subject: [PATCH 04/31] feat(filtering): reload --- keymaps/gitlab-integration.json | 1 + lib/gitlab-integration.coffee | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/keymaps/gitlab-integration.json b/keymaps/gitlab-integration.json index 04c8455..5559504 100644 --- a/keymaps/gitlab-integration.json +++ b/keymaps/gitlab-integration.json @@ -1,6 +1,7 @@ { "atom-workspace": { "shift-ctrl-alt-g": "gitlab-integration:open-gitlab-ci-cd", + "ctrl-alt-cmd-g": "gitlab-integration:reload", "ctrl-alt-g": "gitlab-integration:open-failed-job-selector" } } diff --git a/lib/gitlab-integration.coffee b/lib/gitlab-integration.coffee index 3c01c50..490dc6b 100644 --- a/lib/gitlab-integration.coffee +++ b/lib/gitlab-integration.coffee @@ -151,6 +151,10 @@ class GitlabIntegration if @statusBarView.currentProject and failedStages?.length > 0 @gitlab.openJobSelector(@statusBarView.currentProject, failedStages[0]) + atom.commands.add 'atom-workspace', + 'gitlab-integration:reload': () => + @handleProjects(atom.project.getDirectories()) + @handleProjects(atom.project.getDirectories()) @subscriptions.add atom.project.onDidChangePaths (paths) => @handleProjects(paths.map((path) => new Directory(path))) From 97973676dce2b222703bda4d3d6c6c23b1716d96 Mon Sep 17 00:00:00 2001 From: azachar Date: Fri, 12 Jan 2018 22:22:36 +0100 Subject: [PATCH 05/31] fix(update): update command --- lib/gitlab-integration.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab-integration.coffee b/lib/gitlab-integration.coffee index 490dc6b..629f034 100644 --- a/lib/gitlab-integration.coffee +++ b/lib/gitlab-integration.coffee @@ -153,7 +153,7 @@ class GitlabIntegration atom.commands.add 'atom-workspace', 'gitlab-integration:reload': () => - @handleProjects(atom.project.getDirectories()) + @gitlab.update() @handleProjects(atom.project.getDirectories()) @subscriptions.add atom.project.onDidChangePaths (paths) => From d605b4cb59e81c44a25b5d60897f944b18eb1988 Mon Sep 17 00:00:00 2001 From: azachar Date: Fri, 12 Jan 2018 22:32:39 +0100 Subject: [PATCH 06/31] style(job): nicer job selector --- lib/job-selector-view.coffee | 67 +++++++++++++++++++++--------------- package-lock.json | 5 +++ package.json | 3 +- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index 9c86230..d4bad0f 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -1,6 +1,23 @@ {$, $$, SelectListView} = require 'atom-space-pen-views' - +moment = require 'moment' +# moment.locale('sk') class JobSelectorView extends SelectListView + toHHMMSS: (sec_num) -> + sec_num = Math.round(sec_num) + hours = Math.floor(sec_num / 3600) + minutes = Math.floor((sec_num - (hours * 3600)) / 60) + seconds = sec_num - (hours * 3600) - (minutes * 60) + if hours < 10 + hours = "0"+hours + if minutes < 10 + minutes = "0"+minutes + if seconds < 10 + seconds = "0"+seconds + if hours is "00" + "#{minutes}m #{seconds}s" + else + "#{hours}h #{minutes}m #{seconds}s" + initialize: (jobs, controller, projectPath) -> super @@ -21,42 +38,36 @@ class JobSelectorView extends SelectListView @panel.show() @focusFilterEditor() @getFilterKey = -> 'name' - # @loadingArea.append $$ @extraContent + @loadingArea.append $$ @extraContent @handleEvents # @loadingArea.show() extraContent: -> - @div class: 'input-block-item', => - @div class: 'btn-group', => - @button outlet: 'sortButton', class: 'btn', 'Sort' - @div class: 'btn-group', => - @button outlet: 'allButton', class: 'btn', 'All' - @div class: 'btn-group', => - @button outlet: 'selectedButton', class: 'btn', 'Selected' + @div class: 'block', => + @div class: 'input-block-item', => + @button outlet: 'totalButton', class: 'btn btn-info', 'Total failures' - handleEvents: -> - @sortButton.on 'click', => @sort() - @allButton.on 'click', => @all() - - sort: -> - @setItems @items.sort (a, b) -> - return -1 if a.name < b.name - return 1 if a.name > b.name - return 0 - - all: -> - @cancel() - @controller.openLogs(@projectPath, @items) - - selected: -> - @cancel() - @controller.openLogs(@projectPath, @getSelectedItems) + handleEvents: => + @totalButton.on 'click', => @totalFailures() + totalFailures: -> + @setItems @controller.totalFailed @items viewForItem: (job) -> - artifactIcon = if job.artifacts_file then "| " else "" - "
  • #{job.name} (#{job.id}) | ♨︎ #{Math.round(job.duration)}s #{artifactIcon} | #{job.finished_at} | #{job.runner?.description} | #{job.user?.name}
  • " + artifactIcon = if job.artifacts_file then "icon gitlab-artifact" else "no-icon" + "
  • +
    +
    + #{job.name} + ♨︎ #{@toHHMMSS(job.duration)} + #{job.id} +
    +
    + #{moment(job.finished_at).format('lll')} + #{job.runner?.description} + #{job.user?.name} +
  • " confirmed: (job) => @cancel() diff --git a/package-lock.json b/package-lock.json index 955e21d..a9a2fae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -234,6 +234,11 @@ "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" } }, + "moment": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" + }, "ms": { "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", diff --git a/package.json b/package.json index 48dd61a..7a9fc00 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "dependencies": { "atom-space-pen-views": "^2.2.0", "git-url-parse": "^7.0.0", - "isomorphic-fetch": "^2.2.1" + "isomorphic-fetch": "^2.2.1", + "moment": "^2.20.1" }, "devDependencies": { "nock": "^9.0.14" From fdae0ffcecddb691f412b8a79a21741ac002e26f Mon Sep 17 00:00:00 2001 From: azachar Date: Fri, 12 Jan 2018 13:30:36 +0100 Subject: [PATCH 07/31] feat(pipelines): pipeline selector --- lib/gitlab.coffee | 51 ++++++++++++++++- lib/pipeline-selector-view.coffee | 92 +++++++++++++++++++++++++++++++ lib/status-bar-view.coffee | 6 ++ 3 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 lib/pipeline-selector-view.coffee diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index a9bf6a5..43cc2f3 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -2,6 +2,7 @@ fetch = require 'isomorphic-fetch' log = require './log' shell = require('electron').shell; JobSelectorView = require './job-selector-view' +PipelineSelectorView = require './pipeline-selector-view' class GitlabStatus constructor: (@view, @timeout=null, @projects={}, @pending=[], @jobs={}) -> @@ -217,6 +218,11 @@ class GitlabStatus selector = new JobSelectorView selector.initialize(stage.jobs, @ , projectPath) + openPipelineSelector: (projectPath) -> + selector = new PipelineSelectorView + { host, project, repos } = @projects[projectPath] + selector.initialize(project.pipelines, @ , projectPath) + schedule: -> @timeout = setTimeout @update.bind(@), @period @@ -224,6 +230,12 @@ class GitlabStatus @pending = Object.keys(@projects).slice() @updatePipelines() + updatePipeline: (pipeline, projectPath) -> + { host, project, repos } = @projects[projectPath] + @jobs[project.path_with_namespace] = null + project.userForcedPipeline = pipeline + @updateJobs(host, project, pipeline) + updatePipelines: -> Object.keys(@projects).map( (projectPath) => @@ -241,8 +253,15 @@ class GitlabStatus @fetch(host, "projects/#{project.id}/pipelines#{ref}").then( (pipelines) => log "received pipelines from #{host}/#{project.id}", pipelines + project.pipelines = pipelines; if pipelines.length > 0 - @updateJobs(host, project, pipelines[0]) + if project.userForcedPipeline + currentPipeline = pipelines.filter( (p) => p.id is project.userForcedPipeline.id) + if not currentPipeline or currentPipeline.length is 0 + currentPipeline = pipelines[0] + project.userForcedPipeline = null + @updateJobs(host, project, currentPipeline) + @loadPipelineJobs(host, project, pipeline) for pipeline in pipelines else @onJobs(project, []) ).catch((error) => @@ -292,7 +311,7 @@ class GitlabStatus firstFailedJob: stage.jobs .filter( (job) -> job.status is 'failed' )?[0] - status: stage.jobs + status: stage?.jobs .sort((a, b) -> b.id - a.id) .reduce((status, job) -> switch status @@ -309,6 +328,34 @@ class GitlabStatus @endUpdate(project) ) + alwaysFailed: (items)-> + passedNames = items.filter((job) => job.status is "success").map( (job)=> job.name) + return items.filter((job) => job.status is "failed" and job.name not in passedNames) + + alwaysSuccess: (items)-> + failedNames = items.filter((job) => job.status is "failed").map( (job)=> job.name) + return items.filter((job) => job.status is "success" and job.name not in failedNames) + + statistics: (jobs)-> + if jobs + total = jobs.filter ( (j) => j.status is 'success' or 'failed') + + alwaysSuccess = @alwaysSuccess( jobs ) + success = jobs.filter ( (j) => j.status is 'success') + unstable = success.filter( (j) => j not in alwaysSuccess) + alwaysFailed = @alwaysFailed( jobs ) + + return {alwaysSuccess, unstable, alwaysFailed, total} + else + return {alwaysSuccess:[], unstable:[], alwaysFailed:[], total:[]} + + loadPipelineJobs: (host, project, pipeline) -> + @fetch(host, "projects/#{project.id}/" + "pipelines/#{pipeline.id}/jobs", true) + .then((jobs) -> pipeline.loadedJobs = jobs) + .catch((error) => + console.error "cannot fetch jobs for pipeline ##{pipeline.id} of project #{project.path_with_namespace}", error + ) + onJobs: (project, stages) -> @jobs[project.path_with_namespace] = stages.slice() @endUpdate(project.path_with_namespace) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee new file mode 100644 index 0000000..12eafe7 --- /dev/null +++ b/lib/pipeline-selector-view.coffee @@ -0,0 +1,92 @@ +{$, $$, SelectListView} = require 'atom-space-pen-views' +moment = require 'moment' +moment.locale('sk') + +Array::unique = -> + output = {} + output[@[key]] = @[key] for key in [0...@length] + value for key, value of output + +class PipelineSelectorView extends SelectListView + toHHMMSS: (sec_num) -> + sec_num = Math.round(sec_num) + hours = Math.floor(sec_num / 3600) + minutes = Math.floor((sec_num - (hours * 3600)) / 60) + seconds = sec_num - (hours * 3600) - (minutes * 60) + if hours < 10 + hours = "0"+hours + if minutes < 10 + minutes = "0"+minutes + if seconds < 10 + seconds = "0"+seconds + if hours is "00" + "#{minutes}m #{seconds}s" + else + "#{hours}h #{minutes}m #{seconds}s" + + initialize: (pipelines, controller, projectPath) -> + super + @projectPath = projectPath + @controller = controller + @addClass('overlay from-top') + + pipelines?.sort (a, b) -> + return 1 if a.sha < b.sha + return -1 if a.sha > b.sha + return 1 if a.id < b.id + return -1 if a.id > b.id + return 0 + + @setItems pipelines + + @panel ?= atom.workspace.addModalPanel(item: this) + @panel.show() + @focusFilterEditor() + @getFilterKey = -> 'sha' + + asUniqueNames: (jobs) => + return jobs.map((j) => j.name).unique() + + viewForItem: (pipeline) -> + if pipeline.loadedJobs + {alwaysSuccess, unstable, alwaysFailed, total} = @controller.statistics(pipeline.loadedJobs) + + "
  • +
    +
    + #{pipeline.id} + #{pipeline.sha} +
    +
    + #{@asUniqueNames(alwaysSuccess)} +
    + #{@asUniqueNames(unstable)} +
    +
    + #{@asUniqueNames(alwaysFailed)} +
    + #{total.length} + #{alwaysSuccess.length} + #{unstable.length} + #{alwaysFailed.length} +
  • " + else + "
  • +
    +
    + #{pipeline.id} + #{pipeline.sha} +
    +
    + +
    +
  • " + + confirmed: (pipeline) => + @cancel() + @controller.updatePipeline(pipeline, @projectPath); + + cancelled: -> + @panel.hide() + +module.exports = PipelineSelectorView diff --git a/lib/status-bar-view.coffee b/lib/status-bar-view.coffee index ec6e71f..dbb01ad 100644 --- a/lib/status-bar-view.coffee +++ b/lib/status-bar-view.coffee @@ -110,6 +110,12 @@ class StatusBarView extends HTMLElement icon.onclick = (e) => @controller.openPipeline(project, stages); + pipeline = document.createElement('span') + pipeline.innerHTML = "#{stages[0]?.pipeline}  " + pipeline.onclick = (e) => + @controller.openPipelineSelector(project); + status.appendChild pipeline + stages.forEach((stage) => failedJobs = stage.jobs.filter( (job) -> job.status is 'failed' ) From 1f03e93d2375161bf9f71c41b17ad5d90fb57f7f Mon Sep 17 00:00:00 2001 From: azachar Date: Fri, 12 Jan 2018 23:11:05 +0100 Subject: [PATCH 08/31] fix(view): double initialization --- lib/gitlab.coffee | 6 ++---- lib/job-selector-view.coffee | 9 ++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index 43cc2f3..4f52ea5 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -215,13 +215,11 @@ class GitlabStatus @openGitlabCICD(projectPath) openJobSelector: (projectPath, stage) -> - selector = new JobSelectorView - selector.initialize(stage.jobs, @ , projectPath) + selector = new JobSelectorView(stage.jobs, @ , projectPath) openPipelineSelector: (projectPath) -> - selector = new PipelineSelectorView { host, project, repos } = @projects[projectPath] - selector.initialize(project.pipelines, @ , projectPath) + selector = new PipelineSelectorView(project.pipelines, @ , projectPath) schedule: -> @timeout = setTimeout @update.bind(@), @period diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index d4bad0f..586385a 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -32,7 +32,14 @@ class JobSelectorView extends SelectListView return 1 if a.id > b.id return 0 - @setItems jobs + {alwaysSuccess, unstable, alwaysFailed, total} = @controller.statistics(jobs) + + organized = [] + organized.concat alwaysFailed + organized.concat unstable + organized.concat alwaysSuccess + + @setItems organized @panel ?= atom.workspace.addModalPanel(item: this) @panel.show() From 33ccff9c14295d738544d44b7604525845e99f0d Mon Sep 17 00:00:00 2001 From: azachar Date: Fri, 12 Jan 2018 22:53:11 +0100 Subject: [PATCH 09/31] fix(pipeline): clean up --- lib/pipeline-selector-view.coffee | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index 12eafe7..17051a4 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -8,22 +8,6 @@ Array::unique = -> value for key, value of output class PipelineSelectorView extends SelectListView - toHHMMSS: (sec_num) -> - sec_num = Math.round(sec_num) - hours = Math.floor(sec_num / 3600) - minutes = Math.floor((sec_num - (hours * 3600)) / 60) - seconds = sec_num - (hours * 3600) - (minutes * 60) - if hours < 10 - hours = "0"+hours - if minutes < 10 - minutes = "0"+minutes - if seconds < 10 - seconds = "0"+seconds - if hours is "00" - "#{minutes}m #{seconds}s" - else - "#{hours}h #{minutes}m #{seconds}s" - initialize: (pipelines, controller, projectPath) -> super @projectPath = projectPath From 1ca7491d27d7d50849ddc1293c63009d9ec2c031 Mon Sep 17 00:00:00 2001 From: azachar Date: Fri, 12 Jan 2018 23:22:39 +0100 Subject: [PATCH 10/31] feat(job): show always failed jobs, then success and last unstable --- lib/gitlab.coffee | 4 ++-- lib/job-selector-view.coffee | 38 ++++++++++++++++-------------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index 4f52ea5..b7c848f 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -339,9 +339,9 @@ class GitlabStatus total = jobs.filter ( (j) => j.status is 'success' or 'failed') alwaysSuccess = @alwaysSuccess( jobs ) - success = jobs.filter ( (j) => j.status is 'success') - unstable = success.filter( (j) => j not in alwaysSuccess) + failed = jobs.filter ( (j) => j.status is 'failed') alwaysFailed = @alwaysFailed( jobs ) + unstable = failed.filter( (j) => j not in alwaysFailed) return {alwaysSuccess, unstable, alwaysFailed, total} else diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index 586385a..137bfc5 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -31,13 +31,9 @@ class JobSelectorView extends SelectListView return -1 if a.id < b.id return 1 if a.id > b.id return 0 + {alwaysSuccess, unstable, alwaysFailed, total} = controller.statistics(jobs) - {alwaysSuccess, unstable, alwaysFailed, total} = @controller.statistics(jobs) - - organized = [] - organized.concat alwaysFailed - organized.concat unstable - organized.concat alwaysSuccess + organized = alwaysFailed.concat(alwaysSuccess).concat(unstable) @setItems organized @@ -45,21 +41,21 @@ class JobSelectorView extends SelectListView @panel.show() @focusFilterEditor() @getFilterKey = -> 'name' - @loadingArea.append $$ @extraContent - @handleEvents - # @loadingArea.show() - - extraContent: -> - @div class: 'block', => - @div class: 'input-block-item', => - @button outlet: 'totalButton', class: 'btn btn-info', 'Total failures' - - - handleEvents: => - @totalButton.on 'click', => @totalFailures() - - totalFailures: -> - @setItems @controller.totalFailed @items + # @loadingArea.append $$ @extraContent + # @handleEvents + # @loadingArea.show() + # + # extraContent: -> + # @div class: 'block', => + # @div class: 'input-block-item', => + # @button outlet: 'totalButton', class: 'btn btn-info', 'Total failures' + # + # + # handleEvents: => + # @totalButton.on 'click', => @totalFailures() + # + # totalFailures: -> + # @setItems @controller.totalFailed @items viewForItem: (job) -> artifactIcon = if job.artifacts_file then "icon gitlab-artifact" else "no-icon" From a6d3c2c1d4f646e6910574eee5fd3ab4cdb08a98 Mon Sep 17 00:00:00 2001 From: azachar Date: Fri, 12 Jan 2018 23:25:16 +0100 Subject: [PATCH 11/31] fix(pipeline): clean up --- lib/pipeline-selector-view.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index 17051a4..fad9882 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -1,6 +1,4 @@ {$, $$, SelectListView} = require 'atom-space-pen-views' -moment = require 'moment' -moment.locale('sk') Array::unique = -> output = {} From 2ab1390c4973273f1ed94d51f50975f73231ec10 Mon Sep 17 00:00:00 2001 From: azachar Date: Mon, 15 Jan 2018 12:33:32 +0100 Subject: [PATCH 12/31] feat(progress): color based on success time --- lib/gitlab.coffee | 49 +++++++++++++++++-- lib/job-selector-view.coffee | 80 ++++++++++++++++++------------- lib/pipeline-selector-view.coffee | 48 ++++++++++++++++++- package-lock.json | 5 ++ package.json | 3 +- styles/icons.less | 26 ++++++++++ 6 files changed, 172 insertions(+), 39 deletions(-) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index b7c848f..0de2504 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -57,6 +57,12 @@ class GitlabStatus else res.json() ) + .then( (result) => + if result?.error + throw result + else + return result + ) load: (host, q) -> log " -> fetch '#{q}' from '#{host}" @@ -73,8 +79,8 @@ class GitlabStatus if not @projects[projectPath]? and not @updating[projectPath]? @updating[projectPath] = false @view.loading projectPath, "loading project..." - @fetch(host, "projects?membership=yes", true).then( - (projects) => + @fetch(host, "projects?membership=yes", true) + .then( (projects) => log "received projects from #{host}", projects if projects? project = projects.filter( @@ -90,8 +96,13 @@ class GitlabStatus else @view.unknown(projectPath) ).catch((error) => - @updating[projectPath] = undefined + if error.error_description + atom.notifications.addWarning( + "Gitlab-Integration: #{error.error}: #{error.error_description}", + {dismissable: true} + ) console.error "cannot fetch projects from #{host}", error + @updating[projectPath] = undefined @view.unknown(projectPath) ) @@ -347,9 +358,39 @@ class GitlabStatus else return {alwaysSuccess:[], unstable:[], alwaysFailed:[], total:[]} + toHHMMSS: (sec_num) -> + sec_num = Math.round(sec_num) + hours = Math.floor(sec_num / 3600) + minutes = Math.floor((sec_num - (hours * 3600)) / 60) + seconds = sec_num - (hours * 3600) - (minutes * 60) + if hours < 10 + hours = "0"+hours + if minutes < 10 + minutes = "0"+minutes + if seconds < 10 + seconds = "0"+seconds + if hours is "00" + "#{minutes}m #{seconds}s" + else + "#{hours}h #{minutes}m #{seconds}s" + + toType: (item, percentile) -> + deviation = Math.round(item.duration / percentile * 100) + if deviation < 85 then type = 'success' + if deviation > 115 then type = 'warning' + if deviation > 130 then type = 'error' + return type + loadPipelineJobs: (host, project, pipeline) -> @fetch(host, "projects/#{project.id}/" + "pipelines/#{pipeline.id}/jobs", true) - .then((jobs) -> pipeline.loadedJobs = jobs) + .then((jobs) -> + if jobs?.length > 0 + pipeline.commit = jobs[0].commit + pipeline.duration = jobs.filter( (j) -> j.status is 'success').reduce( ((max, j) -> + Math.max(max, j.duration) + ), 0) + pipeline.loadedJobs = jobs + ) .catch((error) => console.error "cannot fetch jobs for pipeline ##{pipeline.id} of project #{project.path_with_namespace}", error ) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index 137bfc5..47657ce 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -1,26 +1,12 @@ {$, $$, SelectListView} = require 'atom-space-pen-views' +percentile = require('percentile') moment = require 'moment' # moment.locale('sk') class JobSelectorView extends SelectListView - toHHMMSS: (sec_num) -> - sec_num = Math.round(sec_num) - hours = Math.floor(sec_num / 3600) - minutes = Math.floor((sec_num - (hours * 3600)) / 60) - seconds = sec_num - (hours * 3600) - (minutes * 60) - if hours < 10 - hours = "0"+hours - if minutes < 10 - minutes = "0"+minutes - if seconds < 10 - seconds = "0"+seconds - if hours is "00" - "#{minutes}m #{seconds}s" - else - "#{hours}h #{minutes}m #{seconds}s" - initialize: (jobs, controller, projectPath) -> super + @jobs = jobs @controller = controller @projectPath = projectPath @addClass('overlay from-top') @@ -31,6 +17,13 @@ class JobSelectorView extends SelectListView return -1 if a.id < b.id return 1 if a.id > b.id return 0 + + success = jobs.filter( (j) -> j.status is 'success') + @maxDuration = success.reduce( ((max, j) -> + Math.max(max, j.duration) + ), 0 ) + @averageDuration = percentile(50, success, (item) -> item.duration).duration + {alwaysSuccess, unstable, alwaysFailed, total} = controller.statistics(jobs) organized = alwaysFailed.concat(alwaysSuccess).concat(unstable) @@ -41,33 +34,54 @@ class JobSelectorView extends SelectListView @panel.show() @focusFilterEditor() @getFilterKey = -> 'name' - # @loadingArea.append $$ @extraContent - # @handleEvents - # @loadingArea.show() - # - # extraContent: -> - # @div class: 'block', => - # @div class: 'input-block-item', => - # @button outlet: 'totalButton', class: 'btn btn-info', 'Total failures' - # - # - # handleEvents: => - # @totalButton.on 'click', => @totalFailures() - # - # totalFailures: -> - # @setItems @controller.totalFailed @items + @loadingArea.append $$ @extraContent + @handleEvents + @loadingArea.show() + + extraContent: -> + @div class: 'input-block-item', => + @button outlet: 'alwaysFailedButton', class: 'btn btn-error', 'Always Failed' + @button outlet: 'sometimesFailedButton', class: 'btn btn-warning', 'Sometimes Failed' + @button outlet: 'allButton', class: 'btn btn-info', 'All' + + handleEvents: => + @alwaysFailedButton.on 'mouseover', (e) => + e.preventDefault() + @showAlwaysFailedOnly() + @sometimesFailedButton.on 'mouseover', (e) => + e.preventDefault() + @showSometimesFailedButtonOnly() + @allButton.on 'mouseover', (e) => + e.preventDefault() + @showAll() + + showAlwaysFailedOnly: -> + {alwaysSuccess, unstable, alwaysFailed, total} = controller.statistics(jobs) + @setItems alwaysFailed + + showSometimesFailedButtonOnly: -> + {alwaysSuccess, unstable, alwaysFailed, total} = controller.statistics(jobs) + @setItems unstable + + showAll: -> + @setItems @jobs viewForItem: (job) -> + type = @controller.toType(job, @averageDuration) + artifactIcon = if job.artifacts_file then "icon gitlab-artifact" else "no-icon" "
  • #{job.name} - ♨︎ #{@toHHMMSS(job.duration)} + ♨︎ #{@controller.toHHMMSS(job.duration)} #{job.id}
    - #{moment(job.finished_at).format('lll')} +
    + +
    + #{moment(job.created_at).format('lll')} - #{moment(job.finished_at).format('lll')} #{job.runner?.description} #{job.user?.name}
  • " diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index fad9882..dc5ca63 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -1,4 +1,5 @@ {$, $$, SelectListView} = require 'atom-space-pen-views' +percentile = require('percentile') Array::unique = -> output = {} @@ -19,12 +20,49 @@ class PipelineSelectorView extends SelectListView return -1 if a.id > b.id return 0 + success = pipelines.filter( (p) -> p.status is 'success') + @maxDuration = success.reduce( ((max, p) -> + Math.max(max, p.duration) + ), 0 ) + @averageDuration = percentile(50, success, (item) -> item.duration).duration + + allJobs = pipelines.reduce(((all, p) -> + all.concat(p.loadedJobs)), []) + + {@alwaysSuccess, @unstable, @alwaysFailed, @total} = @controller.statistics(allJobs) + @setItems pipelines @panel ?= atom.workspace.addModalPanel(item: this) @panel.show() @focusFilterEditor() @getFilterKey = -> 'sha' + @loadingArea.append $ @extraContent() + @loadingArea.show() + + extraContent: -> + alwaysSuccess = @asUniqueNames(@alwaysSuccess) + unstable = @asUniqueNames(@unstable) + alwaysFailed = @asUniqueNames(@alwaysFailed) + + "
    + 50% ♨︎ #{@controller.toHHMMSS(@averageDuration)} + MAX ♨︎ #{@controller.toHHMMSS(@maxDuration)} +
    +
    + STABLE: #{alwaysSuccess} +
    +
    + UNSTABLE: #{unstable} +
    +
    + ERROR: #{alwaysFailed} +
    +
    + #{alwaysSuccess.length} + #{unstable.length} + #{alwaysFailed.length} +
    " asUniqueNames: (jobs) => return jobs.map((j) => j.name).unique() @@ -33,13 +71,20 @@ class PipelineSelectorView extends SelectListView if pipeline.loadedJobs {alwaysSuccess, unstable, alwaysFailed, total} = @controller.statistics(pipeline.loadedJobs) + type = @controller.toType(pipeline, @averageDuration) + "
  • #{pipeline.id} - #{pipeline.sha} + #{pipeline.ref} / #{pipeline.sha?.substring(0,5)} + #{pipeline.commit?.message}
    +
    + + ♨︎ #{@controller.toHHMMSS(pipeline.duration)} +
    #{@asUniqueNames(alwaysSuccess)}
    #{@asUniqueNames(unstable)} @@ -58,6 +103,7 @@ class PipelineSelectorView extends SelectListView
    #{pipeline.id} #{pipeline.sha} + #{pipeline.commit?.message}
    diff --git a/package-lock.json b/package-lock.json index a9a2fae..d83303e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -276,6 +276,11 @@ "protocols": "https://registry.npmjs.org/protocols/-/protocols-1.4.6.tgz" } }, + "percentile": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/percentile/-/percentile-1.2.0.tgz", + "integrity": "sha1-+jsFwf/TVbNSKFKYNOX6N/C9Rl0=" + }, "propagate": { "version": "https://registry.npmjs.org/propagate/-/propagate-0.4.0.tgz", "integrity": "sha1-8/zKCm/gZzanulcpZgaWF8EwtIE=", diff --git a/package.json b/package.json index 7a9fc00..bcb3196 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "atom-space-pen-views": "^2.2.0", "git-url-parse": "^7.0.0", "isomorphic-fetch": "^2.2.1", - "moment": "^2.20.1" + "moment": "^2.20.1", + "percentile": "^1.2.0" }, "devDependencies": { "nock": "^9.0.14" diff --git a/styles/icons.less b/styles/icons.less index 7a987e2..35da178 100644 --- a/styles/icons.less +++ b/styles/icons.less @@ -112,3 +112,29 @@ width: 12px !important; height: 16px; } + +.progress-variant(@type) { + @text-color-name: "text-color-@{type}"; + + &::-webkit-progress-value { + background-color: @@text-color-name; + } +} + +progress { + &.progress-info { + .progress-variant(info); + } + + &.progress-success { + .progress-variant(success); + } + + &.progress-warning { + .progress-variant(warning); + } + + &.progress-error { + .progress-variant(error); + } +} From 50a4c3acb3e7f6be2749511c2361350dc55ec8db Mon Sep 17 00:00:00 2001 From: azachar Date: Tue, 16 Jan 2018 00:23:30 +0100 Subject: [PATCH 13/31] feat(job): selector filtering --- lib/gitlab.coffee | 2 +- lib/job-selector-view.coffee | 90 ++++++++++++++++++------------------ 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index 0de2504..d624a56 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -346,7 +346,7 @@ class GitlabStatus return items.filter((job) => job.status is "success" and job.name not in failedNames) statistics: (jobs)-> - if jobs + if jobs?.filter total = jobs.filter ( (j) => j.status is 'success' or 'failed') alwaysSuccess = @alwaysSuccess( jobs ) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index 47657ce..a59778f 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -1,5 +1,5 @@ {$, $$, SelectListView} = require 'atom-space-pen-views' -percentile = require('percentile') +percentile = require 'percentile' moment = require 'moment' # moment.locale('sk') class JobSelectorView extends SelectListView @@ -9,62 +9,62 @@ class JobSelectorView extends SelectListView @jobs = jobs @controller = controller @projectPath = projectPath + {@alwaysSuccess, @unstable, @alwaysFailed, @total} = @controller.statistics(@jobs) @addClass('overlay from-top') + @setItems jobs + @panel ?= atom.workspace.addModalPanel(item: this) + @focusFilterEditor() + $$(@extraContent(@)).insertBefore(@list) + @handleEvents() + @panel.show() - jobs?.sort (a, b) -> - return -1 if a.name < b.name - return 1 if a.name > b.name - return -1 if a.id < b.id - return 1 if a.id > b.id - return 0 + getFilterKey: -> 'name' - success = jobs.filter( (j) -> j.status is 'success') - @maxDuration = success.reduce( ((max, j) -> - Math.max(max, j.duration) - ), 0 ) - @averageDuration = percentile(50, success, (item) -> item.duration).duration + extraContent: (thiz) -> + return -> + @div class: 'inline-block', => + @button outlet: 'allButton', class: 'btn btn-info', ' All', => + @span class: 'badge badge-small', thiz.jobs?.length + @div class: 'btn-group', => + @button outlet: 'alwaysSuccessButton', class: 'btn btn-success', ' Always Success', => + @span class: 'badge badge-small', thiz.alwaysSuccess?.length + @button outlet: 'sometimesFailedButton', class: 'btn btn-warning', ' Sometimes Failed', => + @span class: 'badge badge-small', thiz.unstable?.length + @button outlet: 'alwaysFailedButton', class: 'btn btn-error', ' Always Failed', => + @span class: 'badge badge-small', thiz.alwaysFailed?.length - {alwaysSuccess, unstable, alwaysFailed, total} = controller.statistics(jobs) + handleEvents: -> + @wireOutlets(@) - organized = alwaysFailed.concat(alwaysSuccess).concat(unstable) + @alwaysSuccessButton.on 'mouseover', (e) => + @setItems @alwaysSuccess - @setItems organized + @sometimesFailedButton.on 'mouseover', (e) => + @setItems @unstable - @panel ?= atom.workspace.addModalPanel(item: this) - @panel.show() - @focusFilterEditor() - @getFilterKey = -> 'name' - @loadingArea.append $$ @extraContent - @handleEvents - @loadingArea.show() - - extraContent: -> - @div class: 'input-block-item', => - @button outlet: 'alwaysFailedButton', class: 'btn btn-error', 'Always Failed' - @button outlet: 'sometimesFailedButton', class: 'btn btn-warning', 'Sometimes Failed' - @button outlet: 'allButton', class: 'btn btn-info', 'All' - - handleEvents: => @alwaysFailedButton.on 'mouseover', (e) => - e.preventDefault() - @showAlwaysFailedOnly() - @sometimesFailedButton.on 'mouseover', (e) => - e.preventDefault() - @showSometimesFailedButtonOnly() + @setItems @alwaysFailed + @allButton.on 'mouseover', (e) => - e.preventDefault() - @showAll() + @setItems @jobs - showAlwaysFailedOnly: -> - {alwaysSuccess, unstable, alwaysFailed, total} = controller.statistics(jobs) - @setItems alwaysFailed - showSometimesFailedButtonOnly: -> - {alwaysSuccess, unstable, alwaysFailed, total} = controller.statistics(jobs) - @setItems unstable + populateList: () -> + @items?.sort (a, b) -> + return -1 if a.name < b.name + return 1 if a.name > b.name + return -1 if a.id < b.id + return 1 if a.id > b.id + return 0 + + @maxDuration = @items?.reduce( ((max, j) -> + Math.max(max, j.duration) + ), 0 ) + + if @items?.length > 0 + @averageDuration = percentile(50, @items, (item) -> item.duration).duration - showAll: -> - @setItems @jobs + super viewForItem: (job) -> type = @controller.toType(job, @averageDuration) From a2daed5d783ae27e9628e9b15966c2084d6f4353 Mon Sep 17 00:00:00 2001 From: azachar Date: Tue, 16 Jan 2018 11:08:18 +0100 Subject: [PATCH 14/31] feat(job): commit message header --- lib/job-selector-view.coffee | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index a59778f..3802f96 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -14,24 +14,32 @@ class JobSelectorView extends SelectListView @setItems jobs @panel ?= atom.workspace.addModalPanel(item: this) @focusFilterEditor() - $$(@extraContent(@)).insertBefore(@list) + $$(@extraContent(@)).insertBefore(@error) @handleEvents() @panel.show() getFilterKey: -> 'name' extraContent: (thiz) -> + if thiz.jobs?.length > 0 + commit = thiz.jobs[0].commit + ref = thiz.jobs[0].ref return -> - @div class: 'inline-block', => - @button outlet: 'allButton', class: 'btn btn-info', ' All', => - @span class: 'badge badge-small', thiz.jobs?.length - @div class: 'btn-group', => - @button outlet: 'alwaysSuccessButton', class: 'btn btn-success', ' Always Success', => - @span class: 'badge badge-small', thiz.alwaysSuccess?.length - @button outlet: 'sometimesFailedButton', class: 'btn btn-warning', ' Sometimes Failed', => - @span class: 'badge badge-small', thiz.unstable?.length - @button outlet: 'alwaysFailedButton', class: 'btn btn-error', ' Always Failed', => - @span class: 'badge badge-small', thiz.alwaysFailed?.length + @div class: 'block', => + @div class: 'block', => + @button outlet: 'allButton', class: 'btn btn-info', ' All', => + @span class: 'badge badge-small', thiz.jobs?.length + @div class: 'btn-group', => + @button outlet: 'alwaysSuccessButton', class: 'btn btn-success', ' Always Success', => + @span class: 'badge badge-small', thiz.alwaysSuccess?.length + @button outlet: 'sometimesFailedButton', class: 'btn btn-warning', ' Sometimes Failed', => + @span class: 'badge badge-small', thiz.unstable?.length + @button outlet: 'alwaysFailedButton', class: 'btn btn-error', ' Always Failed', => + @span class: 'badge badge-small', thiz.alwaysFailed?.length + @div class: 'block', => + @span class: 'icon icon-git-commit', commit?.message + @span class: 'text-muted', " #{ref} / #{commit?.short_id}" + @span class: 'icon icon-clock', moment(commit?.created_at).format('lll') handleEvents: -> @wireOutlets(@) From a487d1a4889722e2ffd610de55f529fe1adb7c18 Mon Sep 17 00:00:00 2001 From: azachar Date: Tue, 16 Jan 2018 11:07:23 +0100 Subject: [PATCH 15/31] feat(pipeline): sort actions fix(pipeline): npe --- lib/gitlab.coffee | 12 ++- lib/pipeline-selector-view.coffee | 141 ++++++++++++++++++++---------- 2 files changed, 102 insertions(+), 51 deletions(-) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index d624a56..b72d370 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -346,7 +346,7 @@ class GitlabStatus return items.filter((job) => job.status is "success" and job.name not in failedNames) statistics: (jobs)-> - if jobs?.filter + if jobs?.length total = jobs.filter ( (j) => j.status is 'success' or 'failed') alwaysSuccess = @alwaysSuccess( jobs ) @@ -386,9 +386,13 @@ class GitlabStatus .then((jobs) -> if jobs?.length > 0 pipeline.commit = jobs[0].commit - pipeline.duration = jobs.filter( (j) -> j.status is 'success').reduce( ((max, j) -> - Math.max(max, j.duration) - ), 0) + pipeline.created_at = jobs[0].created_at + pipeline.durationSuccess = jobs.filter( (j) -> j.status is 'success').reduce( ((max, j) -> + Math.max(max, j.duration) + ), 0) + pipeline.duration = jobs.reduce( ((max, j) -> + Math.max(max, j.duration) + ), 0) pipeline.loadedJobs = jobs ) .catch((error) => diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index dc5ca63..dfb86f5 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -1,5 +1,6 @@ {$, $$, SelectListView} = require 'atom-space-pen-views' -percentile = require('percentile') +percentile = require 'percentile' +moment = require 'moment' Array::unique = -> output = {} @@ -9,60 +10,102 @@ Array::unique = -> class PipelineSelectorView extends SelectListView initialize: (pipelines, controller, projectPath) -> super - @projectPath = projectPath + + @pipelines = pipelines @controller = controller + @projectPath = projectPath + @addClass('overlay from-top') + @setItems pipelines + @panel ?= atom.workspace.addModalPanel(item: this) + @focusFilterEditor() + $$(@extraContent(@)).insertBefore(@error) + @handleEvents() + @panel.show() - pipelines?.sort (a, b) -> - return 1 if a.sha < b.sha - return -1 if a.sha > b.sha - return 1 if a.id < b.id - return -1 if a.id > b.id - return 0 + getFilterKey: -> 'name' - success = pipelines.filter( (p) -> p.status is 'success') - @maxDuration = success.reduce( ((max, p) -> + extraContent: (thiz) -> + return -> + alwaysSuccess = thiz.asUniqueNames(thiz.alwaysSuccess) + unstable = thiz.asUniqueNames(thiz.unstable) + alwaysFailed = thiz.asUniqueNames(thiz.alwaysFailed) + + @div class: 'block', => + @div class: 'block', => + @div class: 'btn-group', => + @button outlet: 'sortById', class: 'btn', ' Sort by id' + @button outlet: 'sortBySha', class: 'btn', ' Sort by sha' + @button outlet: 'sortByDate', class: 'btn', ' Sort by date' + + @div class: 'block', => + @raw " +
    + All 50% ♨︎ #{thiz.controller.toHHMMSS(thiz.averageDuration)} + All MAX ♨︎ #{thiz.controller.toHHMMSS(thiz.maxDuration)} +
    +
    + Success 50% ♨︎ #{thiz.controller.toHHMMSS(thiz.averageDurationSuccess)} + Sucess MAX ♨︎ #{thiz.controller.toHHMMSS(thiz.maxDurationSuccess)} +
    +
    + STABLE: #{alwaysSuccess} +
    +
    + UNSTABLE: #{unstable} +
    +
    + ERROR: #{alwaysFailed} +
    +
    + #{alwaysSuccess.length} + #{unstable.length} + #{alwaysFailed.length} +
    " + + handleEvents: -> + @wireOutlets(@) + + @sortById.on 'mouseover', (e) => + @setItems @items?.sort (a, b) -> + Number(a.id) - Number(b.id) + + @sortBySha.on 'mouseover', (e) => + @setItems @items?.sort (a, b) -> + return -1 if a.sha < b.sha + return 1 if a.sha > b.sha + return 0 + + @sortByDate.on 'mouseover', (e) => + @setItems @items?.sort (a, b) -> + if a.created and b.created_at + moment(a.created_at).diff(moment(b.created_at)) + else + Number(a.id) - Number(b.id) + + populateList: () -> + @maxDuration = @items?.reduce( ((max, p) -> Math.max(max, p.duration) ), 0 ) - @averageDuration = percentile(50, success, (item) -> item.duration).duration - - allJobs = pipelines.reduce(((all, p) -> - all.concat(p.loadedJobs)), []) + @averageDuration = percentile(50, @items, (item) -> item.duration).duration + + success = @items.filter( (p) -> p.status is 'success') + if success?.length > 0 + @maxDurationSuccess = success?.reduce( ((max, p) -> + Math.max(max, p.durationSuccess) + ), 0 ) + @averageDurationSuccess = percentile(50, success, (item) -> item.durationSuccess).durationSuccess + + allJobs = @items.reduce(((all, p) -> + if p.loadedJobs?.length > 0 + all.concat(p.loadedJobs) + else + all + ), []) {@alwaysSuccess, @unstable, @alwaysFailed, @total} = @controller.statistics(allJobs) - @setItems pipelines - - @panel ?= atom.workspace.addModalPanel(item: this) - @panel.show() - @focusFilterEditor() - @getFilterKey = -> 'sha' - @loadingArea.append $ @extraContent() - @loadingArea.show() - - extraContent: -> - alwaysSuccess = @asUniqueNames(@alwaysSuccess) - unstable = @asUniqueNames(@unstable) - alwaysFailed = @asUniqueNames(@alwaysFailed) - - "
    - 50% ♨︎ #{@controller.toHHMMSS(@averageDuration)} - MAX ♨︎ #{@controller.toHHMMSS(@maxDuration)} -
    -
    - STABLE: #{alwaysSuccess} -
    -
    - UNSTABLE: #{unstable} -
    -
    - ERROR: #{alwaysFailed} -
    -
    - #{alwaysSuccess.length} - #{unstable.length} - #{alwaysFailed.length} -
    " + super asUniqueNames: (jobs) => return jobs.map((j) => j.name).unique() @@ -78,9 +121,12 @@ class PipelineSelectorView extends SelectListView
    #{pipeline.id} #{pipeline.ref} / #{pipeline.sha?.substring(0,5)} - #{pipeline.commit?.message} + #{moment(pipeline.created_at).format('lll')}
    +
    + #{pipeline.commit?.message} +
    ♨︎ #{@controller.toHHMMSS(pipeline.duration)} @@ -96,6 +142,7 @@ class PipelineSelectorView extends SelectListView #{alwaysSuccess.length} #{unstable.length} #{alwaysFailed.length} + #{alwaysFailed.length}
  • " else "
  • From 1f58ab02f85397ad9f182ca3daf6eb54cf8f0580 Mon Sep 17 00:00:00 2001 From: azachar Date: Tue, 16 Jan 2018 12:48:53 +0100 Subject: [PATCH 16/31] style(selectors): commit title instead of message --- lib/job-selector-view.coffee | 2 +- lib/pipeline-selector-view.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index 3802f96..6d0c987 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -37,7 +37,7 @@ class JobSelectorView extends SelectListView @button outlet: 'alwaysFailedButton', class: 'btn btn-error', ' Always Failed', => @span class: 'badge badge-small', thiz.alwaysFailed?.length @div class: 'block', => - @span class: 'icon icon-git-commit', commit?.message + @span class: 'icon icon-git-commit', commit?.title @span class: 'text-muted', " #{ref} / #{commit?.short_id}" @span class: 'icon icon-clock', moment(commit?.created_at).format('lll') diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index dfb86f5..10996f6 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -125,7 +125,7 @@ class PipelineSelectorView extends SelectListView
    - #{pipeline.commit?.message} + #{pipeline.commit?.title}
    From b07cb917d21eab9e384af19f03e7c499af4001c0 Mon Sep 17 00:00:00 2001 From: azachar Date: Wed, 17 Jan 2018 10:09:07 +0100 Subject: [PATCH 17/31] fix(pipeline): search --- lib/gitlab.coffee | 1 + lib/pipeline-selector-view.coffee | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index b72d370..4743f4c 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -387,6 +387,7 @@ class GitlabStatus if jobs?.length > 0 pipeline.commit = jobs[0].commit pipeline.created_at = jobs[0].created_at + pipeline.search = "id#{pipeline.id} ref#{pipeline.ref} sha#{pipeline.sha?.substring(0,5)}" pipeline.durationSuccess = jobs.filter( (j) -> j.status is 'success').reduce( ((max, j) -> Math.max(max, j.duration) ), 0) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index 10996f6..0610b2b 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -23,7 +23,7 @@ class PipelineSelectorView extends SelectListView @handleEvents() @panel.show() - getFilterKey: -> 'name' + getFilterKey: -> 'search' extraContent: (thiz) -> return -> @@ -68,7 +68,7 @@ class PipelineSelectorView extends SelectListView @sortById.on 'mouseover', (e) => @setItems @items?.sort (a, b) -> - Number(a.id) - Number(b.id) + return a.id - b.id @sortBySha.on 'mouseover', (e) => @setItems @items?.sort (a, b) -> @@ -79,9 +79,9 @@ class PipelineSelectorView extends SelectListView @sortByDate.on 'mouseover', (e) => @setItems @items?.sort (a, b) -> if a.created and b.created_at - moment(a.created_at).diff(moment(b.created_at)) + return moment(a.created_at).diff(moment(b.created_at)) else - Number(a.id) - Number(b.id) + return a.id - b.id populateList: () -> @maxDuration = @items?.reduce( ((max, p) -> From 5a1fe7f12c98f573ce6152cd3c81dd102028acc1 Mon Sep 17 00:00:00 2001 From: azachar Date: Wed, 17 Jan 2018 10:43:31 +0100 Subject: [PATCH 18/31] fix(pipeline): sorting --- lib/pipeline-selector-view.coffee | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index 0610b2b..4db4b0b 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -17,6 +17,7 @@ class PipelineSelectorView extends SelectListView @addClass('overlay from-top') @setItems pipelines + @calculate() @panel ?= atom.workspace.addModalPanel(item: this) @focusFilterEditor() $$(@extraContent(@)).insertBefore(@error) @@ -32,12 +33,6 @@ class PipelineSelectorView extends SelectListView alwaysFailed = thiz.asUniqueNames(thiz.alwaysFailed) @div class: 'block', => - @div class: 'block', => - @div class: 'btn-group', => - @button outlet: 'sortById', class: 'btn', ' Sort by id' - @button outlet: 'sortBySha', class: 'btn', ' Sort by sha' - @button outlet: 'sortByDate', class: 'btn', ' Sort by date' - @div class: 'block', => @raw "
    @@ -62,28 +57,33 @@ class PipelineSelectorView extends SelectListView #{unstable.length} #{alwaysFailed.length}
    " + @div class: 'block', => + @div class: 'btn-group', => + @button outlet: 'sortById', class: 'btn', ' Sort by id' + @button outlet: 'sortBySha', class: 'btn', ' Sort by sha' + @button outlet: 'sortByDate', class: 'btn', ' Sort by date' handleEvents: -> @wireOutlets(@) @sortById.on 'mouseover', (e) => - @setItems @items?.sort (a, b) -> + @setItems @items.sort (a, b) -> return a.id - b.id @sortBySha.on 'mouseover', (e) => - @setItems @items?.sort (a, b) -> + @setItems @items.sort (a, b) -> return -1 if a.sha < b.sha return 1 if a.sha > b.sha return 0 @sortByDate.on 'mouseover', (e) => - @setItems @items?.sort (a, b) -> - if a.created and b.created_at - return moment(a.created_at).diff(moment(b.created_at)) + @setItems @items.sort (a, b) -> + if a.created_at and b.created_at + return moment(b.created_at).diff(moment(a.created_at)) else - return a.id - b.id + return 0 - populateList: () -> + calculate: () -> @maxDuration = @items?.reduce( ((max, p) -> Math.max(max, p.duration) ), 0 ) @@ -105,8 +105,6 @@ class PipelineSelectorView extends SelectListView {@alwaysSuccess, @unstable, @alwaysFailed, @total} = @controller.statistics(allJobs) - super - asUniqueNames: (jobs) => return jobs.map((j) => j.name).unique() From 20bcd6af8e35cc15b8815e89dc842aa13cce3367 Mon Sep 17 00:00:00 2001 From: azachar Date: Wed, 17 Jan 2018 10:52:16 +0100 Subject: [PATCH 19/31] feat(job): selector jobs sorting --- lib/job-selector-view.coffee | 46 +++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index 6d0c987..9726702 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -12,6 +12,7 @@ class JobSelectorView extends SelectListView {@alwaysSuccess, @unstable, @alwaysFailed, @total} = @controller.statistics(@jobs) @addClass('overlay from-top') @setItems jobs + @calculate() @panel ?= atom.workspace.addModalPanel(item: this) @focusFilterEditor() $$(@extraContent(@)).insertBefore(@error) @@ -38,33 +39,52 @@ class JobSelectorView extends SelectListView @span class: 'badge badge-small', thiz.alwaysFailed?.length @div class: 'block', => @span class: 'icon icon-git-commit', commit?.title - @span class: 'text-muted', " #{ref} / #{commit?.short_id}" - @span class: 'icon icon-clock', moment(commit?.created_at).format('lll') + @span class: 'icon icon-git-branch text-muted pull-right', " #{ref} / #{commit?.short_id}" + @div class: 'block', => + @span class: 'icon icon-clock text-center', moment(commit?.created_at).format('lll') + @div class: 'block', => + @div class: 'btn-group', => + @button outlet: 'sortById', class: 'btn', ' Sort by id' + @button outlet: 'sortByName', class: 'btn', ' Sort by name' + @button outlet: 'sortByDate', class: 'btn', ' Sort by date' handleEvents: -> @wireOutlets(@) @alwaysSuccessButton.on 'mouseover', (e) => @setItems @alwaysSuccess + @calculate() @sometimesFailedButton.on 'mouseover', (e) => @setItems @unstable + @calculate() @alwaysFailedButton.on 'mouseover', (e) => @setItems @alwaysFailed + @calculate() @allButton.on 'mouseover', (e) => @setItems @jobs - - - populateList: () -> - @items?.sort (a, b) -> - return -1 if a.name < b.name - return 1 if a.name > b.name - return -1 if a.id < b.id - return 1 if a.id > b.id - return 0 - + @calculate() + + @sortById.on 'mouseover', (e) => + @setItems @items.sort (a, b) -> + return a.id - b.id + + @sortByName.on 'mouseover', (e) => + @setItems @items.sort (a, b) -> + return -1 if a.name < b.name + return 1 if a.name > b.name + return a.id - b.id + + @sortByDate.on 'mouseover', (e) => + @setItems @items.sort (a, b) -> + if a.created_at and b.created_at + return moment(b.created_at).diff(moment(a.created_at)) + else + return 0 + + calculate: () -> @maxDuration = @items?.reduce( ((max, j) -> Math.max(max, j.duration) ), 0 ) @@ -72,8 +92,6 @@ class JobSelectorView extends SelectListView if @items?.length > 0 @averageDuration = percentile(50, @items, (item) -> item.duration).duration - super - viewForItem: (job) -> type = @controller.toType(job, @averageDuration) From 8a2eecb89138bf3edd8d550f853dd78b02b0ce5e Mon Sep 17 00:00:00 2001 From: azachar Date: Wed, 17 Jan 2018 11:05:04 +0100 Subject: [PATCH 20/31] style(pipeline): sha info --- lib/pipeline-selector-view.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index 4db4b0b..c47bf45 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -115,10 +115,13 @@ class PipelineSelectorView extends SelectListView type = @controller.toType(pipeline, @averageDuration) "
  • -
    +
    #{pipeline.id} - #{pipeline.ref} / #{pipeline.sha?.substring(0,5)} + + #{pipeline.ref} + #{pipeline.commit.short_id} + #{moment(pipeline.created_at).format('lll')}
    From ceca9783b7e8e1de0c9f479c839f5225581800ee Mon Sep 17 00:00:00 2001 From: azachar Date: Thu, 18 Jan 2018 11:25:57 +0100 Subject: [PATCH 21/31] feat(pipeline): abs elapsed time --- lib/gitlab.coffee | 4 ++++ lib/pipeline-selector-view.coffee | 31 +++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index 4743f4c..a8a49f1 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -384,9 +384,13 @@ class GitlabStatus loadPipelineJobs: (host, project, pipeline) -> @fetch(host, "projects/#{project.id}/" + "pipelines/#{pipeline.id}/jobs", true) .then((jobs) -> + pipeline.durationSuccess = 0 + pipeline.duration = 0 + if jobs?.length > 0 pipeline.commit = jobs[0].commit pipeline.created_at = jobs[0].created_at + pipeline.finished_at = jobs[jobs?.length-1].finished_at pipeline.search = "id#{pipeline.id} ref#{pipeline.ref} sha#{pipeline.sha?.substring(0,5)}" pipeline.durationSuccess = jobs.filter( (j) -> j.status is 'success').reduce( ((max, j) -> Math.max(max, j.duration) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index c47bf45..f5ad02b 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -33,17 +33,25 @@ class PipelineSelectorView extends SelectListView alwaysFailed = thiz.asUniqueNames(thiz.alwaysFailed) @div class: 'block', => + @div class: 'block', => + @span class: 'icon icon-git-branch', " #{thiz.branch}" + @div class: 'block', => @raw "
    All 50% ♨︎ #{thiz.controller.toHHMMSS(thiz.averageDuration)} All MAX ♨︎ #{thiz.controller.toHHMMSS(thiz.maxDuration)}
    -
    - Success 50% ♨︎ #{thiz.controller.toHHMMSS(thiz.averageDurationSuccess)} - Sucess MAX ♨︎ #{thiz.controller.toHHMMSS(thiz.maxDurationSuccess)} -
    -
    + " + + if thiz.maxDurationSuccess + @raw "
    + Success 50% ♨︎ #{thiz.controller.toHHMMSS(thiz.averageDurationSuccess)} + Sucess MAX ♨︎ #{thiz.controller.toHHMMSS(thiz.maxDurationSuccess)} +
    + " + + @raw "
    STABLE: #{alwaysSuccess}
    @@ -57,6 +65,7 @@ class PipelineSelectorView extends SelectListView #{unstable.length} #{alwaysFailed.length}
    " + @div class: 'block', => @div class: 'btn-group', => @button outlet: 'sortById', class: 'btn', ' Sort by id' @@ -84,6 +93,9 @@ class PipelineSelectorView extends SelectListView return 0 calculate: () -> + if @items?.length > 0 + @branch = @items[0].ref + @maxDuration = @items?.reduce( ((max, p) -> Math.max(max, p.duration) ), 0 ) @@ -109,6 +121,8 @@ class PipelineSelectorView extends SelectListView return jobs.map((j) => j.name).unique() viewForItem: (pipeline) -> + pipeline.elapsed = moment(pipeline.finished_at).diff(moment(pipeline.created_at), 'seconds') + if pipeline.loadedJobs {alwaysSuccess, unstable, alwaysFailed, total} = @controller.statistics(pipeline.loadedJobs) @@ -118,11 +132,12 @@ class PipelineSelectorView extends SelectListView
    #{pipeline.id} - - #{pipeline.ref} + #{moment(pipeline.created_at).format('lll')} + + ABS ♨︎ #{@controller.toHHMMSS(pipeline.elapsed)} +   #{pipeline.commit.short_id} - #{moment(pipeline.created_at).format('lll')}
    From 0989027bc95e2097075cfecfe12fa776e543275e Mon Sep 17 00:00:00 2001 From: azachar Date: Thu, 18 Jan 2018 11:19:17 +0100 Subject: [PATCH 22/31] fix(pipeline): skipped status --- lib/pipeline-selector-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index f5ad02b..e34b7ec 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -123,7 +123,7 @@ class PipelineSelectorView extends SelectListView viewForItem: (pipeline) -> pipeline.elapsed = moment(pipeline.finished_at).diff(moment(pipeline.created_at), 'seconds') - if pipeline.loadedJobs + if pipeline.loadedJobs?.length > 0 {alwaysSuccess, unstable, alwaysFailed, total} = @controller.statistics(pipeline.loadedJobs) type = @controller.toType(pipeline, @averageDuration) From abc769b249d0532a58c98981ddbfbc7dbf480e37 Mon Sep 17 00:00:00 2001 From: azachar Date: Thu, 18 Jan 2018 11:08:42 +0100 Subject: [PATCH 23/31] refactor(avatar): moved from job to pipeline --- lib/gitlab.coffee | 1 + lib/job-selector-view.coffee | 7 +++++-- lib/pipeline-selector-view.coffee | 7 +++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index a8a49f1..2e5cbb4 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -391,6 +391,7 @@ class GitlabStatus pipeline.commit = jobs[0].commit pipeline.created_at = jobs[0].created_at pipeline.finished_at = jobs[jobs?.length-1].finished_at + pipeline.user = jobs[0].user pipeline.search = "id#{pipeline.id} ref#{pipeline.ref} sha#{pipeline.sha?.substring(0,5)}" pipeline.durationSuccess = jobs.filter( (j) -> j.status is 'success').reduce( ((max, j) -> Math.max(max, j.duration) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index 9726702..0e40f25 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -41,7 +41,9 @@ class JobSelectorView extends SelectListView @span class: 'icon icon-git-commit', commit?.title @span class: 'icon icon-git-branch text-muted pull-right', " #{ref} / #{commit?.short_id}" @div class: 'block', => - @span class: 'icon icon-clock text-center', moment(commit?.created_at).format('lll') + @span class: 'icon icon-clock text-center', "#{moment(commit?.created_at).format('lll')} / #{moment(commit?.created_at).fromNow()}" + @span class: 'pull-right', => + @raw " #{thiz.user?.name}" @div class: 'block', => @div class: 'btn-group', => @button outlet: 'sortById', class: 'btn', ' Sort by id' @@ -85,6 +87,8 @@ class JobSelectorView extends SelectListView return 0 calculate: () -> + if @items?.length > 0 + @user = @items[0].user @maxDuration = @items?.reduce( ((max, j) -> Math.max(max, j.duration) ), 0 ) @@ -109,7 +113,6 @@ class JobSelectorView extends SelectListView
    #{moment(job.created_at).format('lll')} - #{moment(job.finished_at).format('lll')} #{job.runner?.description} - #{job.user?.name}
  • " confirmed: (job) => diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index e34b7ec..376a94f 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -95,7 +95,7 @@ class PipelineSelectorView extends SelectListView calculate: () -> if @items?.length > 0 @branch = @items[0].ref - + @maxDuration = @items?.reduce( ((max, p) -> Math.max(max, p.duration) ), 0 ) @@ -136,7 +136,7 @@ class PipelineSelectorView extends SelectListView ABS ♨︎ #{@controller.toHHMMSS(pipeline.elapsed)}   - #{pipeline.commit.short_id} + #{pipeline.commit?.short_id}
    @@ -159,6 +159,9 @@ class PipelineSelectorView extends SelectListView #{unstable.length} #{alwaysFailed.length} #{alwaysFailed.length} + + #{pipeline.user?.name} + " else "
  • From f1803339f8c8b1c70e60302fedebaa010e316f51 Mon Sep 17 00:00:00 2001 From: azachar Date: Thu, 18 Jan 2018 11:30:43 +0100 Subject: [PATCH 24/31] feat(pipeline): relative time --- lib/gitlab.coffee | 7 ++----- lib/job-selector-view.coffee | 2 +- lib/pipeline-selector-view.coffee | 11 +++++------ 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index 2e5cbb4..e953eac 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -384,9 +384,6 @@ class GitlabStatus loadPipelineJobs: (host, project, pipeline) -> @fetch(host, "projects/#{project.id}/" + "pipelines/#{pipeline.id}/jobs", true) .then((jobs) -> - pipeline.durationSuccess = 0 - pipeline.duration = 0 - if jobs?.length > 0 pipeline.commit = jobs[0].commit pipeline.created_at = jobs[0].created_at @@ -394,10 +391,10 @@ class GitlabStatus pipeline.user = jobs[0].user pipeline.search = "id#{pipeline.id} ref#{pipeline.ref} sha#{pipeline.sha?.substring(0,5)}" pipeline.durationSuccess = jobs.filter( (j) -> j.status is 'success').reduce( ((max, j) -> - Math.max(max, j.duration) + Math.max(max, j.duration || 0) ), 0) pipeline.duration = jobs.reduce( ((max, j) -> - Math.max(max, j.duration) + Math.max(max, j.duration || 0) ), 0) pipeline.loadedJobs = jobs ) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index 0e40f25..c0ef08e 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -90,7 +90,7 @@ class JobSelectorView extends SelectListView if @items?.length > 0 @user = @items[0].user @maxDuration = @items?.reduce( ((max, j) -> - Math.max(max, j.duration) + Math.max(max, j.duration || 0) ), 0 ) if @items?.length > 0 diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index 376a94f..39107af 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -97,14 +97,14 @@ class PipelineSelectorView extends SelectListView @branch = @items[0].ref @maxDuration = @items?.reduce( ((max, p) -> - Math.max(max, p.duration) + Math.max(max, p.duration || 0) ), 0 ) @averageDuration = percentile(50, @items, (item) -> item.duration).duration success = @items.filter( (p) -> p.status is 'success') if success?.length > 0 @maxDurationSuccess = success?.reduce( ((max, p) -> - Math.max(max, p.durationSuccess) + Math.max(max, p.durationSuccess || 0) ), 0 ) @averageDurationSuccess = percentile(50, success, (item) -> item.durationSuccess).durationSuccess @@ -132,10 +132,8 @@ class PipelineSelectorView extends SelectListView
    #{pipeline.id} - #{moment(pipeline.created_at).format('lll')} - - ABS ♨︎ #{@controller.toHHMMSS(pipeline.elapsed)} -   + #{moment(pipeline.created_at).format('lll')} / #{moment(pipeline.created_at).fromNow()} + #{pipeline.commit?.short_id}
    @@ -146,6 +144,7 @@ class PipelineSelectorView extends SelectListView
    ♨︎ #{@controller.toHHMMSS(pipeline.duration)} + / ABS #{@controller.toHHMMSS(pipeline.elapsed)}
    #{@asUniqueNames(alwaysSuccess)}
    From f36195a8fcf687236d497f1276e82f902e8b3617 Mon Sep 17 00:00:00 2001 From: azachar Date: Thu, 18 Jan 2018 12:28:21 +0100 Subject: [PATCH 25/31] fix(pipeline): always fails duplicated --- lib/pipeline-selector-view.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index 39107af..4d8c68a 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -157,7 +157,6 @@ class PipelineSelectorView extends SelectListView #{alwaysSuccess.length} #{unstable.length} #{alwaysFailed.length} - #{alwaysFailed.length} #{pipeline.user?.name} From 4f9826fd75d0029a15d6de4c28a670359e772f75 Mon Sep 17 00:00:00 2001 From: azachar Date: Fri, 19 Jan 2018 11:10:17 +0100 Subject: [PATCH 26/31] fix(pipeline): undefined if no jobs --- lib/gitlab.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index e953eac..11deb23 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -297,6 +297,7 @@ class GitlabStatus if jobs.length is 0 @onJobs(project, [ name: pipeline.name + pipeline: pipeline.id status: pipeline.status jobs: [] ]) From e325c6703447b0de5d710e8b7024b8c8ccfed8b0 Mon Sep 17 00:00:00 2001 From: azachar Date: Sun, 21 Jan 2018 01:54:58 +0100 Subject: [PATCH 27/31] feat(job): sort by duration --- lib/job-selector-view.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index c0ef08e..b6b71e7 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -49,6 +49,7 @@ class JobSelectorView extends SelectListView @button outlet: 'sortById', class: 'btn', ' Sort by id' @button outlet: 'sortByName', class: 'btn', ' Sort by name' @button outlet: 'sortByDate', class: 'btn', ' Sort by date' + @button outlet: 'sortByDuration', class: 'btn', ' Sort by duration' handleEvents: -> @wireOutlets(@) @@ -86,6 +87,10 @@ class JobSelectorView extends SelectListView else return 0 + @sortByDuration.on 'mouseover', (e) => + @setItems @items.sort (a, b) -> + return b.duration - a.duration + calculate: () -> if @items?.length > 0 @user = @items[0].user From af0db49cb5cc4f9ae2e2fd2b5d4d2173254b6e6e Mon Sep 17 00:00:00 2001 From: azachar Date: Sun, 21 Jan 2018 02:18:36 +0100 Subject: [PATCH 28/31] fix(progress): initial progress bar is calculate for jobs and pipeline --- lib/job-selector-view.coffee | 28 +++++++++----------- lib/pipeline-selector-view.coffee | 43 ++++++++++++++++--------------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index b6b71e7..cfa1583 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -11,8 +11,8 @@ class JobSelectorView extends SelectListView @projectPath = projectPath {@alwaysSuccess, @unstable, @alwaysFailed, @total} = @controller.statistics(@jobs) @addClass('overlay from-top') + @calculate jobs @setItems jobs - @calculate() @panel ?= atom.workspace.addModalPanel(item: this) @focusFilterEditor() $$(@extraContent(@)).insertBefore(@error) @@ -56,19 +56,17 @@ class JobSelectorView extends SelectListView @alwaysSuccessButton.on 'mouseover', (e) => @setItems @alwaysSuccess - @calculate() - + @calculate @items @sometimesFailedButton.on 'mouseover', (e) => @setItems @unstable - @calculate() - + @calculate @items @alwaysFailedButton.on 'mouseover', (e) => @setItems @alwaysFailed - @calculate() + @calculate @items @allButton.on 'mouseover', (e) => @setItems @jobs - @calculate() + @calculate @items @sortById.on 'mouseover', (e) => @setItems @items.sort (a, b) -> @@ -91,15 +89,13 @@ class JobSelectorView extends SelectListView @setItems @items.sort (a, b) -> return b.duration - a.duration - calculate: () -> - if @items?.length > 0 - @user = @items[0].user - @maxDuration = @items?.reduce( ((max, j) -> - Math.max(max, j.duration || 0) - ), 0 ) - - if @items?.length > 0 - @averageDuration = percentile(50, @items, (item) -> item.duration).duration + calculate: (items) -> + if items?.length > 0 + @user = items[0].user + @averageDuration = percentile(50, items, (item) -> item.duration).duration + @maxDuration = items?.reduce( ((max, j) -> + Math.max(max, j.duration || 0) + ), 0 ) viewForItem: (job) -> type = @controller.toType(job, @averageDuration) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index 4d8c68a..0bd9d96 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -16,8 +16,8 @@ class PipelineSelectorView extends SelectListView @projectPath = projectPath @addClass('overlay from-top') + @calculate pipelines @setItems pipelines - @calculate() @panel ?= atom.workspace.addModalPanel(item: this) @focusFilterEditor() $$(@extraContent(@)).insertBefore(@error) @@ -92,30 +92,31 @@ class PipelineSelectorView extends SelectListView else return 0 - calculate: () -> - if @items?.length > 0 - @branch = @items[0].ref + calculate: (items) -> + if items?.length > 0 + @branch = items[0].ref - @maxDuration = @items?.reduce( ((max, p) -> - Math.max(max, p.duration || 0) - ), 0 ) - @averageDuration = percentile(50, @items, (item) -> item.duration).duration - - success = @items.filter( (p) -> p.status is 'success') - if success?.length > 0 - @maxDurationSuccess = success?.reduce( ((max, p) -> - Math.max(max, p.durationSuccess || 0) + @maxDuration = items.reduce( ((max, p) -> + Math.max(max, p.duration || 0) ), 0 ) - @averageDurationSuccess = percentile(50, success, (item) -> item.durationSuccess).durationSuccess - allJobs = @items.reduce(((all, p) -> - if p.loadedJobs?.length > 0 - all.concat(p.loadedJobs) - else - all - ), []) + @averageDuration = percentile(50, items, (item) -> item.duration).duration + + success = items.filter( (p) -> p.status is 'success') + if success?.length > 0 + @maxDurationSuccess = success?.reduce( ((max, p) -> + Math.max(max, p.durationSuccess || 0) + ), 0 ) + @averageDurationSuccess = percentile(50, success, (item) -> item.durationSuccess).durationSuccess + + allJobs = items.reduce(((all, p) -> + if p.loadedJobs?.length > 0 + all.concat(p.loadedJobs) + else + all + ), []) - {@alwaysSuccess, @unstable, @alwaysFailed, @total} = @controller.statistics(allJobs) + {@alwaysSuccess, @unstable, @alwaysFailed, @total} = @controller.statistics(allJobs) asUniqueNames: (jobs) => return jobs.map((j) => j.name).unique() From 3806b3dd9ff6e0c6ad1cb39c4cb2feb027398990 Mon Sep 17 00:00:00 2001 From: azachar Date: Sun, 21 Jan 2018 02:22:32 +0100 Subject: [PATCH 29/31] feat(pipeline): sort by duration --- lib/pipeline-selector-view.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index 0bd9d96..919f5c2 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -71,6 +71,7 @@ class PipelineSelectorView extends SelectListView @button outlet: 'sortById', class: 'btn', ' Sort by id' @button outlet: 'sortBySha', class: 'btn', ' Sort by sha' @button outlet: 'sortByDate', class: 'btn', ' Sort by date' + @button outlet: 'sortByDuration', class: 'btn', ' Sort by duration' handleEvents: -> @wireOutlets(@) @@ -92,6 +93,10 @@ class PipelineSelectorView extends SelectListView else return 0 + @sortByDuration.on 'mouseover', (e) => + @setItems @items.sort (a, b) -> + return b.duration - a.duration + calculate: (items) -> if items?.length > 0 @branch = items[0].ref From d84dbb1a05a7398a5cfcf36c2728c0dc08886b43 Mon Sep 17 00:00:00 2001 From: azachar Date: Sun, 21 Jan 2018 04:38:03 +0100 Subject: [PATCH 30/31] feat(all): pipeline selector for all pipelines regardless branch feat(all): filter success, unstable and failed pipelines fix(gitlab): update pipeline selected by user --- lib/all-pipeline-selector-view.coffee | 172 ++++++++++++++++++++++++++ lib/gitlab-integration.coffee | 4 + lib/gitlab.coffee | 25 +++- lib/job-selector-view.coffee | 4 +- lib/pipeline-selector-view.coffee | 2 +- lib/status-bar-view.coffee | 13 ++ 6 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 lib/all-pipeline-selector-view.coffee diff --git a/lib/all-pipeline-selector-view.coffee b/lib/all-pipeline-selector-view.coffee new file mode 100644 index 0000000..07bfcfe --- /dev/null +++ b/lib/all-pipeline-selector-view.coffee @@ -0,0 +1,172 @@ +{$$, SelectListView} = require 'atom-space-pen-views' +percentile = require 'percentile' +moment = require 'moment' + +class AllPipelineSelectorView extends SelectListView + initialize: (pipelines, controller, projectPath) -> + super + + @pipelines = pipelines + @controller = controller + @projectPath = projectPath + + @addClass('overlay from-top') + @globalCalculate pipelines + @calculate pipelines + @setItems pipelines + @panel ?= atom.workspace.addModalPanel(item: this) + @focusFilterEditor() + $$(@extraContent(@)).insertBefore(@error) + @handleEvents() + @panel.show() + + getFilterKey: -> 'search' + + extraContent: (thiz) -> + return -> + @div class: 'block', => + @button outlet: 'allButton', class: 'btn btn-info', ' All', => + @span class: 'badge badge-small', thiz.jobs?.length + @div class: 'btn-group', => + @button outlet: 'successButton', class: 'btn btn-success', ' Success', => + @span class: 'badge badge-small', thiz.success?.length + @button outlet: 'unstableButton', class: 'btn btn-warning', ' Unstable', => + @span class: 'badge badge-small', thiz.unstable?.length + @button outlet: 'failedButton', class: 'btn btn-error', ' Failed', => + @span class: 'badge badge-small', thiz.failed?.length + + @div class: 'block', => + @div class: 'btn-group', => + @button outlet: 'sortById', class: 'btn', ' Sort by id' + @button outlet: 'sortBySha', class: 'btn', ' Sort by sha' + @button outlet: 'sortByDate', class: 'btn', ' Sort by date' + @button outlet: 'sortByDuration', class: 'btn', ' Sort by duration' + @button outlet: 'sortByBranch', class: 'btn', ' Sort by branch' + + handleEvents: -> + @wireOutlets(@) + + @successButton.on 'mouseover', (e) => + @setItems @success + @calculate @items + + @unstableButton.on 'mouseover', (e) => + @setItems @unstable + @calculate @items + + @failedButton.on 'mouseover', (e) => + @setItems @failed + @calculate @items + + @sortById.on 'mouseover', (e) => + @setItems @items.sort (a, b) -> + return a.id - b.id + + @sortBySha.on 'mouseover', (e) => + @setItems @items.sort (a, b) -> + return -1 if a.sha < b.sha + return 1 if a.sha > b.sha + return 0 + + @sortByDate.on 'mouseover', (e) => + @setItems @items.sort (a, b) -> + if a.created_at and b.created_at + return moment(b.created_at).diff(moment(a.created_at)) + else + return 0 + + @sortByDuration.on 'mouseover', (e) => + @setItems @items.sort (a, b) -> + return b.duration - a.duration + + @sortByBranch.on 'mouseover', (e) => + @setItems @items.sort (a, b) -> + return a.ref - b.ref + + calculate: (items) -> + if items?.length > 0 + @maxDuration = items.reduce( ((max, p) -> + Math.max(max, p.duration || 0) + ), 0 ) + + @averageDuration = percentile(50, items, (item) -> item.duration).duration + + success = items.filter( (p) -> p.status is 'success') + if success?.length > 0 + @maxDurationSuccess = success?.reduce( ((max, p) -> + Math.max(max, p.durationSuccess || 0) + ), 0 ) + @averageDurationSuccess = percentile(50, success, (item) -> item.durationSuccess).durationSuccess + + globalCalculate: (items) -> + if items?.length > 0 + @success = items.filter( (p) -> p.status is 'success') + @failed = items.filter( (p) -> p.status is 'failed') + @unstable = items.filter( (p) -> p.status is 'success' and p.loadedJobs?.filter( (j) => j.status is 'failed')?.length > 0) + + asUniqueNames: (jobs) => + return jobs.map((j) => j.name).unique() + + viewForItem: (pipeline) -> + pipeline.elapsed = moment(pipeline.finished_at).diff(moment(pipeline.created_at), 'seconds') + + if pipeline.loadedJobs?.length > 0 + {alwaysSuccess, unstable, alwaysFailed, total} = @controller.statistics(pipeline.loadedJobs) + + type = @controller.toType(pipeline, @averageDuration) + + "
  • +
    +
    + #{pipeline.id} + #{moment(pipeline.created_at).format('lll')} / #{moment(pipeline.created_at).fromNow()} + + #{pipeline.commit?.short_id} + #{pipeline.ref} + +
    +
    +
    + #{pipeline.commit?.title} +
    +
    + + ♨︎ #{@controller.toHHMMSS(pipeline.duration)} + / ABS #{@controller.toHHMMSS(pipeline.elapsed)} +
    + #{@asUniqueNames(alwaysSuccess)} +
    + #{@asUniqueNames(unstable)} +
    +
    + #{@asUniqueNames(alwaysFailed)} +
    + #{total.length} + #{alwaysSuccess.length} + #{unstable.length} + #{alwaysFailed.length} + + #{pipeline.user?.name} + +
  • " + else + "
  • +
    +
    + #{pipeline.id} + #{pipeline.sha} + #{pipeline.commit?.message} +
    +
    + +
    +
  • " + + confirmed: (pipeline) => + @cancel() + @controller.updatePipeline(pipeline, @projectPath); + + cancelled: -> + @panel.hide() + +module.exports = AllPipelineSelectorView diff --git a/lib/gitlab-integration.coffee b/lib/gitlab-integration.coffee index 629f034..57a49d1 100644 --- a/lib/gitlab-integration.coffee +++ b/lib/gitlab-integration.coffee @@ -4,6 +4,8 @@ GitUrlParse = require 'git-url-parse' StatusBarView = require './status-bar-view' GitlabStatus = require './gitlab' log = require './log' +app = require('electron').app; +moment = require 'moment' class GitlabIntegration config: @@ -123,6 +125,8 @@ class GitlabIntegration ) activate: (state) -> + if app + moment.locale(app.getLocale()) @subscriptions = new CompositeDisposable @statusBarView = new StatusBarView @statusBarView.init() diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index 11deb23..ac4f583 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -3,6 +3,7 @@ log = require './log' shell = require('electron').shell; JobSelectorView = require './job-selector-view' PipelineSelectorView = require './pipeline-selector-view' +AllPipelineSelectorView = require './all-pipeline-selector-view' class GitlabStatus constructor: (@view, @timeout=null, @projects={}, @pending=[], @jobs={}) -> @@ -232,6 +233,19 @@ class GitlabStatus { host, project, repos } = @projects[projectPath] selector = new PipelineSelectorView(project.pipelines, @ , projectPath) + openAllPipelineSelector: (projectPath) -> + { host, project, repos } = @projects[projectPath] + @fetch(host, "projects/#{project.id}/pipelines") + .then( (pipelines) => + loads = [] + loads.push @loadPipelineJobs(host, project, pipeline) for pipeline in pipelines + Promise.all(loads).then( ()=> + return pipelines + ) + ) + .then( (pipelines) => + selector = new AllPipelineSelectorView(pipelines, @ , projectPath) + ) schedule: -> @timeout = setTimeout @update.bind(@), @period @@ -251,7 +265,7 @@ class GitlabStatus { host, project, repos } = @projects[projectPath] if project? and project.id? and not @updating[projectPath] @updating[projectPath] = true - ref = repos?.getShortHead?() + ref = project.userForcedPipeline?.ref || repos?.getShortHead?() if ref? log "project #{project} ref is #{ref}" ref = "?ref=#{ref}" @@ -265,8 +279,11 @@ class GitlabStatus project.pipelines = pipelines; if pipelines.length > 0 if project.userForcedPipeline - currentPipeline = pipelines.filter( (p) => p.id is project.userForcedPipeline.id) - if not currentPipeline or currentPipeline.length is 0 + currentPipelineWrapped = pipelines.filter( (p) => p.id is project.userForcedPipeline.id) + # is in the pipelines? + if currentPipelineWrapped?.length > 0 + currentPipeline = currentPipelineWrapped[0] + if not currentPipeline currentPipeline = pipelines[0] project.userForcedPipeline = null @updateJobs(host, project, currentPipeline) @@ -298,6 +315,7 @@ class GitlabStatus @onJobs(project, [ name: pipeline.name pipeline: pipeline.id + pipelineStatus: pipeline.status status: pipeline.status jobs: [] ]) @@ -311,6 +329,7 @@ class GitlabStatus stage = name: job.stage pipeline: pipeline.id + pipelineStatus: pipeline.status status: 'created' jobs: [] stages = stages.concat([stage]) diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index cfa1583..7fef2f5 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -1,7 +1,7 @@ -{$, $$, SelectListView} = require 'atom-space-pen-views' +{$$, SelectListView} = require 'atom-space-pen-views' percentile = require 'percentile' moment = require 'moment' -# moment.locale('sk') + class JobSelectorView extends SelectListView initialize: (jobs, controller, projectPath) -> super diff --git a/lib/pipeline-selector-view.coffee b/lib/pipeline-selector-view.coffee index 919f5c2..f5e333e 100644 --- a/lib/pipeline-selector-view.coffee +++ b/lib/pipeline-selector-view.coffee @@ -1,4 +1,4 @@ -{$, $$, SelectListView} = require 'atom-space-pen-views' +{$$, SelectListView} = require 'atom-space-pen-views' percentile = require 'percentile' moment = require 'moment' diff --git a/lib/status-bar-view.coffee b/lib/status-bar-view.coffee index dbb01ad..d0a9cd2 100644 --- a/lib/status-bar-view.coffee +++ b/lib/status-bar-view.coffee @@ -110,10 +110,23 @@ class StatusBarView extends HTMLElement icon.onclick = (e) => @controller.openPipeline(project, stages); + allPipeline = document.createElement('span') + allPipeline.classList.add('icon', 'icon-inbox') + @tooltips.push atom.tooltips.add allPipeline, { + title: "Open all pipeline selector" + } + allPipeline.onclick = (e) => + @controller.openAllPipelineSelector(project); + status.appendChild allPipeline + pipeline = document.createElement('span') + pipeline.classList.add('icon', "gitlab-#{stages[0]?.pipelineStatus}") pipeline.innerHTML = "#{stages[0]?.pipeline}  " pipeline.onclick = (e) => @controller.openPipelineSelector(project); + @tooltips.push atom.tooltips.add pipeline, { + title: "Open branch pipeline selector" + } status.appendChild pipeline stages.forEach((stage) => From 4eca93c21059dac705e6211fa7a45fdb2c1346c2 Mon Sep 17 00:00:00 2001 From: azachar Date: Wed, 24 Jan 2018 15:02:25 +0100 Subject: [PATCH 31/31] fix(search): search keywords are based on template id: status: name: and so on --- lib/gitlab.coffee | 819 ++++++++++++++++++----------------- lib/job-selector-view.coffee | 2 +- 2 files changed, 411 insertions(+), 410 deletions(-) diff --git a/lib/gitlab.coffee b/lib/gitlab.coffee index ac4f583..3d94192 100644 --- a/lib/gitlab.coffee +++ b/lib/gitlab.coffee @@ -6,435 +6,436 @@ PipelineSelectorView = require './pipeline-selector-view' AllPipelineSelectorView = require './all-pipeline-selector-view' class GitlabStatus - constructor: (@view, @timeout=null, @projects={}, @pending=[], @jobs={}) -> - @token = atom.config.get('gitlab-integration.token') - @artifactReportPath = atom.config.get('gitlab-integration.artifactReportPath') - @period = atom.config.get('gitlab-integration.period') - @updating = {} - @watchTimeout = null - @view.setController(@) - - fetch: (host, q, paging=false) -> - @load(host,q).then((res) => - log " <- ", res - if res.headers.get('X-Next-Page') - if paging - log " -> retrieving #{res.headers.get('X-Total-Pages')} pages" - Promise.all( - [res.json()].concat( - new Array( - parseInt(res.headers.get('X-Total-Pages')) - 1, - ).fill(0).map( - (dum, i) => - log " -> page #{i + 2}" - fetch( - "https://#{host}/api/v4/#{q}" + - (if q.includes('?') then '&' else '?') + - "per_page=" + res.headers.get('X-Per-Page') + - "&page=#{i+2}", { - headers: { - 'PRIVATE-TOKEN': @token - } - } - ).then((page) => - log " <- page #{i + 2}", page - page.json() - ).catch((error) => - console.error "cannot fetch page #{i + 2}", error - Promise.resolve([]) - ) - ) - ) - ).then((all) => - Promise.resolve(all.reduce( - (all, one) => - all.concat(one) - , []) - ) - ) - else - log " -> ignoring paged output for #{q}" - res.json() - else - res.json() - ) - .then( (result) => - if result?.error - throw result - else - return result - ) - - load: (host, q) -> - log " -> fetch '#{q}' from '#{host}" - fetch( - "https://#{host}/api/v4/#{q}", { - headers: { - "PRIVATE-TOKEN": @token, - } - } - ) - - watch: (host, projectPath, repos) -> - projectPath = projectPath.toLowerCase() - if not @projects[projectPath]? and not @updating[projectPath]? - @updating[projectPath] = false - @view.loading projectPath, "loading project..." - @fetch(host, "projects?membership=yes", true) - .then( (projects) => - log "received projects from #{host}", projects - if projects? - project = projects.filter( - (project) => - project.path_with_namespace.toLowerCase() is - projectPath - )[0] - if project? - @projects[projectPath] = { host, project, repos } - @update() - else - @view.unknown(projectPath) - else - @view.unknown(projectPath) - ).catch((error) => - if error.error_description - atom.notifications.addWarning( - "Gitlab-Integration: #{error.error}: #{error.error_description}", - {dismissable: true} + constructor: (@view, @timeout=null, @projects={}, @pending=[], @jobs={}) -> + @token = atom.config.get('gitlab-integration.token') + @artifactReportPath = atom.config.get('gitlab-integration.artifactReportPath') + @period = atom.config.get('gitlab-integration.period') + @updating = {} + @watchTimeout = null + @view.setController(@) + + fetch: (host, q, paging=false) -> + @load(host,q).then((res) => + log " <- ", res + if res.headers.get('X-Next-Page') + if paging + log " -> retrieving #{res.headers.get('X-Total-Pages')} pages" + Promise.all( + [res.json()].concat( + new Array( + parseInt(res.headers.get('X-Total-Pages')) - 1, + ).fill(0).map( + (dum, i) => + log " -> page #{i + 2}" + fetch( + "https://#{host}/api/v4/#{q}" + + (if q.includes('?') then '&' else '?') + + "per_page=" + res.headers.get('X-Per-Page') + + "&page=#{i+2}", { + headers: { + 'PRIVATE-TOKEN': @token + } + } + ).then((page) => + log " <- page #{i + 2}", page + page.json() + ).catch((error) => + console.error "cannot fetch page #{i + 2}", error + Promise.resolve([]) ) - console.error "cannot fetch projects from #{host}", error - @updating[projectPath] = undefined - @view.unknown(projectPath) + ) ) - - printHeader: (job) -> - "[0K Log from branch #{job.ref} | job #{job.name} | # #{job.id} | pipeline #{job.pipeline.id} [0;m" - - loadJob: (host, project, job) -> - atom.notifications.addInfo( - "Downloading build log for job #{job.name} #{job.id}", - {dismissable: true} - ) - @load(host, "projects/#{project.id}/jobs/#{job.id}/trace", false) - .then( (res) -> - return res.text() - ) - .then( (text) -> - return text: text, job: job - ) - .catch((error) -> - atom.notifications.addWarning( - "Unable to load the build log due to #{error}", - {dismissable: true} + ).then((all) => + Promise.resolve(all.reduce( + (all, one) => + all.concat(one) + , []) ) - console.error "cannot fetch the build log from projects/#{project.id}/jobs/#{job.id}/trace", error - ) - - openLog: (projectPath, job) -> - { host, project, repos } = @projects[projectPath] - @loadJob(host, project, job) - .then( (downloadedLog) => - atom.workspace.open(undefined, { - # split : 'right' - }).then (editor) => - # editor.setFileName("#{job.name}.#{job.ref}.#{job.id}") - editor.setGrammar(atom.grammars.grammarForScopeName('text.ansi')) - editor.insertText @printHeader downloadedLog.job - editor.insertNewline() - editor.insertNewline() - editor.insertText(downloadedLog.text) - ) - .catch((error) -> - console.error "cannot open editor for the build log of #{projectPath} and job #{job.id}", error + ) + else + log " -> ignoring paged output for #{q}" + res.json() + else + res.json() + ) + .then( (result) => + if result?.error + throw result + else + return result + ) + + load: (host, q) -> + log " -> fetch '#{q}' from '#{host}" + fetch( + "https://#{host}/api/v4/#{q}", { + headers: { + "PRIVATE-TOKEN": @token, + } + } + ) + + watch: (host, projectPath, repos) -> + projectPath = projectPath.toLowerCase() + if not @projects[projectPath]? and not @updating[projectPath]? + @updating[projectPath] = false + @view.loading projectPath, "loading project..." + @fetch(host, "projects?membership=yes", true) + .then( (projects) => + log "received projects from #{host}", projects + if projects? + project = projects.filter( + (project) => + project.path_with_namespace.toLowerCase() is + projectPath + )[0] + if project? + @projects[projectPath] = { host, project, repos } + @update() + else + @view.unknown(projectPath) + else + @view.unknown(projectPath) + ).catch((error) => + if error.error_description + atom.notifications.addWarning( + "Gitlab-Integration: #{error.error}: #{error.error_description}", + {dismissable: true} + ) + console.error "cannot fetch projects from #{host}", error + @updating[projectPath] = undefined + @view.unknown(projectPath) ) - openFailedLogs: (projectPath, jobs) -> - @openLogs(projectPath, jobs.filter (job) -> job.status is 'failed') - - openFailedLogsInGroup: (projectPath, jobs, mainJob) -> - @openFailedLogs(projectPath, jobs.filter (job) -> job.name is mainJob.name) - - openLogs: (projectPath, jobs) -> - { host, project, repos } = @projects[projectPath] - - atom.notifications.addInfo( - "Downloading build logs for #{jobs.length} jobs", - {dismissable: true} + printHeader: (job) -> + "[0K Log from branch #{job.ref} | job #{job.name} | # #{job.id} | pipeline #{job.pipeline.id} [0;m" + + loadJob: (host, project, job) -> + atom.notifications.addInfo( + "Downloading build log for job #{job.name} #{job.id}", + {dismissable: true} + ) + @load(host, "projects/#{project.id}/jobs/#{job.id}/trace", false) + .then( (res) -> + return res.text() ) - - jobsToLoad = ( - @loadJob(host, project, aJob) for aJob in jobs + .then( (text) -> + return text: text, job: job ) - - Promise.all(jobsToLoad) - .then (logs) => - logs?.sort (a, b) -> - return -1 if a.job.name < b.job.name - return 1 if a.job.name > b.job.name - - return -1 if a.job.id < b.job.id - return 1 if a.job.id > b.job.id - return 0 - - atom.workspace.open(undefined, { - # split : 'right' - }).then (editor) => - # editor.setFileName("#{job.name}.#{job.ref}.#{job.id}") - editor.setGrammar(atom.grammars.grammarForScopeName('text.ansi')) - for l in logs - editor.insertNewline() - editor.insertText @printHeader l.job - editor.insertNewline() - editor.insertText(l.text) - # atom.notifications.addInfo( - # "Build log #{l.job.name} (#{l.job.id}) downloaded", - # {dismissable: true} - # ) - .catch((error) -> - console.error "cannot open editor for build logs of jobs of #{projectPath}", error - ) - - openReport: (projectPath, job) -> - if not job.artifacts_file + .catch((error) -> atom.notifications.addWarning( - "No artifacts for job #{job.name}", + "Unable to load the build log due to #{error}", {dismissable: true} ) - return - - path = @artifactReportPath.split('').join(job.name) + console.error "cannot fetch the build log from projects/#{project.id}/jobs/#{job.id}/trace", error + ) - if not path - atom.notifications.addWarning( - "Unknown path for artifact to download for #{job.name}", - {dismissable: true} - ) - return + openLog: (projectPath, job) -> + { host, project, repos } = @projects[projectPath] + @loadJob(host, project, job) + .then( (downloadedLog) => + atom.workspace.open(undefined, { + # split : 'right' + }).then (editor) => + # editor.setFileName("#{job.name}.#{job.ref}.#{job.id}") + editor.setGrammar(atom.grammars.grammarForScopeName('text.ansi')) + editor.insertText @printHeader downloadedLog.job + editor.insertNewline() + editor.insertNewline() + editor.insertText(downloadedLog.text) + ) + .catch((error) -> + console.error "cannot open editor for the build log of #{projectPath} and job #{job.id}", error + ) + + openFailedLogs: (projectPath, jobs) -> + @openLogs(projectPath, jobs.filter (job) -> job.status is 'failed') + + openFailedLogsInGroup: (projectPath, jobs, mainJob) -> + @openFailedLogs(projectPath, jobs.filter (job) -> job.name is mainJob.name) + + openLogs: (projectPath, jobs) -> + { host, project, repos } = @projects[projectPath] + + atom.notifications.addInfo( + "Downloading build logs for #{jobs.length} jobs", + {dismissable: true} + ) + + jobsToLoad = ( + @loadJob(host, project, aJob) for aJob in jobs + ) + + Promise.all(jobsToLoad) + .then (logs) => + logs?.sort (a, b) -> + return -1 if a.job.name < b.job.name + return 1 if a.job.name > b.job.name + + return -1 if a.job.id < b.job.id + return 1 if a.job.id > b.job.id + return 0 + + atom.workspace.open(undefined, { + # split : 'right' + }).then (editor) => + # editor.setFileName("#{job.name}.#{job.ref}.#{job.id}") + editor.setGrammar(atom.grammars.grammarForScopeName('text.ansi')) + for l in logs + editor.insertNewline() + editor.insertText @printHeader l.job + editor.insertNewline() + editor.insertText(l.text) + # atom.notifications.addInfo( + # "Build log #{l.job.name} (#{l.job.id}) downloaded", + # {dismissable: true} + # ) + .catch((error) -> + console.error "cannot open editor for build logs of jobs of #{projectPath}", error + ) - { host, project, repos } = @projects[projectPath] - shell.openExternal("https://#{host}/#{encodeURI(projectPath)}/-/jobs/#{job.id}/artifacts/raw/#{path}"); + openReport: (projectPath, job) -> + if not job.artifacts_file + atom.notifications.addWarning( + "No artifacts for job #{job.name}", + {dismissable: true} + ) + return - openGitlabCICD: (projectPath) -> - { host, project, repos } = @projects[projectPath] - return unless host - shell.openExternal("https://#{host}/#{projectPath}/pipelines"); + path = @artifactReportPath.split('').join(job.name) - openPipeline: (projectPath, stages) -> - if stages - { host, project, repos } = @projects[projectPath] - shell.openExternal("https://#{host}/#{projectPath}/pipelines/#{stages[0].pipeline}"); - else - @openGitlabCICD(projectPath) + if not path + atom.notifications.addWarning( + "Unknown path for artifact to download for #{job.name}", + {dismissable: true} + ) + return - openJobSelector: (projectPath, stage) -> - selector = new JobSelectorView(stage.jobs, @ , projectPath) + { host, project, repos } = @projects[projectPath] + shell.openExternal("https://#{host}/#{encodeURI(projectPath)}/-/jobs/#{job.id}/artifacts/raw/#{path}"); - openPipelineSelector: (projectPath) -> - { host, project, repos } = @projects[projectPath] - selector = new PipelineSelectorView(project.pipelines, @ , projectPath) + openGitlabCICD: (projectPath) -> + { host, project, repos } = @projects[projectPath] + return unless host + shell.openExternal("https://#{host}/#{projectPath}/pipelines"); - openAllPipelineSelector: (projectPath) -> + openPipeline: (projectPath, stages) -> + if stages { host, project, repos } = @projects[projectPath] - @fetch(host, "projects/#{project.id}/pipelines") - .then( (pipelines) => - loads = [] - loads.push @loadPipelineJobs(host, project, pipeline) for pipeline in pipelines - Promise.all(loads).then( ()=> - return pipelines - ) - ) - .then( (pipelines) => - selector = new AllPipelineSelectorView(pipelines, @ , projectPath) + shell.openExternal("https://#{host}/#{projectPath}/pipelines/#{stages[0].pipeline}"); + else + @openGitlabCICD(projectPath) + + openJobSelector: (projectPath, stage) -> + selector = new JobSelectorView(stage.jobs, @ , projectPath) + + openPipelineSelector: (projectPath) -> + { host, project, repos } = @projects[projectPath] + selector = new PipelineSelectorView(project.pipelines, @ , projectPath) + + openAllPipelineSelector: (projectPath) -> + { host, project, repos } = @projects[projectPath] + @fetch(host, "projects/#{project.id}/pipelines") + .then( (pipelines) => + loads = [] + loads.push @loadPipelineJobs(host, project, pipeline) for pipeline in pipelines + Promise.all(loads).then( ()=> + return pipelines ) - schedule: -> - @timeout = setTimeout @update.bind(@), @period - - update: -> - @pending = Object.keys(@projects).slice() - @updatePipelines() - - updatePipeline: (pipeline, projectPath) -> - { host, project, repos } = @projects[projectPath] - @jobs[project.path_with_namespace] = null - project.userForcedPipeline = pipeline - @updateJobs(host, project, pipeline) - - updatePipelines: -> - Object.keys(@projects).map( - (projectPath) => - { host, project, repos } = @projects[projectPath] - if project? and project.id? and not @updating[projectPath] - @updating[projectPath] = true - ref = project.userForcedPipeline?.ref || repos?.getShortHead?() - if ref? - log "project #{project} ref is #{ref}" - ref = "?ref=#{ref}" - else - ref = "" - if not @jobs[projectPath]? - @view.loading(projectPath, "loading pipelines...") - @fetch(host, "projects/#{project.id}/pipelines#{ref}").then( - (pipelines) => - log "received pipelines from #{host}/#{project.id}", pipelines - project.pipelines = pipelines; - if pipelines.length > 0 - if project.userForcedPipeline - currentPipelineWrapped = pipelines.filter( (p) => p.id is project.userForcedPipeline.id) - # is in the pipelines? - if currentPipelineWrapped?.length > 0 - currentPipeline = currentPipelineWrapped[0] - if not currentPipeline - currentPipeline = pipelines[0] - project.userForcedPipeline = null - @updateJobs(host, project, currentPipeline) - @loadPipelineJobs(host, project, pipeline) for pipeline in pipelines - else - @onJobs(project, []) - ).catch((error) => - console.error "cannot fetch pipelines for project #{projectPath}", error - @endUpdate(project) - ) - ) - - endUpdate: (project) -> - log "project #{project} update end" - @updating[project] = false - @pending = @pending.filter((pending) => pending isnt project) - if @pending.length is 0 - @view.onStagesUpdate(@jobs) - @schedule() - @jobs[project.path_with_namespace] - - updateJobs: (host, project, pipeline) -> - if not @jobs[project.path_with_namespace]? - @view.loading(project.path_with_namespace, "loading jobs...") - @fetch(host, "projects/#{project.id}/" + "pipelines/#{pipeline.id}/jobs", true) - .then((jobs) => - log "received jobs from #{host}/#{project.id}/#{pipeline.id}", jobs - if jobs.length is 0 - @onJobs(project, [ - name: pipeline.name - pipeline: pipeline.id - pipelineStatus: pipeline.status - status: pipeline.status - jobs: [] - ]) - else - @onJobs(project, jobs.sort((a, b) -> a.id - b.id).reduce( - (stages, job) -> - stage = stages.find( - (stage) -> stage.name is job.stage - ) - if not stage? - stage = - name: job.stage - pipeline: pipeline.id - pipelineStatus: pipeline.status - status: 'created' - jobs: [] - stages = stages.concat([stage]) - stage.jobs = stage.jobs.concat([job]) - return stages - , []).map((stage) -> - Object.assign(stage, { - firstFailedJob: stage.jobs - .filter( (job) -> job.status is 'failed' )?[0] - - status: stage?.jobs - .sort((a, b) -> b.id - a.id) - .reduce((status, job) -> - switch status - when 'failed' then 'failed' - when 'pending' then 'pending' - when 'manual' then 'manual' - when 'running' then 'running' - else job.status - , 'created') - }) - )) - ).catch((error) => - console.error "cannot fetch jobs for pipeline ##{pipeline.id} of project #{project.path_with_namespace}", error + ) + .then( (pipelines) => + selector = new AllPipelineSelectorView(pipelines, @ , projectPath) + ) + schedule: -> + @timeout = setTimeout @update.bind(@), @period + + update: -> + @pending = Object.keys(@projects).slice() + @updatePipelines() + + updatePipeline: (pipeline, projectPath) -> + { host, project, repos } = @projects[projectPath] + @jobs[project.path_with_namespace] = null + project.userForcedPipeline = pipeline + @updateJobs(host, project, pipeline) + + updatePipelines: -> + Object.keys(@projects).map( + (projectPath) => + { host, project, repos } = @projects[projectPath] + if project? and project.id? and not @updating[projectPath] + @updating[projectPath] = true + ref = project.userForcedPipeline?.ref || repos?.getShortHead?() + if ref? + log "project #{project} ref is #{ref}" + ref = "?ref=#{ref}" + else + ref = "" + if not @jobs[projectPath]? + @view.loading(projectPath, "loading pipelines...") + @fetch(host, "projects/#{project.id}/pipelines#{ref}").then( + (pipelines) => + log "received pipelines from #{host}/#{project.id}", pipelines + project.pipelines = pipelines; + if pipelines.length > 0 + if project.userForcedPipeline + currentPipelineWrapped = pipelines.filter( (p) => p.id is project.userForcedPipeline.id) + # is in the pipelines? + if currentPipelineWrapped?.length > 0 + currentPipeline = currentPipelineWrapped[0] + if not currentPipeline + currentPipeline = pipelines[0] + project.userForcedPipeline = null + @updateJobs(host, project, currentPipeline) + @loadPipelineJobs(host, project, pipeline) for pipeline in pipelines + else + @onJobs(project, []) + ).catch((error) => + console.error "cannot fetch pipelines for project #{projectPath}", error @endUpdate(project) - ) - - alwaysFailed: (items)-> - passedNames = items.filter((job) => job.status is "success").map( (job)=> job.name) - return items.filter((job) => job.status is "failed" and job.name not in passedNames) - - alwaysSuccess: (items)-> - failedNames = items.filter((job) => job.status is "failed").map( (job)=> job.name) - return items.filter((job) => job.status is "success" and job.name not in failedNames) - - statistics: (jobs)-> - if jobs?.length - total = jobs.filter ( (j) => j.status is 'success' or 'failed') - - alwaysSuccess = @alwaysSuccess( jobs ) - failed = jobs.filter ( (j) => j.status is 'failed') - alwaysFailed = @alwaysFailed( jobs ) - unstable = failed.filter( (j) => j not in alwaysFailed) - - return {alwaysSuccess, unstable, alwaysFailed, total} - else - return {alwaysSuccess:[], unstable:[], alwaysFailed:[], total:[]} - - toHHMMSS: (sec_num) -> - sec_num = Math.round(sec_num) - hours = Math.floor(sec_num / 3600) - minutes = Math.floor((sec_num - (hours * 3600)) / 60) - seconds = sec_num - (hours * 3600) - (minutes * 60) - if hours < 10 - hours = "0"+hours - if minutes < 10 - minutes = "0"+minutes - if seconds < 10 - seconds = "0"+seconds - if hours is "00" - "#{minutes}m #{seconds}s" + ) + ) + + endUpdate: (project) -> + log "project #{project} update end" + @updating[project] = false + @pending = @pending.filter((pending) => pending isnt project) + if @pending.length is 0 + @view.onStagesUpdate(@jobs) + @schedule() + @jobs[project.path_with_namespace] + + updateJobs: (host, project, pipeline) -> + if not @jobs[project.path_with_namespace]? + @view.loading(project.path_with_namespace, "loading jobs...") + @fetch(host, "projects/#{project.id}/" + "pipelines/#{pipeline.id}/jobs", true) + .then((jobs) => + log "received jobs from #{host}/#{project.id}/#{pipeline.id}", jobs + jobs.every (job) -> job.search = "id:#{job.id} name:#{job.name} runner:#{job.runner?.description} status:#{job.status}" + if jobs.length is 0 + @onJobs(project, [ + name: pipeline.name + pipeline: pipeline.id + pipelineStatus: pipeline.status + status: pipeline.status + jobs: [] + ]) else - "#{hours}h #{minutes}m #{seconds}s" - - toType: (item, percentile) -> - deviation = Math.round(item.duration / percentile * 100) - if deviation < 85 then type = 'success' - if deviation > 115 then type = 'warning' - if deviation > 130 then type = 'error' - return type - - loadPipelineJobs: (host, project, pipeline) -> - @fetch(host, "projects/#{project.id}/" + "pipelines/#{pipeline.id}/jobs", true) - .then((jobs) -> - if jobs?.length > 0 - pipeline.commit = jobs[0].commit - pipeline.created_at = jobs[0].created_at - pipeline.finished_at = jobs[jobs?.length-1].finished_at - pipeline.user = jobs[0].user - pipeline.search = "id#{pipeline.id} ref#{pipeline.ref} sha#{pipeline.sha?.substring(0,5)}" - pipeline.durationSuccess = jobs.filter( (j) -> j.status is 'success').reduce( ((max, j) -> - Math.max(max, j.duration || 0) - ), 0) - pipeline.duration = jobs.reduce( ((max, j) -> - Math.max(max, j.duration || 0) - ), 0) - pipeline.loadedJobs = jobs - ) - .catch((error) => - console.error "cannot fetch jobs for pipeline ##{pipeline.id} of project #{project.path_with_namespace}", error - ) - - onJobs: (project, stages) -> - @jobs[project.path_with_namespace] = stages.slice() - @endUpdate(project.path_with_namespace) - Promise.resolve(stages) - - stop: -> - if @timeout? - clearTimeout @timeout - if @watchTimeout? - clearTimeout @watchTimeout - @view.hide() - - deactivate: -> - @stop() + @onJobs(project, jobs.sort((a, b) -> a.id - b.id).reduce( + (stages, job) -> + stage = stages.find( + (stage) -> stage.name is job.stage + ) + if not stage? + stage = + name: job.stage + pipeline: pipeline.id + pipelineStatus: pipeline.status + status: 'created' + jobs: [] + stages = stages.concat([stage]) + stage.jobs = stage.jobs.concat([job]) + return stages + , []).map((stage) -> + Object.assign(stage, { + firstFailedJob: stage.jobs + .filter( (job) -> job.status is 'failed' )?[0] + + status: stage?.jobs + .sort((a, b) -> b.id - a.id) + .reduce((status, job) -> + switch status + when 'failed' then 'failed' + when 'pending' then 'pending' + when 'manual' then 'manual' + when 'running' then 'running' + else job.status + , 'created') + }) + )) + ).catch((error) => + console.error "cannot fetch jobs for pipeline ##{pipeline.id} of project #{project.path_with_namespace}", error + @endUpdate(project) + ) + + alwaysFailed: (items)-> + passedNames = items.filter((job) => job.status is "success").map( (job)=> job.name) + return items.filter((job) => job.status is "failed" and job.name not in passedNames) + + alwaysSuccess: (items)-> + failedNames = items.filter((job) => job.status is "failed").map( (job)=> job.name) + return items.filter((job) => job.status is "success" and job.name not in failedNames) + + statistics: (jobs)-> + if jobs?.length + total = jobs.filter ( (j) => j.status is 'success' or 'failed') + + alwaysSuccess = @alwaysSuccess( jobs ) + failed = jobs.filter ( (j) => j.status is 'failed') + alwaysFailed = @alwaysFailed( jobs ) + unstable = failed.filter( (j) => j not in alwaysFailed) + + return {alwaysSuccess, unstable, alwaysFailed, total} + else + return {alwaysSuccess:[], unstable:[], alwaysFailed:[], total:[]} + + toHHMMSS: (sec_num) -> + sec_num = Math.round(sec_num) + hours = Math.floor(sec_num / 3600) + minutes = Math.floor((sec_num - (hours * 3600)) / 60) + seconds = sec_num - (hours * 3600) - (minutes * 60) + if hours < 10 + hours = "0"+hours + if minutes < 10 + minutes = "0"+minutes + if seconds < 10 + seconds = "0"+seconds + if hours is "00" + "#{minutes}m #{seconds}s" + else + "#{hours}h #{minutes}m #{seconds}s" + + toType: (item, percentile) -> + deviation = Math.round(item.duration / percentile * 100) + if deviation < 85 then type = 'success' + if deviation > 115 then type = 'warning' + if deviation > 130 then type = 'error' + return type + + loadPipelineJobs: (host, project, pipeline) -> + @fetch(host, "projects/#{project.id}/" + "pipelines/#{pipeline.id}/jobs", true) + .then((jobs) -> + if jobs?.length > 0 + pipeline.commit = jobs[0].commit + pipeline.created_at = jobs[0].created_at + pipeline.finished_at = jobs[jobs?.length-1].finished_at + pipeline.user = jobs[0].user + pipeline.search = "id:#{pipeline.id} ref:#{pipeline.ref} sha:#{pipeline.commit?.short_id} status:#{pipeline.status}" + pipeline.durationSuccess = jobs.filter( (j) -> j.status is 'success').reduce( ((max, j) -> + Math.max(max, j.duration || 0) + ), 0) + pipeline.duration = jobs.reduce( ((max, j) -> + Math.max(max, j.duration || 0) + ), 0) + pipeline.loadedJobs = jobs + ) + .catch((error) => + console.error "cannot fetch jobs for pipeline ##{pipeline.id} of project #{project.path_with_namespace}", error + ) + + onJobs: (project, stages) -> + @jobs[project.path_with_namespace] = stages.slice() + @endUpdate(project.path_with_namespace) + Promise.resolve(stages) + + stop: -> + if @timeout? + clearTimeout @timeout + if @watchTimeout? + clearTimeout @watchTimeout + @view.hide() + + deactivate: -> + @stop() module.exports = GitlabStatus diff --git a/lib/job-selector-view.coffee b/lib/job-selector-view.coffee index 7fef2f5..9315829 100644 --- a/lib/job-selector-view.coffee +++ b/lib/job-selector-view.coffee @@ -19,7 +19,7 @@ class JobSelectorView extends SelectListView @handleEvents() @panel.show() - getFilterKey: -> 'name' + getFilterKey: -> 'search' extraContent: (thiz) -> if thiz.jobs?.length > 0