diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index 029fd6a67d4..efba6fc1aff 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -1,23 +1,36 @@ import $ from 'jquery'; import { parseQueryStringIntoObject } from '~/lib/utils/common_utils'; import axios from '~/lib/utils/axios_utils'; -import flash from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; export default class GpgBadges { static fetch() { - const badges = $('.js-loading-gpg-badge'); const tag = $('.js-signature-container'); + if (tag.length === 0) { + return Promise.resolve(); + } + + const badges = $('.js-loading-gpg-badge'); badges.html(''); + const displayError = () => createFlash(__('An error occurred while loading commit signatures')); + + const endpoint = tag.data('signaturesPath'); + if (!endpoint) { + displayError(); + return Promise.reject(new Error('Missing commit signatures endpoint!')); + } + const params = parseQueryStringIntoObject(tag.serialize()); - return axios.get(tag.data('signaturesPath'), { params }) - .then(({ data }) => { - data.signatures.forEach((signature) => { - badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); - }); - }) - .catch(() => flash(__('An error occurred while loading commits'))); + return axios + .get(endpoint, { params }) + .then(({ data }) => { + data.signatures.forEach(signature => { + badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); + }); + }) + .catch(displayError); } } diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index 85c6862d629..84e5bb3c46e 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import BlobViewer from '~/blob/viewer/index'; import initBlob from '~/pages/projects/init_blob'; +import GpgBadges from '~/gpg_badges'; document.addEventListener('DOMContentLoaded', () => { new BlobViewer(); // eslint-disable-line no-new @@ -26,4 +27,6 @@ document.addEventListener('DOMContentLoaded', () => { }, }); } + + GpgBadges.fetch(); }); diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 3b0f0f960b8..d2dc0c4570e 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -7,6 +7,7 @@ import TreeView from '~/tree'; import BlobViewer from '~/blob/viewer/index'; import Activities from '~/activities'; import { ajaxGet } from '~/lib/utils/common_utils'; +import GpgBadges from '~/gpg_badges'; import Star from '../../../star'; import notificationsDropdown from '../../../notifications_dropdown'; @@ -38,4 +39,6 @@ document.addEventListener('DOMContentLoaded', () => { $(treeSlider).waitForImages(() => { ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); + + GpgBadges.fetch(); }); diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js index 7ad082a5e61..33d69d891d8 100644 --- a/app/assets/javascripts/pages/projects/tree/show/index.js +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Vue from 'vue'; import initBlob from '~/blob_edit/blob_bundle'; import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; +import GpgBadges from '~/gpg_badges'; import TreeView from '../../../../tree'; import ShortcutsNavigation from '../../../../shortcuts_navigation'; import BlobViewer from '../../../../blob/viewer'; @@ -14,7 +15,8 @@ document.addEventListener('DOMContentLoaded', () => { new BlobViewer(); // eslint-disable-line no-new new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new $('#tree-slider').waitForImages(() => - ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath), + ); initBlob(); const commitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status'); @@ -36,4 +38,6 @@ document.addEventListener('DOMContentLoaded', () => { }, }); } + + GpgBadges.fetch(); }); diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index a4c7c143e56..1c1e17563a1 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -1,27 +1,27 @@ diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 523fcb05a87..646cedd79ed 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -294,6 +294,10 @@ .btn-clipboard { border: 0; padding: 0 5px; + + svg { + top: auto; + } } .input-group-prepend, diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index f75be4e01cd..63585e26022 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -205,7 +205,7 @@ > .ci-status-link, > .btn, > .commit-sha-group { - margin-left: $gl-padding-8; + margin-left: $gl-padding; } } @@ -235,10 +235,6 @@ fill: $gl-text-color-secondary; } - .fa-clipboard { - color: $gl-text-color-secondary; - } - :first-child { border-bottom-left-radius: $border-radius-default; border-top-left-radius: $border-radius-default; diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 3605d6a3c95..0171a880164 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -51,7 +51,7 @@ module ButtonHelper } content_tag :button, button_attributes do - concat(icon('clipboard', 'aria-hidden': 'true')) unless hide_button_icon + concat(sprite_icon('duplicate')) unless hide_button_icon concat(button_text) end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index f49b5c7b51a..330959e536d 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -56,7 +56,7 @@ module CiStatusHelper status.humanize end - def ci_icon_for_status(status) + def ci_icon_for_status(status, size: 16) if detailed_status?(status) return sprite_icon(status.icon) end @@ -85,7 +85,7 @@ module CiStatusHelper 'status_canceled' end - sprite_icon(icon_name, size: 16) + sprite_icon(icon_name, size: size) end def pipeline_status_cache_key(pipeline_status) @@ -111,7 +111,8 @@ module CiStatusHelper 'commit', commit.status(ref), path, - tooltip_placement: tooltip_placement) + tooltip_placement: tooltip_placement, + icon_size: 24) end def render_pipeline_status(pipeline, tooltip_placement: 'left') @@ -125,16 +126,16 @@ module CiStatusHelper Ci::Runner.instance_type.blank? end - def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body') + def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16) klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" title = "#{type.titleize}: #{ci_label_for_status(status)}" data = { toggle: 'tooltip', placement: tooltip_placement, container: container } if path - link_to ci_icon_for_status(status), path, + link_to ci_icon_for_status(status, size: icon_size), path, class: klass, title: title, data: data else - content_tag :span, ci_icon_for_status(status), + content_tag :span, ci_icon_for_status(status, size: icon_size), class: klass, title: title, data: data end end diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index efb8175398b..5edab38bd64 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -3,6 +3,8 @@ - page_title @blob.path, @ref +.js-signature-container{ data: { 'signatures-path': namespace_project_signatures_path } } + %div{ class: container_class } = render 'projects/last_push' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index e28accd5b43..803ecca48f7 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -8,6 +8,10 @@ = render partial: 'flash_messages', locals: { project: @project } +- if @project.repository_exists? && !@project.empty_repo? + - signatures_path = namespace_project_signatures_path(project_id: @project.path, id: @project.default_branch) + .js-signature-container{ data: { 'signatures-path': signatures_path } } + %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } = render "projects/last_push" diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 3b4057e56d0..ace8120eeff 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -1,11 +1,14 @@ - @no_container = true - breadcrumb_title _("Repository") - @content_class = "limit-container-width" unless fluid_layout +- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.path, project_id: @project.path, id: @ref) - page_title @path.presence || _("Files"), @ref = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") +.js-signature-container{ data: { 'signatures-path': signatures_path } } + %div{ class: [(container_class), ("limit-container-width" unless fluid_layout)] } = render 'projects/last_push' = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id) diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml index 37747faed62..ddc089b0bd7 100644 --- a/app/views/sherlock/queries/_general.html.haml +++ b/app/views/sherlock/queries/_general.html.haml @@ -27,7 +27,7 @@ .card-header .float-right %button.js-clipboard-trigger.btn.btn-sm{ title: t('sherlock.copy_to_clipboard'), type: :button } - %i.fa.fa-clipboard + = sprite_icon('duplicate') %pre.hidden = @query.formatted_query %strong @@ -42,7 +42,7 @@ .card-header .float-right %button.js-clipboard-trigger.btn.btn-sm{ title: t('sherlock.copy_to_clipboard'), type: :button } - %i.fa.fa-clipboard + = sprite_icon('duplicate') %pre.hidden = @query.explain %strong diff --git a/changelogs/unreleased/winh-tree-view-gpg.yml b/changelogs/unreleased/winh-tree-view-gpg.yml new file mode 100644 index 00000000000..84d63814a47 --- /dev/null +++ b/changelogs/unreleased/winh-tree-view-gpg.yml @@ -0,0 +1,5 @@ +--- +title: Display GPG status on repository and blob pages +merge_request: 20524 +author: +type: changed diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 75b88a2cb2f..09a35b5da07 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -465,7 +465,7 @@ msgstr "" msgid "An error occurred while importing project: ${details}" msgstr "" -msgid "An error occurred while loading commits" +msgid "An error occurred while loading commit signatures" msgstr "" msgid "An error occurred while loading diff" diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb index fee8df10129..630f3eff258 100644 --- a/spec/helpers/button_helper_spec.rb +++ b/spec/helpers/button_helper_spec.rb @@ -121,6 +121,8 @@ describe ButtonHelper do end describe 'clipboard_button' do + include IconsHelper + let(:user) { create(:user) } let(:project) { build_stubbed(:project) } @@ -145,7 +147,7 @@ describe ButtonHelper do expect(element.attr('data-clipboard-text')).to eq(nil) expect(element.inner_text).to eq("") - expect(element).to have_selector('.fa.fa-clipboard') + expect(element.to_html).to include sprite_icon('duplicate') end end @@ -178,7 +180,7 @@ describe ButtonHelper do context 'with `hide_button_icon` attribute provided' do it 'shows copy to clipboard button without tooltip support' do - expect(element(hide_button_icon: true)).not_to have_selector('.fa.fa-clipboard') + expect(element(hide_button_icon: true).to_html).not_to include sprite_icon('duplicate') end end end diff --git a/spec/javascripts/gpg_badges_spec.js b/spec/javascripts/gpg_badges_spec.js index 97c771dcfd3..78330dd9633 100644 --- a/spec/javascripts/gpg_badges_spec.js +++ b/spec/javascripts/gpg_badges_spec.js @@ -1,23 +1,27 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import GpgBadges from '~/gpg_badges'; +import { TEST_HOST } from 'spec/test_constants'; describe('GpgBadges', () => { let mock; const dummyCommitSha = 'n0m0rec0ffee'; const dummyBadgeHtml = 'dummy html'; const dummyResponse = { - signatures: [{ - commit_sha: dummyCommitSha, - html: dummyBadgeHtml, - }], + signatures: [ + { + commit_sha: dummyCommitSha, + html: dummyBadgeHtml, + }, + ], }; + const dummyUrl = `${TEST_HOST}/dummy/signatures`; beforeEach(() => { mock = new MockAdapter(axios); setFixtures(`
@@ -32,25 +36,55 @@ describe('GpgBadges', () => { mock.restore(); }); - it('displays a loading spinner', (done) => { - mock.onGet('/hello').reply(200); + it('does not make a request if there is no container element', done => { + setFixtures(''); + spyOn(axios, 'get'); - GpgBadges.fetch().then(() => { - expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null); - const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin'); - expect(spinners.length).toBe(1); - done(); - }).catch(done.fail); + GpgBadges.fetch() + .then(() => { + expect(axios.get).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); }); - it('replaces the loading spinner', (done) => { - mock.onGet('/hello').reply(200, dummyResponse); + it('throws an error if the endpoint is missing', done => { + setFixtures('
'); + spyOn(axios, 'get'); - GpgBadges.fetch().then(() => { - expect(document.querySelector('.js-loading-gpg-badge')).toBe(null); - const parentContainer = document.querySelector('.parent-container'); - expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml); - done(); - }).catch(done.fail); + GpgBadges.fetch() + .then(() => done.fail('Expected error to be thrown')) + .catch(error => { + expect(error.message).toBe('Missing commit signatures endpoint!'); + expect(axios.get).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('displays a loading spinner', done => { + mock.onGet(dummyUrl).replyOnce(200); + + GpgBadges.fetch() + .then(() => { + expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null); + const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin'); + expect(spinners.length).toBe(1); + done(); + }) + .catch(done.fail); + }); + + it('replaces the loading spinner', done => { + mock.onGet(dummyUrl).replyOnce(200, dummyResponse); + + GpgBadges.fetch() + .then(() => { + expect(document.querySelector('.js-loading-gpg-badge')).toBe(null); + const parentContainer = document.querySelector('.parent-container'); + expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml); + done(); + }) + .catch(done.fail); }); }); diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js index 97f0fbb04db..e135690349e 100644 --- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js +++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js @@ -21,7 +21,7 @@ describe('clipboard button', () => { it('renders a button for clipboard', () => { expect(vm.$el.tagName).toEqual('BUTTON'); expect(vm.$el.getAttribute('data-clipboard-text')).toEqual('copy me'); - expect(vm.$el.querySelector('i').className).toEqual('fa fa-clipboard'); + expect(vm.$el).toHaveSpriteIcon('duplicate'); }); it('should have a tooltip with default values', () => {