diff --git a/app/assets/images/ci_favicons/icon_status_canceled.ico b/app/assets/images/ci_favicons/icon_status_canceled.ico new file mode 100755 index 0000000000000000000000000000000000000000..5a19458f2a2e0e5585f3932f01736cf838b86c76 Binary files /dev/null and b/app/assets/images/ci_favicons/icon_status_canceled.ico differ diff --git a/app/assets/images/ci_favicons/icon_status_created.ico b/app/assets/images/ci_favicons/icon_status_created.ico new file mode 100755 index 0000000000000000000000000000000000000000..4dca9640cb373737df8c7cce41af6dd8160b3913 Binary files /dev/null and b/app/assets/images/ci_favicons/icon_status_created.ico differ diff --git a/app/assets/images/ci_favicons/icon_status_failed.ico b/app/assets/images/ci_favicons/icon_status_failed.ico new file mode 100755 index 0000000000000000000000000000000000000000..c961ff9a69baa8f3cbd63ab9083c05554482052e Binary files /dev/null and b/app/assets/images/ci_favicons/icon_status_failed.ico differ diff --git a/app/assets/images/ci_favicons/icon_status_manual.ico b/app/assets/images/ci_favicons/icon_status_manual.ico new file mode 100755 index 0000000000000000000000000000000000000000..5fbbc99ea7cc79f1d48ee5e6f1ef88d2829b2c6f Binary files /dev/null and b/app/assets/images/ci_favicons/icon_status_manual.ico differ diff --git a/app/assets/images/ci_favicons/icon_status_not_found.ico b/app/assets/images/ci_favicons/icon_status_not_found.ico new file mode 100755 index 0000000000000000000000000000000000000000..21afa9c72e6ee7432b988907f1d8f90b70cd02a2 Binary files /dev/null and b/app/assets/images/ci_favicons/icon_status_not_found.ico differ diff --git a/app/assets/images/ci_favicons/icon_status_pending.ico b/app/assets/images/ci_favicons/icon_status_pending.ico new file mode 100755 index 0000000000000000000000000000000000000000..8be32dab85a13884518d830f2e3dbf788b75f14f Binary files /dev/null and b/app/assets/images/ci_favicons/icon_status_pending.ico differ diff --git a/app/assets/images/ci_favicons/icon_status_running.ico b/app/assets/images/ci_favicons/icon_status_running.ico new file mode 100755 index 0000000000000000000000000000000000000000..f328ff1a5ed098a18d98abf27676e60128e75303 Binary files /dev/null and b/app/assets/images/ci_favicons/icon_status_running.ico differ diff --git a/app/assets/images/ci_favicons/icon_status_skipped.ico b/app/assets/images/ci_favicons/icon_status_skipped.ico new file mode 100755 index 0000000000000000000000000000000000000000..b4394e1b4af94ab7efe98ed2c5dad06c49434c7f Binary files /dev/null and b/app/assets/images/ci_favicons/icon_status_skipped.ico differ diff --git a/app/assets/images/ci_favicons/icon_status_success.ico b/app/assets/images/ci_favicons/icon_status_success.ico new file mode 100755 index 0000000000000000000000000000000000000000..4f436c9524219a91bde74d32eda45fd37c6acef1 Binary files /dev/null and b/app/assets/images/ci_favicons/icon_status_success.ico differ diff --git a/app/assets/images/ci_favicons/icon_status_warning.ico b/app/assets/images/ci_favicons/icon_status_warning.ico new file mode 100755 index 0000000000000000000000000000000000000000..805cc20cdec49d82bc29a06676e8263a89425a9d Binary files /dev/null and b/app/assets/images/ci_favicons/icon_status_warning.ico differ diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 6efd26ccc3788ab2b290fac0c86315cba7e9be5f..fe54ecffdfe8446e9b879bc32ea72d4a60a7ad94 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -88,6 +88,7 @@ window.Build = (function() { dataType: 'json', success: function(buildData) { $('.js-build-output').html(buildData.trace_html); + gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); if (window.location.hash === DOWN_BUILD_TRACE) { $("html,body").scrollTop(this.$buildTrace.height()); } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 5e827b44a078257c82df353d85e0001d392ab118..f06cfcb9c06adf964509d74c8648803c44762e97 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -231,9 +231,11 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'projects:pipelines:builds': case 'projects:pipelines:show': const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; + const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; new gl.Pipelines({ initTabs: true, + pipelineStatusUrl, tabsOptions: { action: controllerAction, defaultAction: 'pipelines', diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 46b80c04e20e24bd704eae28485362a59f5cd89d..e1e6ca254465a4ba621656a0591532dfc2ec2c7d 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -2,6 +2,8 @@ (function() { (function(w) { var base; + const faviconEl = document.getElementById('favicon'); + const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; w.gl || (w.gl = {}); (base = w.gl).utils || (base.utils = {}); w.gl.utils.isInGroupsPage = function() { @@ -361,5 +363,34 @@ fn(next, stop); }); }; + + w.gl.utils.setFavicon = (iconName) => { + if (faviconEl && iconName) { + faviconEl.setAttribute('href', `/assets/${iconName}.ico`); + } + }; + + w.gl.utils.resetFavicon = () => { + if (faviconEl) { + faviconEl.setAttribute('href', originalFavicon); + } + }; + + w.gl.utils.setCiStatusFavicon = (pageUrl) => { + $.ajax({ + url: pageUrl, + dataType: 'json', + success: function(data) { + if (data && data.icon) { + gl.utils.setFavicon(`ci_favicons/${data.icon}`); + } else { + gl.utils.resetFavicon(); + } + }, + error: function() { + gl.utils.resetFavicon(); + } + }); + }; })(window); }).call(window); diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index f32d77c1648858bc6e2d8b0423fee3685b48fd93..a75247b236b7e05e1b65b173992ce6a1ad236506 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -38,11 +38,14 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; function MergeRequestWidget(opts) { // Initialize MergeRequestWidget behavior // - // check_enable - Boolean, whether to check automerge status - // merge_check_url - String, URL to use to check automerge status + // check_enable - Boolean, whether to check automerge status + // merge_check_url - String, URL to use to check automerge status // ci_status_url - String, URL to use to check CI status + // pipeline_status_url - String, URL to use to get CI status for Favicon // this.opts = opts; + this.opts.pipeline_status_url = `${this.opts.pipeline_status_url}.json`; + this.$widgetBody = $('.mr-widget-body'); $('#modal_merge_info').modal({ show: false }); @@ -180,6 +183,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; _this.status = data.status; _this.hasCi = data.has_ci; _this.updateMergeButton(_this.status, _this.hasCi); + gl.utils.setCiStatusFavicon(_this.opts.pipeline_status_url); if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (data.status !== _this.opts.ci_status || data.sha !== _this.opts.ci_sha || diff --git a/app/assets/javascripts/pipelines.js b/app/assets/javascripts/pipelines.js index 9203abefbbc27f8fff36035ede4af18755e30f91..4252b6158877ec6d2b67503a8ebd2c24ab595a21 100644 --- a/app/assets/javascripts/pipelines.js +++ b/app/assets/javascripts/pipelines.js @@ -9,6 +9,10 @@ require('./lib/utils/bootstrap_linked_tabs'); new global.LinkedTabs(options.tabsOptions); } + if (options.pipelineStatusUrl) { + gl.utils.setCiStatusFavicon(options.pipelineStatusUrl); + } + this.addMarginToBuildColumns(); } diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index f6d8bb08a646a4c24bc9b8d2aa0261d4b7245fb0..a611481a0a43c3f4532cfd70206bd451f30d511e 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -23,7 +23,7 @@ %title= page_title(site_name) %meta{ name: "description", content: page_description } - = favicon_link_tag favicon + = favicon_link_tag favicon, id: 'favicon' = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 0b0fb7854c2171316aef143a1fc1ffd41349f80f..c716b69b35b5e5da8dc36933b055209030b65eea 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -12,6 +12,7 @@ merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + pipeline_status_url: "#{pipeline_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", diff --git a/changelogs/unreleased/12818-ci-status-as-favicon.yml b/changelogs/unreleased/12818-ci-status-as-favicon.yml new file mode 100644 index 0000000000000000000000000000000000000000..70194178d9060aa74977977692d6c4f27b4fb900 --- /dev/null +++ b/changelogs/unreleased/12818-ci-status-as-favicon.yml @@ -0,0 +1,4 @@ +--- +title: Show CI status as Favicon on Pipelines, Job and MR pages +merge_request: 10144 +author: diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 549c7af8ea8dae77e979703d8ff3e62e414bbb4d..beee6cb2969e24c154c354dc790261fae925ca5f 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -75,6 +75,7 @@ describe('Build', () => { expect(url).toBe(`${BUILD_URL}.json`); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); success.call(context, { trace_html: 'Example', status: 'running' }); @@ -83,6 +84,7 @@ describe('Build', () => { it('removes the spinner', () => { const [{ success, context }] = $.ajax.calls.argsFor(0); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); success.call(context, { trace_html: 'Example', status: 'success' }); expect($('.js-build-refresh').length).toBe(0); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 5a93d479c1fc06afd863b5ad56ffd960809bc106..03f3c206f44c001140adb7595c7fc98af3b41b42 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -310,5 +310,56 @@ require('~/lib/utils/common_utils'); }); }, 10000); }); + + describe('gl.utils.setFavicon', () => { + it('should set page favicon to provided favicon', () => { + const faviconName = 'custom_favicon'; + const fakeLink = { + setAttribute() {}, + }; + + spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); + spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { + expect(attr).toEqual('href'); + expect(val.indexOf('/assets/custom_favicon.ico') > -1).toBe(true); + }); + gl.utils.setFavicon(faviconName); + }); + }); + + describe('gl.utils.resetFavicon', () => { + it('should reset page favicon to tanuki', () => { + const fakeLink = { + setAttribute() {}, + }; + + spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); + spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { + expect(attr).toEqual('href'); + expect(val).toMatch(/favicon/); + }); + gl.utils.resetFavicon(); + }); + }); + + describe('gl.utils.setCiStatusFavicon', () => { + it('should set page favicon to CI status favicon based on provided status', () => { + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`; + const FAVICON_PATH = 'ci_favicons/'; + const FAVICON = 'icon_status_success'; + const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub(); + const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub(); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ icon: FAVICON }); + expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH + FAVICON); + options.success(); + expect(spyResetFavicon).toHaveBeenCalled(); + options.error(); + expect(spyResetFavicon).toHaveBeenCalled(); + }); + + gl.utils.setCiStatusFavicon(BUILD_URL); + }); + }); }); })(); diff --git a/spec/javascripts/merge_request_widget/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget/merge_request_widget_spec.js index 561e6e8a036bdb9b0091dfad99a5a71c4e946310..889c26b1ee5d8dcbfc08030ac5b9aa589c14f81d 100644 --- a/spec/javascripts/merge_request_widget/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget/merge_request_widget_spec.js @@ -143,18 +143,21 @@ require('~/lib/utils/datetime_utility'); it('should call showCIStatus even if a notification should not be displayed', function() { var spy; spy = spyOn(this["class"], 'showCIStatus').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); }); it('should call showCIStatus when a notification should be displayed', function() { var spy; spy = spyOn(this["class"], 'showCIStatus').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(true); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); }); it('should call showCICoverage when the coverage rate is set', function() { var spy; spy = spyOn(this["class"], 'showCICoverage').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage); }); @@ -162,12 +165,14 @@ require('~/lib/utils/datetime_utility'); var spy; this.ciStatusData.coverage = null; spy = spyOn(this["class"], 'showCICoverage').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); return expect(spy).not.toHaveBeenCalled(); }); it('should not display a notification on the first check after the widget has been created', function() { var spy; spy = spyOn(window, 'notify'); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"] = new window.gl.MergeRequestWidget(this.opts); this["class"].getCIStatus(true); return expect(spy).not.toHaveBeenCalled(); @@ -175,6 +180,7 @@ require('~/lib/utils/datetime_utility'); it('should update the pipeline URL when the pipeline changes', function() { var spy; spy = spyOn(this["class"], 'updatePipelineUrls').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); this.ciStatusData.pipeline += 1; this["class"].getCIStatus(false); @@ -183,6 +189,7 @@ require('~/lib/utils/datetime_utility'); it('should update the commit URL when the sha changes', function() { var spy; spy = spyOn(this["class"], 'updateCommitUrls').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); this.ciStatusData.sha = "9b50b99a"; this["class"].getCIStatus(false);