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);