diff options
Diffstat (limited to 'app')
27 files changed, 144 insertions, 146 deletions
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 73675d300be..9ebbb22e807 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -5,21 +5,28 @@ import './preview_markdown'; window.DropzoneInput = (function() { function DropzoneInput(form) { - var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile, addFileToForm; Dropzone.autoDiscover = false; - divHover = '<div class="div-dropzone-hover"></div>'; - iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; - $attachButton = form.find('.button-attach-file'); - $attachingFileMessage = form.find('.attaching-file-message'); - $cancelButton = form.find('.button-cancel-uploading-files'); - $retryLink = form.find('.retry-uploading-link'); - $uploadProgress = form.find('.uploading-progress'); - $uploadingErrorContainer = form.find('.uploading-error-container'); - $uploadingErrorMessage = form.find('.uploading-error-message'); - $uploadingProgressContainer = form.find('.uploading-progress-container'); - uploadsPath = window.uploads_path || null; - maxFileSize = gon.max_file_size || 10; - formTextarea = form.find('.js-gfm-input'); + const divHover = '<div class="div-dropzone-hover"></div>'; + const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; + const $attachButton = form.find('.button-attach-file'); + const $attachingFileMessage = form.find('.attaching-file-message'); + const $cancelButton = form.find('.button-cancel-uploading-files'); + const $retryLink = form.find('.retry-uploading-link'); + const $uploadProgress = form.find('.uploading-progress'); + const $uploadingErrorContainer = form.find('.uploading-error-container'); + const $uploadingErrorMessage = form.find('.uploading-error-message'); + const $uploadingProgressContainer = form.find('.uploading-progress-container'); + const uploadsPath = window.uploads_path || null; + const maxFileSize = gon.max_file_size || 10; + const formTextarea = form.find('.js-gfm-input'); + let handlePaste; + let pasteText; + let addFileToForm; + let updateAttachingMessage; + let isImage; + let getFilename; + let uploadFile; + formTextarea.wrap('<div class="div-dropzone"></div>'); formTextarea.on('paste', (function(_this) { return function(event) { @@ -28,16 +35,16 @@ window.DropzoneInput = (function() { })(this)); // Add dropzone area to the form. - $mdArea = formTextarea.closest('.md-area'); + const $mdArea = formTextarea.closest('.md-area'); form.setupMarkdownPreview(); - $formDropzone = form.find('.div-dropzone'); + const $formDropzone = form.find('.div-dropzone'); $formDropzone.parent().addClass('div-dropzone-wrapper'); $formDropzone.append(divHover); $formDropzone.find('.div-dropzone-hover').append(iconPaperclip); if (!uploadsPath) return; - dropzone = $formDropzone.dropzone({ + const dropzone = $formDropzone.dropzone({ url: uploadsPath, dictDefaultMessage: '', clickable: true, @@ -117,7 +124,7 @@ window.DropzoneInput = (function() { } }); - child = $(dropzone[0]).children('textarea'); + const child = $(dropzone[0]).children('textarea'); // removeAllFiles(true) stops uploading files (if any) // and remove them from dropzone files queue. @@ -214,6 +221,35 @@ window.DropzoneInput = (function() { return value.first(); }; + const showSpinner = function(e) { + return $uploadingProgressContainer.removeClass('hide'); + }; + + const closeSpinner = function() { + return $uploadingProgressContainer.addClass('hide'); + }; + + const showError = function(message) { + $uploadingErrorContainer.removeClass('hide'); + $uploadingErrorMessage.html(message); + }; + + const closeAlertMessage = function() { + return form.find('.div-dropzone-alert').alert('close'); + }; + + const insertToTextArea = function(filename, url) { + return $(child).val(function(index, val) { + return val.replace(`{{${filename}}}`, url); + }); + }; + + const appendToTextArea = function(url) { + return $(child).val(function(index, val) { + return val + url + "\n"; + }); + }; + uploadFile = function(item, filename) { var formData; formData = new FormData(); @@ -262,35 +298,6 @@ window.DropzoneInput = (function() { messageContainer.text(attachingMessage); }; - insertToTextArea = function(filename, url) { - return $(child).val(function(index, val) { - return val.replace(`{{${filename}}}`, url); - }); - }; - - appendToTextArea = function(url) { - return $(child).val(function(index, val) { - return val + url + "\n"; - }); - }; - - showSpinner = function(e) { - return $uploadingProgressContainer.removeClass('hide'); - }; - - closeSpinner = function() { - return $uploadingProgressContainer.addClass('hide'); - }; - - showError = function(message) { - $uploadingErrorContainer.removeClass('hide'); - $uploadingErrorMessage.html(message); - }; - - closeAlertMessage = function() { - return form.find('.div-dropzone-alert').alert('close'); - }; - form.find('.markdown-selector').click(function(e) { e.preventDefault(); $(this).closest('.gfm-form').find('.div-dropzone').click(); diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index 415e50f32ae..625e53ee9de 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -3,6 +3,7 @@ */ export default { + ABORTED: 0, NO_CONTENT: 204, OK: 200, }; diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index e31cc5fbabe..97666e13ebe 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -81,6 +81,9 @@ export default class Poll { }) .catch((error) => { notificationCallback(false); + if (error.status === httpStatusCodes.ABORTED) { + return; + } errorCallback(error); }); } diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 4ae2b164d2e..06f7af33f94 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -60,7 +60,7 @@ } &:not([href]):hover { - border-color: rgba($avatar-border, .2); + border-color: darken($avatar-border, 10%); } } @@ -99,7 +99,7 @@ .avatar-counter { background-color: $gray-darkest; color: $white-light; - border: 1px solid $border-color; + border: 1px solid $avatar-border; border-radius: 1em; font-family: $regular_font; font-size: 9px; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index f89f2f30443..5e410cbf563 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -205,6 +205,7 @@ @media (max-width: $screen-sm-min) { width: 100%; + min-width: 180px; } &.dropdown-open-left { @@ -288,27 +289,15 @@ padding: 5px 8px; color: $gl-text-color-secondary; } - - .badge { - position: absolute; - right: 8px; - top: 5px; - } } .droplab-dropdown { - .description { - display: inline-block; - white-space: normal; - margin-left: 5px; - } - .dropdown-toggle > i { pointer-events: none; } - li { - padding: $gl-btn-padding $gl-btn-padding 2px; + .dropdown-menu li { + padding: $gl-btn-padding; cursor: pointer; > a, @@ -344,9 +333,25 @@ visibility: visible; } + &.divider { + margin: 0 8px; + padding: 0; + border-top: $gray-darkest; + } + .icon { visibility: hidden; } + + .description { + display: inline-block; + white-space: normal; + margin-left: 5px; + + p { + margin-bottom: 0; + } + } } .icon { @@ -354,12 +359,6 @@ vertical-align: top; padding-top: 2px; } - - .divider { - margin: 0 8px; - padding: 0; - border-top: $gray-darkest; - } } .droplab-dropdown .dropdown-menu, @@ -462,10 +461,6 @@ left: auto; right: 0; margin-top: -5px; - - @media (max-width: $screen-xs-max) { - left: 0; - } } .dropdown-menu-selectable { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 7e4e5fd7f1c..41184907abb 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -275,7 +275,7 @@ } .filtered-search-input-dropdown-menu { - max-height: 215px; + max-height: 225px; max-width: 280px; overflow: auto; @@ -382,10 +382,6 @@ } } -.dropdown-menu .filter-dropdown-item { - padding: 0; -} - @media (max-width: $screen-xs-max) { .issues-details-filters { padding: 0 0 10px; @@ -435,6 +431,7 @@ .fa { width: 15px; + line-height: $line-height-base; } .dropdown-label-box { diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index e59cd0eea82..868e65a8f46 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -236,6 +236,8 @@ ul.content-list { ul.controls { float: right; list-style: none; + display: flex; + align-items: center; .btn { padding: 10px 14px; @@ -259,6 +261,12 @@ ul.controls { } } } + + .issuable-pipeline-broken a, + .issuable-pipeline-status a, + .author_link { + display: flex; + } } ul.indent-list { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 28b2a7cfacd..e71bf04aec7 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -325,7 +325,7 @@ position: absolute; top: 7px; right: 15px; - z-index: 2; + z-index: 300; li.active { font-weight: bold; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 785b09e622f..77b7d901f9a 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -2,6 +2,10 @@ color: $gl-text-color; word-wrap: break-word; + [dir="auto"] { + text-align: initial; + } + a { color: $md-link-color; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 8bd69faf84c..7016208f624 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -379,7 +379,7 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5); * Avatar */ $avatar_radius: 50%; -$avatar-border: rgba(0, 0, 0, .1); +$avatar-border: $border-color; $gl-avatar-size: 40px; /* diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index 82cabefa129..bd9a5d7392d 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -65,7 +65,6 @@ $new-sidebar-width: 220px; .settings-avatar { background-color: $white-light; - transition: background-color 100ms linear; i { font-size: 20px; @@ -73,7 +72,6 @@ $new-sidebar-width: 220px; color: $gl-text-color-secondary; text-align: center; align-self: center; - transition: color 100ms linear; } } @@ -90,6 +88,7 @@ $new-sidebar-width: 220px; box-shadow: inset -2px 0 0 $border-color; a { + transition: none; text-decoration: none; } @@ -177,7 +176,6 @@ $new-sidebar-width: 220px; color: $hover-color; .badge { - transition: background-color 100ms linear, color 100ms linear; background-color: $indigo-500; color: $hover-color; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 56a4b53ed61..aa04e490649 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -346,13 +346,9 @@ display: none; } - .avatar:hover, - .avatar-counter:hover { - border-color: $issuable-sidebar-color; - } - .avatar-counter:hover { color: $issuable-sidebar-color; + border-color: $issuable-sidebar-color; } .btn-clipboard { @@ -813,8 +809,6 @@ } .description { - margin-bottom: 10px; - .text { margin: 0; } diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 13f03e7e63e..0ac9da2ff0f 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -266,7 +266,7 @@ class Projects::IssuesController < Projects::ApplicationController if action_name == 'new' redirect_to external.new_issue_path else - redirect_to external.project_path + redirect_to external.issue_tracker_path end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 26d11f9ab46..9a8d296d514 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -195,7 +195,7 @@ module ProjectsHelper controller.controller_name, controller.action_name, current_application_settings.cache_key, - 'v2.4' + 'v2.5' ] key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? diff --git a/app/models/commit.rb b/app/models/commit.rb index c7f62617c4c..1e19f00106a 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -1,5 +1,6 @@ class Commit extend ActiveModel::Naming + extend Gitlab::Cache::RequestCache include ActiveModel::Conversion include Noteable @@ -169,19 +170,9 @@ class Commit end def author - if RequestStore.active? - key = "commit_author:#{author_email.downcase}" - # nil is a valid value since no author may exist in the system - if RequestStore.store.key?(key) - @author = RequestStore.store[key] - else - @author = find_author_by_any_email - RequestStore.store[key] = @author - end - else - @author ||= find_author_by_any_email - end + User.find_by_any_email(author_email.downcase) end + request_cache(:author) { author_email.downcase } def committer @committer ||= User.find_by_any_email(committer_email.downcase) @@ -322,7 +313,7 @@ class Commit def raw_diffs(*args) if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs) - Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args) + Gitlab::GitalyClient::CommitService.new(project.repository).diff_from_parent(self, *args) else raw.diffs(*args) end @@ -331,7 +322,7 @@ class Commit def raw_deltas @deltas ||= Gitlab::GitalyClient.migrate(:commit_deltas) do |is_enabled| if is_enabled - Gitlab::GitalyClient::Commit.new(project.repository).commit_deltas(self) + Gitlab::GitalyClient::CommitService.new(project.repository).commit_deltas(self) else raw.deltas end @@ -368,10 +359,6 @@ class Commit end end - def find_author_by_any_email - User.find_by_any_email(author_email.downcase) - end - def repo_changes changes = { added: [], modified: [], removed: [] } diff --git a/app/models/note.rb b/app/models/note.rb index 3d39047d32f..d0e3bc0bfed 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -190,7 +190,7 @@ class Note < ActiveRecord::Base # override to return commits, which are not active record def noteable if for_commit? - project.commit(commit_id) + @commit ||= project.commit(commit_id) else super end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 420102875a5..88c428b4aae 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -23,7 +23,7 @@ class GitlabIssueTrackerService < IssueTrackerService project_issue_url(project, id: iid) end - def project_path + def issue_tracker_path project_issues_path(project) end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 1fa4cd4db30..6d6a3ae3647 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -20,8 +20,8 @@ class IssueTrackerService < Service self.issues_url.gsub(':id', iid.to_s) end - def project_path - read_attribute(:project_url) + def issue_tracker_path + project_url end def new_issue_path diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 62f7c057c5b..dee99bbb859 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -59,21 +59,21 @@ class KubernetesService < DeploymentService def fields [ { type: 'text', - name: 'namespace', - title: 'Kubernetes namespace', - placeholder: namespace_placeholder }, - { type: 'text', name: 'api_url', title: 'API URL', placeholder: 'Kubernetes API URL, like https://kube.example.com/' }, - { type: 'text', - name: 'token', - title: 'Service token', - placeholder: 'Service token' }, { type: 'textarea', name: 'ca_pem', - title: 'Custom CA bundle', - placeholder: 'Certificate Authority bundle (PEM format)' } + title: 'CA Certificate', + placeholder: 'Certificate Authority bundle (PEM format)' }, + { type: 'text', + name: 'namespace', + title: 'Project namespace (optional/unique)', + placeholder: namespace_placeholder }, + { type: 'text', + name: 'token', + title: 'Token', + placeholder: 'Service token' } ] end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index a886efc1360..386822d3ff6 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -3,9 +3,13 @@ module Ci condition(:protected_action) do next false unless @subject.action? - !::Gitlab::UserAccess - .new(@user, project: @subject.project) - .can_merge_to_branch?(@subject.ref) + access = ::Gitlab::UserAccess.new(@user, project: @subject.project) + + if @subject.tag? + !access.can_create_tag?(@subject.ref) + else + !access.can_merge_to_branch?(@subject.ref) + end end rule { protected_action }.prevent :update_build diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 4f35255fb53..273386776fa 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -135,7 +135,7 @@ module Ci end def pipeline_created_counter - @pipeline_created_counter ||= Gitlab::Metrics.counter(:pipelines_created_count, "Pipelines created count") + @pipeline_created_counter ||= Gitlab::Metrics.counter(:pipelines_created_total, "Counter of pipelines created") end end end diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb index c92f070601c..a02eee4961b 100644 --- a/app/services/metrics_service.rb +++ b/app/services/metrics_service.rb @@ -31,6 +31,6 @@ class MetricsService end def multiprocess_metrics_path - @multiprocess_metrics_path ||= Rails.root.join(ENV['prometheus_multiproc_dir']).freeze + ::Prometheus::Client.configuration.multiprocess_files_dir end end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 0da7a025591..05a2091633a 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -16,7 +16,7 @@ class GitlabUploader < CarrierWave::Uploader::Base def self.base_dir return root_dir unless file_storage? - File.join(root_dir, 'system') + File.join(root_dir, '-', 'system') end def self.file_storage? diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index ac222ad8c82..be7d27df2a0 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -42,18 +42,18 @@ .key = icon('arrow-up', 'aria-label' => 'hidden') I + %span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:issues)) %span Issues - .badge= number_with_delimiter(assigned_issuables_count(:issues)) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do .shortcut-mappings .key = icon('arrow-up', 'aria-label' => 'hidden') M + %span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:merge_requests)) %span Merge Requests - .badge= number_with_delimiter(assigned_issuables_count(:merge_requests)) = nav_link(controller: 'dashboard/snippets') do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do .shortcut-mappings diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml index c80308ed0de..6e0c45739f1 100644 --- a/app/views/layouts/nav/_new_group_sidebar.html.haml +++ b/app/views/layouts/nav/_new_group_sidebar.html.haml @@ -6,15 +6,15 @@ = @group.name %ul.sidebar-top-level-items = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Home' do + = link_to group_path(@group), title: 'About group' do %span - Group + About %ul.sidebar-sub-level-items = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group Home' do + = link_to group_path(@group), title: 'Group details' do %span - Home + Details = nav_link(path: 'groups#activity') do = link_to activity_group_path(@group), title: 'Activity' do diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index 7c9822c5a6a..882123c0b0a 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -7,14 +7,14 @@ = @project.name %ul.sidebar-top-level-items = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do + = link_to project_path(@project), title: 'About project', class: 'shortcuts-project' do %span - Project + About %ul.sidebar-sub-level-items = nav_link(path: 'projects#show') do - = link_to project_path(@project), title: _('Project home'), class: 'shortcuts-project' do - %span= _('Home') + = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do + %span= _('Details') = nav_link(path: 'projects#activity') do = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 576e5b385af..a33743c2f57 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -5,12 +5,6 @@ .tree-holder .nav-block - .tree-controls - = link_to download_project_job_artifacts_path(@project, @build), - rel: 'nofollow', download: '', class: 'btn btn-default download' do - = icon('download') - Download artifacts archive - %ul.breadcrumb.repo-breadcrumb %li = link_to 'Artifacts', browse_project_job_artifacts_path(@project, @build) @@ -18,6 +12,12 @@ %li = link_to truncate(title, length: 40), browse_project_job_artifacts_path(@project, @build, path) + .tree-controls + = link_to download_project_job_artifacts_path(@project, @build), + rel: 'nofollow', download: '', class: 'btn btn-default download' do + = icon('download') + Download artifacts archive + .tree-content-holder %table.table.tree-table %thead |