Commit 24eaba35 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-11-08

# Conflicts:
#	app/assets/javascripts/boards/stores/boards_store.js
#	app/assets/javascripts/dispatcher.js
#	app/assets/javascripts/milestone_select.js
#	app/assets/javascripts/repo/components/repo_file.vue
#	app/assets/javascripts/repo/components/repo_loading_file.vue
#	app/assets/stylesheets/framework/avatar.scss
#	app/assets/stylesheets/framework/modal.scss
#	app/assets/stylesheets/pages/note_form.scss
#	app/controllers/projects/issues_controller.rb
#	app/controllers/projects/refs_controller.rb
#	app/models/merge_request.rb
#	app/models/repository.rb
#	config/initializers/8_metrics.rb
#	config/routes/group.rb
#	db/schema.rb
#	doc/api/merge_requests.md
#	lib/gitlab/checks/change_access.rb
#	lib/gitlab/metrics/transaction.rb
#	lib/gitlab/path_regex.rb
#	lib/gitlab/usage_data.rb
#	spec/lib/gitlab/checks/change_access_spec.rb
#	spec/lib/gitlab/metrics/web_transaction_spec.rb
#	spec/lib/gitlab/usage_data_spec.rb
#	spec/models/merge_request_spec.rb
#	spec/requests/api/groups_spec.rb
#	spec/routing/group_routing_spec.rb
#	spec/support/kubernetes_helpers.rb

[ci skip]
parents 747646b7 f2f58a60
{"iconCount":164,"spriteSize":72823,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","dashboard","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} {"iconCount":173,"spriteSize":75815,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file \ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
<svg xmlns="http://www.w3.org/2000/svg" width="492.509" height="453.68" viewBox="0 0 492.50943 453.67966"><g fill="none" fill-rule="evenodd"><path d="M491.589 259.398l-27.559-84.814L409.413 6.486c-2.81-8.648-15.045-8.648-17.856 0l-54.619 168.098H155.572L100.952 6.486c-2.81-8.648-15.046-8.648-17.856 0L28.478 174.584.921 259.398a18.775 18.775 0 0 0 6.82 20.992l238.513 173.29L484.77 280.39a18.777 18.777 0 0 0 6.82-20.992" fill="#fc6d26"/><path d="M246.255 453.68l90.684-279.096H155.57z" fill="#e24329"/><path d="M246.255 453.68L155.57 174.583H28.479z" fill="#fc6d26"/><path d="M28.479 174.584L.92 259.4a18.773 18.773 0 0 0 6.821 20.99l238.514 173.29z" fill="#fca326"/><path d="M28.479 174.584H155.57L100.952 6.487c-2.81-8.65-15.047-8.65-17.856 0z" fill="#e24329"/><path d="M246.255 453.68l90.684-279.096H464.03z" fill="#fc6d26"/><path d="M464.03 174.584l27.56 84.815a18.773 18.773 0 0 1-6.822 20.99L246.255 453.68z" fill="#fca326"/><path d="M464.03 174.584H336.94L391.557 6.487c2.811-8.65 15.047-8.65 17.856 0z" fill="#e24329"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="430" height="220" viewBox="0 0 430 220"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M189.8 182l2.4-12H114c-5.523 0-10-4.477-10-10V34c0-5.523 4.477-10 10-10h200c5.523 0 10 4.477 10 10v126c0 5.523-4.477 10-10 10h-78.2l2.4 12h22.52a9.651 9.651 0 0 1 9.28 7 5.491 5.491 0 0 1-5.28 7H164.159a5.787 5.787 0 0 1-5.659-7 8.855 8.855 0 0 1 8.659-7H189.8zM114 28a6 6 0 0 0-6 6v126a6 6 0 0 0 6 6h200a6 6 0 0 0 6-6V34a6 6 0 0 0-6-6H114zm5 6h190a5 5 0 0 1 5 5v116a5 5 0 0 1-5 5H119a5 5 0 0 1-5-5V39a5 5 0 0 1 5-5zm0 4a1 1 0 0 0-1 1v116a1 1 0 0 0 1 1h190a1 1 0 0 0 1-1V39a1 1 0 0 0-1-1H119zm112.72 132h-35.44l-2.4 12h40.24l-2.4-12zm-64.561 16c-2.29 0-4.268 1.6-4.748 3.838A1.787 1.787 0 0 0 164.16 192h100.56a1.491 1.491 0 0 0 1.435-1.901A5.651 5.651 0 0 0 260.72 186h-93.561z"/><path fill="#FEF0E8" d="M177.965 99H194a2 2 0 1 1 0 4h-16.322c-1.374 6.29-6.976 11-13.678 11-6.702 0-12.304-4.71-13.678-11h-3.365l-7.395 9.249a2 2 0 0 1-3.049.089L128.11 103h-5.844a2 2 0 1 1 0-4H129a2 2 0 0 1 1.487.662l7.423 8.248 6.523-8.159a2 2 0 0 1 1.562-.751h4.04c.513-7.265 6.57-13 13.965-13 7.396 0 13.452 5.735 13.965 13zM164 110c5.523 0 10-4.477 10-10s-4.477-10-10-10-10 4.477-10 10 4.477 10 10 10z"/><path fill="#EFEDF8" d="M273.847 103c-.962 6.23-6.347 11-12.847 11-6.5 0-11.885-4.77-12.847-11H232a2 2 0 0 1 0-4h16.153c.962-6.23 6.347-11 12.847-11 6.5 0 11.885 4.77 12.847 11h3.998l8.404-9.338a2 2 0 0 1 3.048.09L296.692 99H305a2 2 0 0 1 0 4h-9.27a2 2 0 0 1-1.562-.751l-6.523-8.16-7.423 8.249a2 2 0 0 1-1.487.662h-4.888zM261 110a9 9 0 1 0 0-18 9 9 0 0 0 0 18z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M213 119c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19zm0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15z"/><path fill="#FC6D26" d="M211.586 101.828L208.757 99a2 2 0 1 0-2.828 2.828l4.243 4.243c.39.39.902.586 1.414.586.512 0 1.023-.195 1.414-.586L220.071 99a2 2 0 1 0-2.828-2.828l-5.657 5.656z"/><path fill="#FDC4A8" d="M162.95 101.07l-1.768-1.767a1.5 1.5 0 0 0-2.121 2.121l2.828 2.829c.293.293.677.439 1.06.439.385 0 .769-.146 1.062-.44l4.242-4.242a1.5 1.5 0 1 0-2.121-2.121l-3.182 3.182z"/><path fill="#6B4FBB" d="M256.39 104.841A6 6 0 1 0 261 95v6l-4.61 3.841z"/><path fill="#FEF0E8" fill-rule="nonzero" d="M99 99h-5a2 2 0 1 0 0 4h5a2 2 0 1 0 0-4zm-16 0h-5a2 2 0 1 0 0 4h5a2 2 0 1 0 0-4zm-14.384-.078l-3.643-3.425a2 2 0 1 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zm-11.657-10.96l-3.642-3.425a2 2 0 1 0-2.74 2.914l3.642 3.425a2 2 0 0 0 2.74-2.914zm-11.656-10.96l-3.643-3.425a2 2 0 0 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zm-14.367-3.885l-3.593 3.477a2 2 0 0 0 2.782 2.875l3.593-3.477a2 2 0 0 0-2.782-2.875zM19.44 84.244l-3.593 3.477a2 2 0 1 0 2.781 2.874l3.593-3.477a2 2 0 0 0-2.781-2.874zM7.94 95.371l-3.593 3.477a2 2 0 1 0 2.782 2.874l3.593-3.477a2 2 0 1 0-2.782-2.874z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M423.611 99.56l-3.598 3.472a2 2 0 0 0 2.777 2.879l3.599-3.472a2 2 0 0 0-2.778-2.878zm-11.514 11.11l-3.598 3.472a2 2 0 0 0 2.777 2.878l3.598-3.471a2 2 0 0 0-2.777-2.879zm-11.514 11.11l-3.599 3.471a2 2 0 1 0 2.778 2.879l3.598-3.472a2 2 0 1 0-2.777-2.879zm-8.799 4.48l-3.642-3.426a2 2 0 0 0-2.74 2.915l3.642 3.425a2 2 0 0 0 2.74-2.915zm-11.656-10.96l-3.643-3.426a2 2 0 1 0-2.74 2.914l3.643 3.426a2 2 0 1 0 2.74-2.915zm-11.657-10.96l-3.643-3.426a2 2 0 1 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zM353.001 99h-5a2 2 0 1 0 0 4h5a2 2 0 0 0 0-4zm-16 0h-5a2 2 0 1 0 0 4h5a2 2 0 0 0 0-4z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 121.94154 121.84154" width="121.942" height="121.842"><style id="style200">.st0{fill:#ecb32d}.st1{fill:#63c1a0}.st2{fill:#e01a59}.st3{fill:#331433}.st4{fill:#d62027}.st5{fill:#89d3df}.st6{fill:#258b74}.st7{fill:#819c3c}</style><path class="st0" d="M79.03 7.511c-1.9-5.7-8-8.8-13.7-7-5.7 1.9-8.8 8-7 13.7l28.1 86.4c1.9 5.3 7.7 8.3 13.2 6.7 5.8-1.7 9.3-7.8 7.4-13.4 0-.2-28-86.4-28-86.4z" id="path202" fill="#ecb32d"/><path class="st1" d="M35.53 21.611c-1.9-5.7-8-8.8-13.7-7-5.7 1.9-8.8 8-7 13.7l28.1 86.4c1.9 5.3 7.7 8.3 13.2 6.7 5.8-1.7 9.3-7.8 7.4-13.4 0-.2-28-86.4-28-86.4z" id="path204" fill="#63c1a0"/><path class="st2" d="M114.43 79.011c5.7-1.9 8.8-8 7-13.7-1.9-5.7-8-8.8-13.7-7l-86.5 28.2c-5.3 1.9-8.3 7.7-6.7 13.2 1.7 5.8 7.8 9.3 13.4 7.4.2 0 86.5-28.1 86.5-28.1z" id="path206" fill="#e01a59"/><path class="st3" d="M39.23 103.511c5.6-1.8 12.9-4.2 20.7-6.7-1.8-5.6-4.2-12.9-6.7-20.7l-20.7 6.7z" id="path208" fill="#331433"/><path class="st4" d="M82.83 89.311c7.8-2.5 15.1-4.9 20.7-6.7-1.8-5.6-4.2-12.9-6.7-20.7l-20.7 6.7z" id="path210" fill="#d62027"/><path class="st5" d="M100.23 35.511c5.7-1.9 8.8-8 7-13.7-1.9-5.7-8-8.8-13.7-7l-86.4 28.1c-5.3 1.9-8.3 7.7-6.7 13.2 1.7 5.8 7.8 9.3 13.4 7.4.2 0 86.4-28 86.4-28z" id="path212" fill="#89d3df"/><path class="st6" d="M25.13 59.911c5.6-1.8 12.9-4.2 20.7-6.7-2.5-7.8-4.9-15.1-6.7-20.7l-20.7 6.7z" id="path214" fill="#258b74"/><path class="st7" d="M68.63 45.811c7.8-2.5 15.1-4.9 20.7-6.7-2.5-7.8-4.9-15.1-6.7-20.7l-20.7 6.7z" id="path216" fill="#819c3c"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="412" height="260" viewBox="0 0 412 260" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M6.447.894L12 12H0L5.553.894a.5.5 0 0 1 .894 0z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#FEF0E8" fill-rule="nonzero" d="M338 50.287C322.695 41.45 303.124 46.694 294.287 62c-8.836 15.305-3.592 34.876 11.713 43.712 15.306 8.837 34.877 3.593 43.713-11.712 8.837-15.306 3.593-34.877-11.713-43.713zm2-3.464C357.22 56.763 363.118 78.78 353.177 96c-9.941 17.218-31.958 23.118-49.177 13.176-17.218-9.94-23.118-31.958-13.177-49.176C300.764 42.78 322.782 36.88 340 46.823z"/><g transform="rotate(-150 171.003 8.53)"><path fill="#FC6D26" fill-rule="nonzero" d="M4 16v25a2 2 0 1 0 4 0V16H4zm8-4v29a6 6 0 1 1-12 0V12h12z"/><use fill="#D8D8D8" xlink:href="#a"/><path stroke="#FDC4A8" stroke-width="4" d="M6 4.472L3.236 10h5.528L6 4.472z"/><path fill="#FC6D26" d="M9 6L6.447.894a.5.5 0 0 0-.894 0L3 6c.836.628 1.874 1 3 1a4.978 4.978 0 0 0 3-1z"/></g><path fill="#F9F9F9" d="M263.116 237.116A10.002 10.002 0 0 1 254 243h-86c-11.046 0-20-8.954-20-20V121c0-4.056 2.414-7.547 5.884-9.116A9.964 9.964 0 0 0 153 116v106c0 8.837 7.163 16 16 16h90c1.467 0 2.86-.316 4.116-.884z"/><path fill="#EEE" fill-rule="nonzero" d="M214.5 106H163c-5.523 0-10 4.477-10 10v106c0 8.837 7.163 16 16 16h90c5.523 0 10-4.477 10-10v-17.999a10.036 10.036 0 0 1-4 3.167V228a6 6 0 0 1-6 6h-90c-6.627 0-12-5.373-12-12V116a6 6 0 0 1 6-6h7v-4h44.5z"/><path fill="#EEE" fill-rule="nonzero" d="M260 218.268V214h-90a6 6 0 0 0 0 12h86a4 4 0 0 0 4-4v-.268a1.99 1.99 0 0 1-1 .268h-50a2 2 0 0 1 0-4h50c.364 0 .706.097 1 .268zM170 210h90.5a3.5 3.5 0 0 1 3.5 3.5v8.5a8 8 0 0 1-8 8h-86c-5.523 0-10-4.477-10-10s4.477-10 10-10z"/><path fill="#EEE" fill-rule="nonzero" d="M174 110v100h87a6 6 0 0 0 6-6v-88a6 6 0 0 0-6-6h-87zm-4-4h91c5.523 0 10 4.477 10 10v88c0 5.523-4.477 10-10 10h-91V106z"/><path fill="#EFEDF8" d="M230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M236.182 129.207a5.5 5.5 0 0 1 6.102.04l7.716 5.219V105a2 2 0 0 0-2-2h-18a2 2 0 0 0-2 2v29.584l8.182-5.377zM230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><g fill-rule="nonzero"><path fill="#EFEDF8" d="M156 74c14.912 0 27-12.088 27-27s-12.088-27-27-27-27 12.088-27 27 12.088 27 27 27zm0 4c-17.12 0-31-13.88-31-31s13.88-31 31-31 31 13.88 31 31-13.88 31-31 31z"/><path fill="#6B4FBB" d="M147.535 44.916l-.116 1.086a8.446 8.446 0 0 0 .093 2.44l.2 1.08-2.262 1.202a.495.495 0 0 0-.213.678l.941 1.77c.128.239.434.332.68.201l2.25-1.196.785.775a8.544 8.544 0 0 0 1.967 1.45l.975.522-.486 2.5a.495.495 0 0 0 .392.59l1.968.383a.504.504 0 0 0 .585-.401l.489-2.515 1.086-.13a8.584 8.584 0 0 0 2.363-.633l1.005-.43 1.68 1.933a.495.495 0 0 0 .708.055l1.513-1.315a.504.504 0 0 0 .044-.708l-1.67-1.922.583-.94c.431-.696.761-1.45.978-2.239l.292-1.063 2.547-.089a.495.495 0 0 0 .488-.515l-.07-2.003a.504.504 0 0 0-.523-.48l-2.56.09-.367-1.037a8.446 8.446 0 0 0-1.139-2.159l-.644-.882 1.509-2.076a.495.495 0 0 0-.106-.702l-1.621-1.178a.504.504 0 0 0-.7.116l-1.494 2.057-1.05-.362a8.459 8.459 0 0 0-2.398-.455l-1.1-.047-.66-2.466a.495.495 0 0 0-.613-.36l-1.936.519a.504.504 0 0 0-.35.617l.661 2.466-.93.59a8.459 8.459 0 0 0-1.848 1.594l-.728.838-2.322-1.034a.495.495 0 0 0-.665.25l-.815 1.83a.504.504 0 0 0 .26.661l2.344 1.044zm-3.565 1.697a3.504 3.504 0 0 1-1.78-4.622l.815-1.83a3.495 3.495 0 0 1 4.626-1.77l.346.154c.259-.245.529-.477.81-.697l-.106-.394a3.504 3.504 0 0 1 2.471-4.292l1.936-.519a3.495 3.495 0 0 1 4.286 2.481l.106.395c.353.05.703.116 1.05.198l.222-.306a3.504 3.504 0 0 1 4.89-.78l1.622 1.178a3.495 3.495 0 0 1 .769 4.892l-.258.355c.184.312.354.633.508.962l.42-.014a3.504 3.504 0 0 1 3.625 3.373l.07 2.003a3.495 3.495 0 0 1-3.382 3.618l-.4.014c-.127.332-.27.659-.426.978l.256.294a3.504 3.504 0 0 1-.34 4.941l-1.512 1.315a3.495 3.495 0 0 1-4.94-.351l-.283-.325a11.669 11.669 0 0 1-1.05.28l-.082.424a3.504 3.504 0 0 1-4.103 2.774l-1.967-.382a3.495 3.495 0 0 1-2.765-4.11l.075-.383a11.547 11.547 0 0 1-.858-.633l-.354.188a3.504 3.504 0 0 1-4.738-1.442l-.94-1.77a3.495 3.495 0 0 1 1.453-4.734l.37-.197a11.436 11.436 0 0 1-.041-1.088l-.4-.178zm13.326 5.608a5.5 5.5 0 1 1-2.847-10.625 5.5 5.5 0 0 1 2.847 10.625zm-.776-2.898a2.5 2.5 0 1 0-1.294-4.83 2.5 2.5 0 0 0 1.294 4.83z"/></g><g fill-rule="nonzero"><path fill="#EFEDF8" d="M326.979 222.047c14.403 3.86 29.209-4.688 33.068-19.092 3.86-14.403-4.688-29.209-19.092-33.068-14.403-3.86-29.209 4.688-33.068 19.092-3.86 14.404 4.688 29.209 19.092 33.068zm-1.035 3.864c-16.538-4.431-26.352-21.43-21.92-37.967 4.43-16.538 21.429-26.352 37.966-21.92 16.538 4.43 26.352 21.429 21.92 37.966-4.43 16.538-21.429 26.352-37.966 21.92z"/><path fill="#6B4FBB" d="M329.376 201.598c-4.668-2.621-7.155-8.157-5.706-13.566 1.715-6.402 8.295-10.201 14.697-8.486 6.402 1.716 10.2 8.296 8.485 14.697-1.45 5.41-6.371 8.96-11.725 8.897a3.03 3.03 0 0 1-.074.365l-1.812 6.761a3 3 0 0 1-5.795-1.552l1.812-6.762a3.03 3.03 0 0 1 .118-.354zm3.815-2.733a8 8 0 1 0 4.14-15.455 8 8 0 0 0-4.14 15.455z"/></g><path fill="#FEF0E8" fill-rule="nonzero" d="M91.373 193c17.071-4.574 27.202-22.12 22.628-39.191-4.575-17.071-22.121-27.202-39.192-22.628-17.071 4.574-27.202 22.121-22.628 39.192 4.574 17.071 22.121 27.202 39.192 22.627zm1.035 3.864c-19.204 5.146-38.945-6.25-44.09-25.456-5.146-19.204 6.25-38.945 25.455-44.09 19.205-5.146 38.945 6.25 44.091 25.455 5.146 19.205-6.25 38.945-25.456 44.091z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M70.067 152.122l6.73 25.114 19.318-5.176-6.73-25.114-19.318 5.176zm-1.035-3.864l19.318-5.176a4 4 0 0 1 4.9 2.828l6.729 25.114a4 4 0 0 1-2.829 4.9L77.832 181.1a4 4 0 0 1-4.9-2.829l-6.729-25.114a4 4 0 0 1 2.829-4.899z"/><path fill="#FC6D26" d="M76.898 154.433l7.727-2.07a2 2 0 0 1 1.036 3.863l-7.728 2.07a2 2 0 1 1-1.035-3.863zm1.812 6.761l5.795-1.553a2 2 0 0 1 1.035 3.864l-5.795 1.553a2 2 0 1 1-1.035-3.864zm1.811 6.762l7.728-2.07a2 2 0 0 1 1.035 3.863l-7.727 2.07a2 2 0 1 1-1.036-3.863z"/></g></svg>
\ No newline at end of file
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
import _ from 'underscore'; import _ from 'underscore';
import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils'; import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils';
import { placeholderImage } from './lazy_loader'; import { placeholderImage } from '../lazy_loader';
const gfmRules = { const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
...@@ -284,7 +285,7 @@ const gfmRules = { ...@@ -284,7 +285,7 @@ const gfmRules = {
}, },
}; };
class CopyAsGFM { export class CopyAsGFM {
constructor() { constructor() {
$(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); $(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
...@@ -469,7 +470,12 @@ class CopyAsGFM { ...@@ -469,7 +470,12 @@ class CopyAsGFM {
} }
} }
window.gl = window.gl || {}; // Export CopyAsGFM as a global for rspec to access
window.gl.CopyAsGFM = CopyAsGFM; // see /spec/features/copy_as_gfm_spec.rb
if (process.env.NODE_ENV !== 'production') {
window.CopyAsGFM = CopyAsGFM;
}
new CopyAsGFM(); export default function initCopyAsGFM() {
return new CopyAsGFM();
}
import './autosize'; import './autosize';
import './bind_in_out'; import './bind_in_out';
import initCopyAsGFM from './copy_as_gfm';
import './details_behavior'; import './details_behavior';
import installGlEmojiElement from './gl_emoji'; import installGlEmojiElement from './gl_emoji';
import './quick_submit'; import './quick_submit';
...@@ -7,3 +8,4 @@ import './requires_input'; ...@@ -7,3 +8,4 @@ import './requires_input';
import './toggler_behavior'; import './toggler_behavior';
installGlEmojiElement(); installGlEmojiElement();
initCopyAsGFM();
...@@ -25,7 +25,7 @@ gl.issueBoards.BoardsStore = { ...@@ -25,7 +25,7 @@ gl.issueBoards.BoardsStore = {
}, },
moving: { moving: {
issue: {}, issue: {},
list: {} list: {},
}, },
create () { create () {
this.state.lists = []; this.state.lists = [];
...@@ -33,6 +33,7 @@ gl.issueBoards.BoardsStore = { ...@@ -33,6 +33,7 @@ gl.issueBoards.BoardsStore = {
this.detail = { this.detail = {
issue: {}, issue: {},
}; };
<<<<<<< HEAD
}, },
createNewListDropdownData() { createNewListDropdownData() {
this.state.currentBoard = { this.state.currentBoard = {
...@@ -44,6 +45,8 @@ gl.issueBoards.BoardsStore = { ...@@ -44,6 +45,8 @@ gl.issueBoards.BoardsStore = {
showPage(page) { showPage(page) {
this.state.reload = false; this.state.reload = false;
this.state.currentPage = page; this.state.currentPage = page;
=======
>>>>>>> upstream/master
}, },
addList (listObj, defaultAvatar) { addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar); const list = new List(listObj, defaultAvatar);
......
/* globals Flash */
import Visibility from 'visibilityjs';
import axios from 'axios';
import setAxiosCsrfToken from './lib/utils/axios_utils';
import Poll from './lib/utils/poll';
import { s__ } from './locale';
import initSettingsPanels from './settings_panels';
import Flash from './flash';
/**
* Cluster page has 2 separate parts:
* Toggle button
*
* - Polling status while creating or scheduled
* -- Update status area with the response result
*/
class ClusterService {
constructor(options = {}) {
this.options = options;
setAxiosCsrfToken();
}
fetchData() {
return axios.get(this.options.endpoint);
}
}
export default class Clusters {
constructor() {
initSettingsPanels();
const dataset = document.querySelector('.js-edit-cluster-form').dataset;
this.state = {
statusPath: dataset.statusPath,
clusterStatus: dataset.clusterStatus,
clusterStatusReason: dataset.clusterStatusReason,
toggleStatus: dataset.toggleStatus,
};
this.service = new ClusterService({ endpoint: this.state.statusPath });
this.toggleButton = document.querySelector('.js-toggle-cluster');
this.toggleInput = document.querySelector('.js-toggle-input');
this.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating');
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.toggleButton.addEventListener('click', this.toggle.bind(this));
if (this.state.clusterStatus !== 'created') {
this.updateContainer(this.state.clusterStatus, this.state.clusterStatusReason);
}
if (this.state.statusPath) {
this.initPolling();
}
}
toggle() {
this.toggleButton.classList.toggle('checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
}
initPolling() {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: data => this.handleSuccess(data),
errorCallback: () => Clusters.handleError(),
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
this.service.fetchData()
.then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError());
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
static handleError() {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
}
handleSuccess(data) {
const { status, status_reason } = data.data;
this.updateContainer(status, status_reason);
}
hideAll() {
this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden');
this.creatingContainer.classList.add('hidden');
}
updateContainer(status, error) {
this.hideAll();
switch (status) {
case 'created':
this.successContainer.classList.remove('hidden');
break;
case 'errored':
this.errorContainer.classList.remove('hidden');
this.errorReasonContainer.textContent = error;
break;
case 'scheduled':
case 'creating':
this.creatingContainer.classList.remove('hidden');
break;
default:
this.hideAll();
}
}
}
import Visibility from 'visibilityjs';
import Vue from 'vue';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
import {
APPLICATION_INSTALLED,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
} from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue';
/**
* Cluster page has 2 separate parts:
* Toggle button and applications section
*
* - Polling status while creating or scheduled
* - Update status area with the response result
*/
export default class Clusters {
constructor() {
const {
statusPath,
installHelmPath,
installIngressPath,
installRunnerPath,
clusterStatus,
clusterStatusReason,
helpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore();
this.store.setHelpPath(helpPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
this.service = new ClustersService({
endpoint: statusPath,
installHelmEndpoint: installHelmPath,
installIngressEndpoint: installIngressPath,
installRunnerEndpoint: installRunnerPath,
});
this.toggle = this.toggle.bind(this);
this.installApplication = this.installApplication.bind(this);
this.toggleButton = document.querySelector('.js-toggle-cluster');
this.toggleInput = document.querySelector('.js-toggle-input');
this.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating');
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
initSettingsPanels();
this.initApplications();
if (this.store.state.status !== 'created') {
this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
}
this.addListeners();
if (statusPath) {
this.initPolling();
}
}
initApplications() {
const store = this.store;
const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({
el,
components: {
applications,
},
data() {
return {
state: store.state,
};
},
render(createElement) {
return createElement('applications', {
props: {
applications: this.state.applications,
helpPath: this.state.helpPath,
},
});
},
});
}
addListeners() {
this.toggleButton.addEventListener('click', this.toggle);
eventHub.$on('installApplication', this.installApplication);
}
removeListeners() {
this.toggleButton.removeEventListener('click', this.toggle);
eventHub.$off('installApplication', this.installApplication);
}
initPolling() {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: data => this.handleSuccess(data),
errorCallback: () => Clusters.handleError(),
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
this.service.fetchData()
.then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError());
}
Visibility.change(() => {
if (!Visibility.hidden() && !this.destroyed) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
static handleError() {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
}
handleSuccess(data) {
const prevStatus = this.store.state.status;
const prevApplicationMap = Object.assign({}, this.store.state.applications);
this.store.updateStateFromServer(data.data);
this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
}
toggle() {
this.toggleButton.classList.toggle('checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
}
hideAll() {
this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden');
this.creatingContainer.classList.add('hidden');
}
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
const appTitles = Object.keys(newApplicationMap)
.filter(appId => newApplicationMap[appId].status === APPLICATION_INSTALLED &&
prevApplicationMap[appId].status !== APPLICATION_INSTALLED &&
prevApplicationMap[appId].status !== null)
.map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), {
appList: appTitles.join(', '),
});
Flash(text, 'notice', this.successApplicationContainer);
}
}
updateContainer(prevStatus, status, error) {
this.hideAll();
// We poll all the time but only want the `created` banner to show when newly created
if (this.store.state.status !== 'created' || prevStatus !== this.store.state.status) {
switch (status) {
case 'created':
this.successContainer.classList.remove('hidden');
break;
case 'errored':
this.errorContainer.classList.remove('hidden');
this.errorReasonContainer.textContent = error;
break;
case 'scheduled':
case 'creating':
this.creatingContainer.classList.remove('hidden');
break;
default:
this.hideAll();
}
}
}
installApplication(appId) {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null);
this.service.installApplication(appId)
.then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
})
.catch(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed'));
});
}
destroy() {
this.destroyed = true;
this.removeListeners();
if (this.poll) {
this.poll.stop();
}
this.applications.$destroy();
}
}
<script>
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
APPLICATION_NOT_INSTALLABLE,
APPLICATION_SCHEDULED,
APPLICATION_INSTALLABLE,
APPLICATION_INSTALLING,
APPLICATION_INSTALLED,
APPLICATION_ERROR,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
} from '../constants';
export default {
props: {
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
titleLink: {
type: String,
required: false,
},
description: {
type: String,
required: true,
},
status: {
type: String,
required: false,
},
statusReason: {
type: String,
required: false,
},
requestStatus: {
type: String,
required: false,
},
requestReason: {
type: String,
required: false,
},
},
components: {
loadingButton,
},
computed: {
rowJsClass() {
return `js-cluster-application-row-${this.id}`;
},
installButtonLoading() {
return !this.status ||
this.status === APPLICATION_SCHEDULED ||
this.status === APPLICATION_INSTALLING ||
this.requestStatus === REQUEST_LOADING;
},
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) ||
this.requestStatus === REQUEST_LOADING ||
this.requestStatus === REQUEST_SUCCESS;
},
installButtonLabel() {
let label;
if (
this.status === APPLICATION_NOT_INSTALLABLE ||
this.status === APPLICATION_INSTALLABLE ||
this.status === APPLICATION_ERROR
) {
label = s__('ClusterIntegration|Install');
} else if (this.status === APPLICATION_SCHEDULED || this.status === APPLICATION_INSTALLING) {
label = s__('ClusterIntegration|Installing');
} else if (this.status === APPLICATION_INSTALLED) {
label = s__('ClusterIntegration|Installed');
}
return label;
},
hasError() {
return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE;
},
generalErrorDescription() {
return sprintf(
s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title,
},
);
},
},
methods: {
installClicked() {
eventHub.$emit('installApplication', this.id);
},
},
};
</script>
<template>
<div
class="gl-responsive-table-row gl-responsive-table-row-col-span"
:class="rowJsClass"
>
<div
class="gl-responsive-table-row-layout"
role="row"
>
<a
v-if="titleLink"
:href="titleLink"
target="blank"
rel="noopener noreferrer"
role="gridcell"
class="table-section section-15 section-align-top js-cluster-application-title"
>
{{ title }}
</a>
<span
v-else
class="table-section section-15 section-align-top js-cluster-application-title"
>
{{ title }}
</span>
<div
class="table-section section-wrap"
role="gridcell"
>
<div v-html="description"></div>
</div>
<div
class="table-section table-button-footer section-15 section-align-top"
role="gridcell"
>
<div class="btn-group table-action-buttons">
<loading-button
class="js-cluster-application-install-button"
:loading="installButtonLoading"
:disabled="installButtonDisabled"
:label="installButtonLabel"
@click="installClicked"
/>
</div>
</div>
</div>
<div
v-if="hasError"
class="gl-responsive-table-row-layout"
role="row"
>
<div
class="alert alert-danger alert-block append-bottom-0 table-section section-100"
role="gridcell"
>
<div>
<p class="js-cluster-application-general-error-message">
{{ generalErrorDescription }}
</p>
<ul v-if="statusReason || requestReason">
<li
v-if="statusReason"
class="js-cluster-application-status-error-message"
>
{{ statusReason }}
</li>
<li
v-if="requestReason"
class="js-cluster-application-request-error-message"
>
{{ requestReason }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
export default {
props: {
applications: {
type: Object,
required: false,
default: () => ({}),
},
helpPath: {
type: String,
required: false,
},
},
components: {
applicationRow,
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), {
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
},
false,
);
},
helmTillerDescription() {
return _.escape(s__(
`ClusterIntegration|Helm streamlines installing and managing Kubernets applications.
Tiller runs inside of your Kubernetes Cluster, and manages
releases of your charts.`,
));
},
ingressDescription() {
const descriptionParagraph = _.escape(s__(
`ClusterIntegration|Ingress gives you a way to route requests to services based on the
request host or path, centralizing a number of services into a single entrypoint.`,
));
const extraCostParagraph = sprintf(
_.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), {
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GKE pricing'))}
</a>`,
},
false,
);
return `
<p>
${descriptionParagraph}
</p>
<p class="append-bottom-0">
${extraCostParagraph}
</p>
`;
},
gitlabRunnerDescription() {
return _.escape(s__(
`ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs
and send the results back to GitLab.`,
));
},
},
};
</script>
<template>
<section class="settings no-animate expanded">
<div class="settings-header">
<h4>
{{ s__('ClusterIntegration|Applications') }}
</h4>
<p
class="append-bottom-0"
v-html="generalApplicationDescription"
>
</p>
</div>
<div class="settings-content">
<div class="append-bottom-20">
<application-row
id="helm"
:title="applications.helm.title"
title-link="https://docs.helm.sh/"
:description="helmTillerDescription"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
/>
<application-row
id="ingress"
:title="applications.ingress.title"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
:description="ingressDescription"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
/>
<!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests -->
<!-- Add GitLab Runner row, all other plumbing is complete -->
</div>
</div>
</section>
</template>
// These need to match what is returned from the server
export const APPLICATION_NOT_INSTALLABLE = 'not_installable';
export const APPLICATION_INSTALLABLE = 'installable';
export const APPLICATION_SCHEDULED = 'scheduled';
export const APPLICATION_INSTALLING = 'installing';
export const APPLICATION_INSTALLED = 'installed';
export const APPLICATION_ERROR = 'errored';
// These are only used client-side
export const REQUEST_LOADING = 'request-loading';
export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
import Vue from 'vue';
export default new Vue();
import axios from 'axios';
import setAxiosCsrfToken from '../../lib/utils/axios_utils';
export default class ClusterService {
constructor(options = {}) {
setAxiosCsrfToken();
this.options = options;
this.appInstallEndpointMap = {
helm: this.options.installHelmEndpoint,
ingress: this.options.installIngressEndpoint,
runner: this.options.installRunnerEndpoint,
};
}
fetchData() {
return axios.get(this.options.endpoint);
}
installApplication(appId) {
const endpoint = this.appInstallEndpointMap[appId];
return axios.post(endpoint);
}
}
import { s__ } from '../../locale';
export default class ClusterStore {
constructor() {
this.state = {
helpPath: null,
status: null,
statusReason: null,
applications: {
helm: {
title: s__('ClusterIntegration|Helm Tiller'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
},
ingress: {
title: s__('ClusterIntegration|Ingress'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
},
runner: {
title: s__('ClusterIntegration|GitLab Runner'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
},
},
};
}
setHelpPath(helpPath) {
this.state.helpPath = helpPath;
}
updateStatus(status) {
this.state.status = status;
}
updateStatusReason(reason) {
this.state.statusReason = reason;
}
updateAppProperty(appId, prop, value) {
this.state.applications[appId][prop] = value;
}
updateStateFromServer(serverState = {}) {
this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason;
serverState.applications.forEach((serverAppEntry) => {
const {
name: appId,
status,
status_reason: statusReason,
} = serverAppEntry;
this.state.applications[appId] = {
...(this.state.applications[appId] || {}),
status,
statusReason,
};
});
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
import { s__ } from './locale';
/* global ProjectSelect */ /* global ProjectSelect */
import IssuableIndex from './issuable_index'; import IssuableIndex from './issuable_index';
/* global Milestone */ /* global Milestone */
...@@ -32,8 +33,13 @@ import NamespaceSelect from './namespace_select'; ...@@ -32,8 +33,13 @@ import NamespaceSelect from './namespace_select';
import Labels from './labels'; import Labels from './labels';
import LabelManager from './label_manager'; import LabelManager from './label_manager';
/* global Sidebar */ /* global Sidebar */
<<<<<<< HEAD
/* global WeightSelect */ /* global WeightSelect */
/* global AdminEmailSelect */ /* global AdminEmailSelect */
=======
import Flash from './flash';
>>>>>>> upstream/master
import CommitsList from './commits'; import CommitsList from './commits';
import Issue from './issue'; import Issue from './issue';
import BindInOut from './behaviors/bind_in_out'; import BindInOut from './behaviors/bind_in_out';
...@@ -601,9 +607,12 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -601,9 +607,12 @@ import initGroupAnalytics from './init_group_analytics';
new DueDateSelectors(); new DueDateSelectors();
break; break;
case 'projects:clusters:show': case 'projects:clusters:show':
import(/* webpackChunkName: "clusters" */ './clusters') import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap .then(cluster => new cluster.default()) // eslint-disable-line new-cap
.catch(() => {}); .catch((err) => {
Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript'));
throw err;
});
break; break;
case 'admin:licenses:new': case 'admin:licenses:new':
const $licenseFile = $('.license-file'); const $licenseFile = $('.license-file');
......
...@@ -46,7 +46,6 @@ import './commits'; ...@@ -46,7 +46,6 @@ import './commits';
import './compare'; import './compare';
import './compare_autocomplete'; import './compare_autocomplete';
import './confirm_danger_modal'; import './confirm_danger_modal';
import './copy_as_gfm';
import './copy_to_clipboard'; import './copy_to_clipboard';
import './diff'; import './diff';
import './files_comment_button'; import './files_comment_button';
......
...@@ -148,6 +148,7 @@ import _ from 'underscore'; ...@@ -148,6 +148,7 @@ import _ from 'underscore';
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
<<<<<<< HEAD
hideRow: function(milestone) { hideRow: function(milestone) {
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
!$dropdown.closest('.add-issues-modal').length && gl.issueBoards.BoardsStore.state.currentBoard.milestone && !$dropdown.closest('.add-issues-modal').length && gl.issueBoards.BoardsStore.state.currentBoard.milestone &&
...@@ -166,6 +167,8 @@ import _ from 'underscore'; ...@@ -166,6 +167,8 @@ import _ from 'underscore';
return true; return true;
}, },
=======
>>>>>>> upstream/master
clicked: function(clickEvent) { clicked: function(clickEvent) {
const { $el, e } = clickEvent; const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj; let selected = clickEvent.selectedObj;
......
...@@ -138,7 +138,7 @@ ...@@ -138,7 +138,7 @@
renderAxesPaths() { renderAxesPaths() {
this.timeSeries = createTimeSeries( this.timeSeries = createTimeSeries(
this.graphData.queries[0], this.graphData.queries,
this.graphWidth, this.graphWidth,
this.graphHeight, this.graphHeight,
this.graphHeightOffset, this.graphHeightOffset,
...@@ -153,8 +153,9 @@ ...@@ -153,8 +153,9 @@
const axisYScale = d3.scale.linear() const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]); .range([this.graphHeight - this.graphHeightOffset, 0]);
axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time)); const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]); axisXScale.domain(d3.extent(allValues, d => d.time));
axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
const xAxis = d3.svg.axis() const xAxis = d3.svg.axis()
.scale(axisXScale) .scale(axisXScale)
...@@ -246,6 +247,7 @@ ...@@ -246,6 +247,7 @@
:key="index" :key="index"
:generated-line-path="path.linePath" :generated-line-path="path.linePath"
:generated-area-path="path.areaPath" :generated-area-path="path.areaPath"
:line-style="path.lineStyle"
:line-color="path.lineColor" :line-color="path.lineColor"
:area-color="path.areaColor" :area-color="path.areaColor"
/> />
......
...@@ -79,7 +79,8 @@ ...@@ -79,7 +79,8 @@
}, },
formatMetricUsage(series) { formatMetricUsage(series) {
const value = series.values[this.currentDataIndex].value; const value = series.values[this.currentDataIndex] &&
series.values[this.currentDataIndex].value;
if (isNaN(value)) { if (isNaN(value)) {
return '-'; return '-';
} }
...@@ -92,6 +93,12 @@ ...@@ -92,6 +93,12 @@
} }
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`; return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
}, },
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
},
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
...@@ -162,13 +169,15 @@ ...@@ -162,13 +169,15 @@
v-for="(series, index) in timeSeries" v-for="(series, index) in timeSeries"
:key="index" :key="index"
:transform="translateLegendGroup(index)"> :transform="translateLegendGroup(index)">
<rect <line
:fill="series.areaColor" :stroke="series.lineColor"
:width="measurements.legends.width" :stroke-width="measurements.legends.height"
:height="measurements.legends.height" :stroke-dasharray="strokeDashArray(series.lineStyle)"
x="20" :x1="measurements.legends.offsetX"
:y="graphHeight - measurements.legendOffset"> :x2="measurements.legends.offsetX + measurements.legends.width"
</rect> :y1="graphHeight - measurements.legends.offsetY"
:y2="graphHeight - measurements.legends.offsetY">
</line>
<text <text
v-if="timeSeries.length > 1" v-if="timeSeries.length > 1"
class="legend-metric-title" class="legend-metric-title"
......
...@@ -9,6 +9,10 @@ ...@@ -9,6 +9,10 @@
type: String, type: String,
required: true, required: true,
}, },
lineStyle: {
type: String,
required: false,
},
lineColor: { lineColor: {
type: String, type: String,
required: true, required: true,
...@@ -18,6 +22,13 @@ ...@@ -18,6 +22,13 @@
required: true, required: true,
}, },
}, },
computed: {
strokeDashArray() {
if (this.lineStyle === 'dashed') return '3, 1';
if (this.lineStyle === 'dotted') return '1, 1';
return null;
},
},
}; };
</script> </script>
<template> <template>
...@@ -34,6 +45,7 @@ ...@@ -34,6 +45,7 @@
:stroke="lineColor" :stroke="lineColor"
fill="none" fill="none"
stroke-width="1" stroke-width="1"
:stroke-dasharray="strokeDashArray"
transform="translate(-5, 20)"> transform="translate(-5, 20)">
</path> </path>
</g> </g>
......
...@@ -7,15 +7,16 @@ export default { ...@@ -7,15 +7,16 @@ export default {
left: 40, left: 40,
}, },
legends: { legends: {
width: 10, width: 15,
height: 3, height: 3,
offsetX: 20,
offsetY: 32,
}, },
backgroundLegend: { backgroundLegend: {
width: 30, width: 30,
height: 50, height: 50,
}, },
axisLabelLineOffset: -20, axisLabelLineOffset: -20,
legendOffset: 33,
}, },
large: { // This covers both md and lg screen sizes large: { // This covers both md and lg screen sizes
margin: { margin: {
...@@ -27,13 +28,14 @@ export default { ...@@ -27,13 +28,14 @@ export default {
legends: { legends: {
width: 15, width: 15,
height: 3, height: 3,
offsetX: 20,
offsetY: 34,
}, },
backgroundLegend: { backgroundLegend: {
width: 30, width: 30,
height: 150, height: 150,
}, },
axisLabelLineOffset: 20, axisLabelLineOffset: 20,
legendOffset: 36,
}, },
xTicks: 8, xTicks: 8,
yTicks: 3, yTicks: 3,
......
...@@ -11,7 +11,9 @@ const defaultColorPalette = { ...@@ -11,7 +11,9 @@ const defaultColorPalette = {
const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple']; const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) { const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
let usedColors = []; let usedColors = [];
function pickColor(name) { function pickColor(name) {
...@@ -31,17 +33,7 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra ...@@ -31,17 +33,7 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
return defaultColorPalette[pick]; return defaultColorPalette[pick];
} }
const maxValues = queryData.result.map((timeSeries, index) => { return query.result.map((timeSeries, timeSeriesNumber) => {
const maxValue = d3.max(timeSeries.values.map(d => d.value));
return {
maxValue,
index,
};
});
const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
return queryData.result.map((timeSeries, timeSeriesNumber) => {
let metricTag = ''; let metricTag = '';
let lineColor = ''; let lineColor = '';
let areaColor = ''; let areaColor = '';
...@@ -52,9 +44,9 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra ...@@ -52,9 +44,9 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
const timeSeriesScaleY = d3.scale.linear() const timeSeriesScaleY = d3.scale.linear()
.range([graphHeight - graphHeightOffset, 0]); .range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time)); timeSeriesScaleX.domain(xDom);
timeSeriesScaleX.ticks(d3.time.minute, 60); timeSeriesScaleX.ticks(d3.time.minute, 60);
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]); timeSeriesScaleY.domain(yDom);
const defined = d => !isNaN(d.value) && d.value != null; const defined = d => !isNaN(d.value) && d.value != null;
...@@ -72,10 +64,10 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra ...@@ -72,10 +64,10 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
.y1(d => timeSeriesScaleY(d.value)); .y1(d => timeSeriesScaleY(d.value));
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]]; const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
const seriesCustomizationData = queryData.series != null && const seriesCustomizationData = query.series != null &&
_.findWhere(queryData.series[0].when, _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
{ value: timeSeriesMetricLabel });
if (seriesCustomizationData != null) { if (seriesCustomizationData) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel; metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color); [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else { } else {
...@@ -83,14 +75,35 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra ...@@ -83,14 +75,35 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
[lineColor, areaColor] = pickColor(); [lineColor, areaColor] = pickColor();
} }
if (query.track) {
metricTag += ` - ${query.track}`;
}
return { return {
linePath: lineFunction(timeSeries.values), linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values), areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX, timeSeriesScaleX,
values: timeSeries.values, values: timeSeries.values,
lineStyle,
lineColor, lineColor,
areaColor, areaColor,
metricTag, metricTag,
}; };
}); });
} }
export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat(
query.result.reduce((allResults, result) => allResults.concat(result.values), []),
), []);
const xDom = d3.extent(allValues, d => d.time);
const yDom = [0, d3.max(allValues.map(d => d.value))];
return queries.reduce((series, query, index) => {
const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
return series.concat(
queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle),
);
}, []);
}
...@@ -413,8 +413,9 @@ export default class Notes { ...@@ -413,8 +413,9 @@ export default class Notes {
return; return;
} }
this.note_ids.push(noteEntity.id); this.note_ids.push(noteEntity.id);
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row = form.closest('tr'); row = (form.length || !noteEntity.discussion_line_code) ? form.closest('tr') : $(`#${noteEntity.discussion_line_code}`);
if (noteEntity.on_image) { if (noteEntity.on_image) {
row = form; row = form;
......
...@@ -54,7 +54,14 @@ ...@@ -54,7 +54,14 @@
<tr <tr
class="file" class="file"
@click.prevent="clickedTreeRow(file)"> @click.prevent="clickedTreeRow(file)">
<<<<<<< HEAD
<td :colspan="submoduleColSpan"> <td :colspan="submoduleColSpan">
=======
<td
class="multi-file-table-col-name"
:colspan="submoduleColSpan"
>
>>>>>>> upstream/master
<i <i
class="fa fa-fw file-icon" class="fa fa-fw file-icon"
:class="fileIcon" :class="fileIcon"
......
...@@ -19,7 +19,11 @@ ...@@ -19,7 +19,11 @@
class="loading-file" class="loading-file"
aria-label="Loading files" aria-label="Loading files"
> >
<<<<<<< HEAD
<td> <td>
=======
<td class="multi-file-table-col-name">
>>>>>>> upstream/master
<skeleton-loading-container <skeleton-loading-container
:small="true" :small="true"
/> />
......
...@@ -57,7 +57,7 @@ export default { ...@@ -57,7 +57,7 @@ export default {
</strong> </strong>
</th> </th>
<template v-else> <template v-else>
<th class="name"> <th class="name multi-file-table-col-name">
Name Name
</th> </th>
<th class="hidden-sm hidden-xs last-commit"> <th class="hidden-sm hidden-xs last-commit">
......
...@@ -162,13 +162,19 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. ...@@ -162,13 +162,19 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
items = [ items = [
{ {
header: "" + name header: "" + name
}, { }
];
const issueItems = [
{
text: 'Issues assigned to me', text: 'Issues assigned to me',
url: issuesPath + "/?assignee_username=" + userName url: issuesPath + "/?assignee_username=" + userName
}, { }, {
text: "Issues I've created", text: "Issues I've created",
url: issuesPath + "/?author_username=" + userName url: issuesPath + "/?author_username=" + userName
}, 'separator', { }
];
const mergeRequestItems = [
{
text: 'Merge requests assigned to me', text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_username=" + userName url: mrPath + "/?assignee_username=" + userName
}, { }, {
...@@ -176,6 +182,11 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. ...@@ -176,6 +182,11 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
url: mrPath + "/?author_username=" + userName url: mrPath + "/?author_username=" + userName
} }
]; ];
if (options.issuesDisabled) {
items = items.concat(mergeRequestItems);
} else {
items = items.concat(...issueItems, 'separator', ...mergeRequestItems);
}
if (!name) { if (!name) {
items.splice(0, 1); items.splice(0, 1);
} }
...@@ -408,6 +419,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. ...@@ -408,6 +419,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
gl.projectOptions[projectPath] = { gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'), name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'), issuesPath: $projectOptionsDataEl.data('issues-path'),
issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
mrPath: $projectOptionsDataEl.data('mr-path') mrPath: $projectOptionsDataEl.data('mr-path')
}; };
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import _ from 'underscore'; import _ from 'underscore';
import 'mousetrap'; import 'mousetrap';
import ShortcutsNavigation from './shortcuts_navigation'; import ShortcutsNavigation from './shortcuts_navigation';
import { CopyAsGFM } from './behaviors/copy_as_gfm';
export default class ShortcutsIssuable extends ShortcutsNavigation { export default class ShortcutsIssuable extends ShortcutsNavigation {
constructor(isMergeRequest) { constructor(isMergeRequest) {
...@@ -33,8 +34,8 @@ export default class ShortcutsIssuable extends ShortcutsNavigation { ...@@ -33,8 +34,8 @@ export default class ShortcutsIssuable extends ShortcutsNavigation {
return false; return false;
} }
const el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
const selected = window.gl.CopyAsGFM.nodeToGFM(el); const selected = CopyAsGFM.nodeToGFM(el);
if (selected.trim() === '') { if (selected.trim() === '') {
return false; return false;
......
...@@ -3,9 +3,10 @@ ...@@ -3,9 +3,10 @@
* and controllable by a public API. * and controllable by a public API.
*/ */
class SmartInterval { export default class SmartInterval {
/** /**
* @param { function } opts.callback Function to be called on each iteration (required) * @param { function } opts.callback Function that returns a promise, called on each iteration
* unless still in progress (required)
* @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
...@@ -42,13 +43,16 @@ class SmartInterval { ...@@ -42,13 +43,16 @@ class SmartInterval {
const cfg = this.cfg; const cfg = this.cfg;
const state = this.state; const state = this.state;
if (cfg.immediateExecution) { if (cfg.immediateExecution && !this.isLoading) {
cfg.immediateExecution = false; cfg.immediateExecution = false;
cfg.callback(); this.triggerCallback();
} }
state.intervalId = window.setInterval(() => { state.intervalId = window.setInterval(() => {
cfg.callback(); if (this.isLoading) {
return;
}
this.triggerCallback();
if (this.getCurrentInterval() === cfg.maxInterval) { if (this.getCurrentInterval() === cfg.maxInterval) {
return; return;
...@@ -76,7 +80,7 @@ class SmartInterval { ...@@ -76,7 +80,7 @@ class SmartInterval {
// start a timer, using the existing interval // start a timer, using the existing interval
resume() { resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped this.stopTimer(); // stop existing timer, in case timer was not previously stopped
this.start(); this.start();
} }
...@@ -104,6 +108,18 @@ class SmartInterval { ...@@ -104,6 +108,18 @@ class SmartInterval {
this.initPageUnloadHandling(); this.initPageUnloadHandling();
} }
triggerCallback() {
this.isLoading = true;
this.cfg.callback()
.then(() => {
this.isLoading = false;
})
.catch((err) => {
this.isLoading = false;
throw err;
});
}
initVisibilityChangeHandling() { initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling) // cancel interval when tab no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
...@@ -154,4 +170,3 @@ class SmartInterval { ...@@ -154,4 +170,3 @@ class SmartInterval {
} }
} }
window.gl.SmartInterval = SmartInterval;
import SmartInterval from '~/smart_interval';
import Flash from '../flash'; import Flash from '../flash';
import { import {
WidgetHeader, WidgetHeader,
...@@ -83,7 +84,7 @@ export default { ...@@ -83,7 +84,7 @@ export default {
return new MRWidgetService(endpoints); return new MRWidgetService(endpoints);
}, },
checkStatus(cb) { checkStatus(cb) {
this.service.checkStatus() return this.service.checkStatus()
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then((res) => {
this.handleNotification(res); this.handleNotification(res);
...@@ -97,7 +98,7 @@ export default { ...@@ -97,7 +98,7 @@ export default {
.catch(() => new Flash('Something went wrong. Please try again.')); .catch(() => new Flash('Something went wrong. Please try again.'));
}, },
initPolling() { initPolling() {
this.pollingInterval = new gl.SmartInterval({ this.pollingInterval = new SmartInterval({
callback: this.checkStatus, callback: this.checkStatus,
startingInterval: 10000, startingInterval: 10000,
maxInterval: 30000, maxInterval: 30000,
...@@ -106,7 +107,7 @@ export default { ...@@ -106,7 +107,7 @@ export default {
}); });
}, },
initDeploymentsPolling() { initDeploymentsPolling() {
this.deploymentsInterval = new gl.SmartInterval({ this.deploymentsInterval = new SmartInterval({
callback: this.fetchDeployments, callback: this.fetchDeployments,
startingInterval: 30000, startingInterval: 30000,
maxInterval: 120000, maxInterval: 120000,
...@@ -121,7 +122,7 @@ export default { ...@@ -121,7 +122,7 @@ export default {
} }
}, },
fetchDeployments() { fetchDeployments() {
this.service.fetchDeployments() return this.service.fetchDeployments()
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then((res) => {
if (res.length) { if (res.length) {
......
...@@ -26,6 +26,11 @@ export default { ...@@ -26,6 +26,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
disabled: {
type: Boolean,
required: false,
default: false,
},
label: { label: {
type: String, type: String,
required: false, required: false,
...@@ -47,7 +52,7 @@ export default { ...@@ -47,7 +52,7 @@ export default {
class="btn btn-align-content" class="btn btn-align-content"
@click="onClick" @click="onClick"
type="button" type="button"
:disabled="loading" :disabled="loading || disabled"
> >
<transition name="fade"> <transition name="fade">
<loading-icon <loading-icon
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import GLForm from '../../../gl_form'; import GLForm from '../../../gl_form';
import markdownHeader from './header.vue'; import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue'; import markdownToolbar from './toolbar.vue';
import icon from '../icon.vue';
export default { export default {
props: { props: {
...@@ -37,6 +38,7 @@ ...@@ -37,6 +38,7 @@
components: { components: {
markdownHeader, markdownHeader,
markdownToolbar, markdownToolbar,
icon,
}, },
computed: { computed: {
shouldShowReferencedUsers() { shouldShowReferencedUsers() {
...@@ -45,8 +47,10 @@ ...@@ -45,8 +47,10 @@
}, },
}, },
methods: { methods: {
toggleMarkdownPreview() { showPreviewTab() {
this.previewMarkdown = !this.previewMarkdown; if (this.previewMarkdown) return;
this.previewMarkdown = true;
/* /*
Can't use `$refs` as the component is technically in the parent component Can't use `$refs` as the component is technically in the parent component
...@@ -54,20 +58,22 @@ ...@@ -54,20 +58,22 @@
*/ */
const text = this.$slots.textarea[0].elm.value; const text = this.$slots.textarea[0].elm.value;
if (!this.previewMarkdown) { if (text) {
this.markdownPreview = '';
} else if (text) {
this.markdownPreviewLoading = true; this.markdownPreviewLoading = true;
this.$http.post(this.markdownPreviewPath, { text }) this.$http.post(this.markdownPreviewPath, { text })
.then(resp => resp.json()) .then(resp => resp.json())
.then((data) => { .then(data => this.renderMarkdown(data))
this.renderMarkdown(data);
})
.catch(() => new Flash('Error loading markdown preview')); .catch(() => new Flash('Error loading markdown preview'));
} else { } else {
this.renderMarkdown(); this.renderMarkdown();
} }
}, },
showWriteTab() {
this.markdownPreview = '';
this.previewMarkdown = false;
},
renderMarkdown(data = {}) { renderMarkdown(data = {}) {
this.markdownPreviewLoading = false; this.markdownPreviewLoading = false;
this.markdownPreview = data.body || 'Nothing to preview.'; this.markdownPreview = data.body || 'Nothing to preview.';
...@@ -104,7 +110,8 @@ ...@@ -104,7 +110,8 @@
ref="gl-form"> ref="gl-form">
<markdown-header <markdown-header
:preview-markdown="previewMarkdown" :preview-markdown="previewMarkdown"
@toggle-markdown="toggleMarkdownPreview" /> @preview-markdown="showPreviewTab"
@write-markdown="showWriteTab" />
<div <div
class="md-write-holder" class="md-write-holder"
v-show="!previewMarkdown"> v-show="!previewMarkdown">
...@@ -114,10 +121,10 @@ ...@@ -114,10 +121,10 @@
class="zen-control zen-control-leave js-zen-leave" class="zen-control zen-control-leave js-zen-leave"
href="#" href="#"
aria-label="Enter zen mode"> aria-label="Enter zen mode">
<i <icon
class="fa fa-compress" name="screen-normal"
aria-hidden="true"> :size="32">
</i> </icon>
</a> </a>
<markdown-toolbar <markdown-toolbar
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
......
<script> <script>
import tooltip from '../../directives/tooltip'; import tooltip from '../../directives/tooltip';
import toolbarButton from './toolbar_button.vue'; import toolbarButton from './toolbar_button.vue';
import icon from '../icon.vue';
export default { export default {
props: { props: {
...@@ -14,25 +15,34 @@ ...@@ -14,25 +15,34 @@
}, },
components: { components: {
toolbarButton, toolbarButton,
icon,
}, },
methods: { methods: {
toggleMarkdownPreview(e, form) { isMarkdownForm(form) {
if (form && !form.find('.js-vue-markdown-field').length) { return form && !form.find('.js-vue-markdown-field').length;
return; },
} else if (e.target.blur) {
e.target.blur(); previewMarkdownTab(event, form) {
} if (event.target.blur) event.target.blur();
if (this.isMarkdownForm(form)) return;
this.$emit('preview-markdown');
},
writeMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
if (this.isMarkdownForm(form)) return;
this.$emit('toggle-markdown'); this.$emit('write-markdown');
}, },
}, },
mounted() { mounted() {
$(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview); $(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).on('markdown-preview:hide.vue', this.toggleMarkdownPreview); $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
}, },
beforeDestroy() { beforeDestroy() {
$(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview); $(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).off('markdown-preview:hide.vue', this.toggleMarkdownPreview); $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
}, },
}; };
</script> </script>
...@@ -42,17 +52,19 @@ ...@@ -42,17 +52,19 @@
<ul class="nav-links clearfix"> <ul class="nav-links clearfix">
<li :class="{ active: !previewMarkdown }"> <li :class="{ active: !previewMarkdown }">
<a <a
class="js-write-link"
href="#md-write-holder" href="#md-write-holder"
tabindex="-1" tabindex="-1"
@click.prevent="toggleMarkdownPreview($event)"> @click.prevent="writeMarkdownTab($event)">
Write Write
</a> </a>
</li> </li>
<li :class="{ active: previewMarkdown }"> <li :class="{ active: previewMarkdown }">
<a <a
class="js-preview-link"
href="#md-preview-holder" href="#md-preview-holder"
tabindex="-1" tabindex="-1"
@click.prevent="toggleMarkdownPreview($event)"> @click.prevent="previewMarkdownTab($event)">
Preview Preview
</a> </a>
</li> </li>
...@@ -70,7 +82,7 @@ ...@@ -70,7 +82,7 @@
tag="> " tag="> "
:prepend="true" :prepend="true"
button-title="Insert a quote" button-title="Insert a quote"
icon="quote-right" /> icon="quote" />
<toolbar-button <toolbar-button
tag="`" tag="`"
tag-block="```" tag-block="```"
...@@ -80,17 +92,17 @@ ...@@ -80,17 +92,17 @@
tag="* " tag="* "
:prepend="true" :prepend="true"
button-title="Add a bullet list" button-title="Add a bullet list"
icon="list-ul" /> icon="list-bulleted" />
<toolbar-button <toolbar-button
tag="1. " tag="1. "
:prepend="true" :prepend="true"
button-title="Add a numbered list" button-title="Add a numbered list"
icon="list-ol" /> icon="list-numbered" />
<toolbar-button <toolbar-button
tag="* [ ] " tag="* [ ] "
:prepend="true" :prepend="true"
button-title="Add a task list" button-title="Add a task list"
icon="check-square-o" /> icon="task-done" />
</div> </div>
<div class="toolbar-group"> <div class="toolbar-group">
<button <button
...@@ -101,10 +113,9 @@ ...@@ -101,10 +113,9 @@
tabindex="-1" tabindex="-1"
title="Go full screen" title="Go full screen"
type="button"> type="button">
<i <icon
aria-hidden="true" name="screen-full">
class="fa fa-arrows-alt fa-fw"> </icon>
</i>
</button> </button>
</div> </div>
</li> </li>
......
<script> <script>
import tooltip from '../../directives/tooltip'; import tooltip from '../../directives/tooltip';
import icon from '../icon.vue';
export default { export default {
props: { props: {
...@@ -26,14 +27,12 @@ ...@@ -26,14 +27,12 @@
default: false, default: false,
}, },
}, },
components: {
icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
computed: {
iconClass() {
return `fa-${this.icon}`;
},
},
}; };
</script> </script>
...@@ -49,10 +48,8 @@ ...@@ -49,10 +48,8 @@
:data-md-prepend="prepend" :data-md-prepend="prepend"
:title="buttonTitle" :title="buttonTitle"
:aria-label="buttonTitle"> :aria-label="buttonTitle">
<i <icon
aria-hidden="true" :name="icon">
class="fa fa-fw" </icon>
:class="iconClass">
</i>
</button> </button>
</template> </template>
...@@ -23,16 +23,6 @@ ...@@ -23,16 +23,6 @@
@include webkit-prefix(animation-duration, 2s); @include webkit-prefix(animation-duration, 2s);
} }
&.spin-cw {
transform-origin: center;
animation: spin 4s linear infinite;
}
&.spin-ccw {
transform-origin: center;
animation: spin 4s linear infinite reverse;
}
&.flipOutX, &.flipOutX,
&.flipOutY, &.flipOutY,
&.bounceIn, &.bounceIn,
...@@ -281,9 +271,3 @@ a { ...@@ -281,9 +271,3 @@ a {
transform: translateX(468px); transform: translateX(468px);
} }
} }
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
...@@ -42,8 +42,7 @@ ...@@ -42,8 +42,7 @@
&.avatar-inline { &.avatar-inline {
float: none; float: none;
display: inline-block; display: inline-block;
margin-left: 4px; margin-left: 2px;
margin-bottom: 2px;
flex-shrink: 0; flex-shrink: 0;
-webkit-flex-shrink: 0; -webkit-flex-shrink: 0;
...@@ -60,10 +59,13 @@ ...@@ -60,10 +59,13 @@
&.avatar-tile { &.avatar-tile {
border-radius: 0; border-radius: 0;
border: 0; border: 0;
<<<<<<< HEAD
} }
&.avatar-placeholder { &.avatar-placeholder {
border: 0; border: 0;
=======
>>>>>>> upstream/master
} }
&:not([href]):hover { &:not([href]):hover {
......
...@@ -298,6 +298,7 @@ ...@@ -298,6 +298,7 @@
.btn-align-content { .btn-align-content {
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
} }
......
...@@ -352,77 +352,7 @@ ...@@ -352,77 +352,7 @@
.header-user .dropdown-menu-nav, .header-user .dropdown-menu-nav,
.header-new .dropdown-menu-nav { .header-new .dropdown-menu-nav {
margin-top: 4px; margin-top: $dropdown-vertical-offset;
}
.search {
margin: 4px 8px 0;
form {
height: 32px;
border: 0;
border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
box-shadow: none;
}
}
.search-input {
color: $white-light;
background: none;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
height: 32px;
transition: border-color ease-in-out 0.15s;
}
&.search-active {
form {
background-color: rgba($indigo-200, .3);
box-shadow: none;
.search-input {
color: $gl-text-color;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
color: $gl-text-color-tertiary;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: $gl-text-color-tertiary;
transition: color ease-in-out 0.15s;
}
}
}
.location-badge {
background-color: $nav-badge-bg;
border-color: $border-color;
}
.search-input-wrap {
.clear-icon {
color: $white-light;
}
}
}
} }
.breadcrumbs { .breadcrumbs {
......
...@@ -138,15 +138,23 @@ ...@@ -138,15 +138,23 @@
.toolbar-btn { .toolbar-btn {
float: left; float: left;
padding: 0 5px; padding: 0 7px;
color: $gl-text-color-secondary;
background: transparent; background: transparent;
border: 0; border: 0;
outline: 0; outline: 0;
svg {
width: 14px;
height: 14px;
margin-top: 3px;
fill: $gl-text-color-secondary;
}
&:hover, &:hover,
&:focus { &:focus {
color: $gl-link-color; svg {
fill: $gl-link-color;
}
} }
} }
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
} }
.modal-body { .modal-body {
background-color: $modal-body-bg;
position: relative; position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size}; padding: #{3 * $grid-size} #{2 * $grid-size};
background-color: $modal-body-bg; background-color: $modal-body-bg;
...@@ -47,3 +48,7 @@ body.modal-open { ...@@ -47,3 +48,7 @@ body.modal-open {
.modal.popup-dialog { .modal.popup-dialog {
display: block; display: block;
} }
<<<<<<< HEAD
=======
>>>>>>> upstream/master
...@@ -367,64 +367,11 @@ ...@@ -367,64 +367,11 @@
} }
} }
.page-with-layout-nav { .project-item-select-holder.btn-group {
.right-sidebar { display: flex;
top: ($header-height + 1) * 2; max-width: 350px;
} overflow: hidden;
float: right;
&.page-with-sub-nav {
.right-sidebar {
top: ($header-height + 1) * 3;
&.affix {
top: $header-height;
}
}
}
}
.with-performance-bar .page-with-layout-nav {
.right-sidebar {
top: ($header-height + 1) * 2 + $performance-bar-height;
}
&.page-with-sub-nav {
.right-sidebar {
top: ($header-height + 1) * 3 + $performance-bar-height;
&.affix {
top: $header-height + $performance-bar-height;
}
}
}
}
@media (max-width: $screen-xs-max) {
.top-area {
flex-flow: row wrap;
.nav-controls {
$controls-margin: $btn-xs-side-margin - 2px;
flex: 0 0 100%;
&.controls-flex {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: center;
padding: 0 0 $gl-padding-top;
}
.controls-item,
.controls-item-full,
.controls-item:last-child {
flex: 1 1 35%;
display: block;
width: 100%;
margin: $controls-margin;
}
}
}
.new-project-item-link { .new-project-item-link {
white-space: nowrap; white-space: nowrap;
......
...@@ -60,12 +60,17 @@ ...@@ -60,12 +60,17 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
border: 1px solid $dropdown-border-color; border: 1px solid $dropdown-border-color;
min-width: 175px; min-width: 175px;
color: $gl-grayish-blue; color: $gl-text-color;
z-index: 999;
} }
.select2-results .select2-result-label, .select2-drop-mask {
.select2-more-results { z-index: 998;
padding: 10px 15px; }
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid $dropdown-border-color;
margin-top: -6px;
} }
.select2-container-active { .select2-container-active {
...@@ -158,18 +163,35 @@ ...@@ -158,18 +163,35 @@
} }
} }
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: $gray-light;
display: list-item;
padding: 10px 15px;
}
.select2-results { .select2-results {
margin: 0; margin: 0;
padding: 10px 0; padding: #{$gl-padding / 2} 0;
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-result-label,
.select2-more-results {
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-highlighted {
background: transparent;
color: $gl-text-color;
.select2-result-label {
background: $dropdown-item-hover-bg;
}
}
.select2-result {
padding: 0 1px;
}
li.select2-result-with-children > .select2-result-label { li.select2-result-with-children > .select2-result-label {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
...@@ -190,8 +212,6 @@ ...@@ -190,8 +212,6 @@
} }
.select2-highlighted { .select2-highlighted {
background: $gl-link-color !important;
.group-result { .group-result {
.group-path { .group-path {
color: $white-light; color: $white-light;
......
...@@ -52,6 +52,37 @@ ...@@ -52,6 +52,37 @@
.label.label-gray { .label.label-gray {
background-color: $well-expand-item; background-color: $well-expand-item;
} }
.branches {
display: inline;
}
.branch-link {
margin-bottom: 2px;
}
.limit-box {
cursor: pointer;
display: inline-flex;
align-items: center;
background-color: $red-100;
border-radius: $border-radius-default;
text-align: center;
&:hover {
background-color: $red-200;
}
.limit-icon {
margin: 0 8px;
}
.limit-message {
line-height: 16px;
margin-right: 8px;
font-size: 12px;
}
}
} }
.light-well { .light-well {
......
...@@ -57,7 +57,15 @@ ...@@ -57,7 +57,15 @@
padding: 5px; padding: 5px;
font-size: 36px; font-size: 36px;
svg {
fill: $gl-text-color;
}
&:hover { &:hover {
color: $black; color: $black;
svg {
fill: $black;
}
} }
} }
...@@ -3,3 +3,8 @@ ...@@ -3,3 +3,8 @@
background-color: $white-light; background-color: $white-light;
} }
} }
.cluster-applications-table {
// Wait for the Vue to kick-in and render the applications block
min-height: 302px;
}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
color: $gl-text-color; color: $gl-text-color;
line-height: 34px; line-height: 34px;
display: flex;
a { a {
color: $gl-text-color; color: $gl-text-color;
......
...@@ -416,12 +416,6 @@ ...@@ -416,12 +416,6 @@
padding: 0; padding: 0;
padding-bottom: 100%; padding-bottom: 100%;
.label-axis-text {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 10px;
}
.text-metric-usage, .text-metric-usage,
.legend-metric-title { .legend-metric-title {
fill: $black; fill: $black;
...@@ -436,19 +430,33 @@ ...@@ -436,19 +430,33 @@
left: 0; left: 0;
top: 0; top: 0;
.label-axis-text, text {
.text-metric-usage { fill: $gl-text-color;
stroke-width: 0;
}
.text-metric-bold {
font-weight: $gl-font-weight-bold;
}
.label-axis-text {
fill: $black; fill: $black;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
font-size: 12px; font-size: 10px;
} }
.legend-axis-text { .legend-axis-text {
fill: $black; fill: $black;
} }
.tick > text { .tick {
font-size: 12px; > line {
stroke: $gray-darker;
}
> text {
font-size: 12px;
}
} }
.text-metric-title { .text-metric-title {
......
...@@ -131,12 +131,12 @@ ...@@ -131,12 +131,12 @@
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
transition: width .3s; transition: width $right-sidebar-transition-duration;
background: $gray-light; background: $gray-light;
z-index: 200; z-index: 200;
overflow: hidden; overflow: hidden;
a, a:not(.btn-retry),
.btn-link { .btn-link {
color: inherit; color: inherit;
} }
...@@ -612,6 +612,8 @@ ...@@ -612,6 +612,8 @@
float: none; float: none;
display: inline-block; display: inline-block;
margin-top: 0; margin-top: 0;
height: auto;
align-self: center;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
position: absolute; position: absolute;
...@@ -625,6 +627,8 @@ ...@@ -625,6 +627,8 @@
padding-left: 45px; padding-left: 45px;
padding-right: 45px; padding-right: 45px;
line-height: 35px; line-height: 35px;
display: flex;
flex-grow: 1;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
float: left; float: left;
...@@ -636,11 +640,12 @@ ...@@ -636,11 +640,12 @@
.issuable-actions { .issuable-actions {
@include new-style-dropdown; @include new-style-dropdown;
padding-top: 10px; align-self: center;
flex-shrink: 0;
flex: 0 0 auto;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
float: right; float: right;
padding-top: 0;
} }
} }
...@@ -654,8 +659,9 @@ ...@@ -654,8 +659,9 @@
.issuable-meta { .issuable-meta {
display: inline-block; display: inline-block;
line-height: 18px;
font-size: 14px; font-size: 14px;
line-height: 24px;
align-self: center;
} }
.js-issuable-selector-wrap { .js-issuable-selector-wrap {
......
...@@ -135,11 +135,24 @@ ul.related-merge-requests > li { ...@@ -135,11 +135,24 @@ ul.related-merge-requests > li {
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.issue-btn-group { .detail-page-header,
width: 100%; .issuable-header {
display: block;
.issuable-meta {
line-height: 18px;
}
}
.btn { .issuable-actions {
margin-top: 10px;
.issue-btn-group {
width: 100%; width: 100%;
.btn {
width: 100%;
}
} }
} }
} }
......
...@@ -149,18 +149,6 @@ ...@@ -149,18 +149,6 @@
display: block; display: block;
} }
.mr-widget-body {
@include clearfix;
&.media > *:first-child {
margin-right: 10px;
}
.approve-btn {
margin-right: 5px;
}
}
.mr-widget-pipeline-graph { .mr-widget-pipeline-graph {
padding: 0 4px; padding: 0 4px;
...@@ -168,9 +156,8 @@ ...@@ -168,9 +156,8 @@
z-index: 300; z-index: 300;
} }
.ci-action-icon-wrapper svg { .ci-action-icon-wrapper {
width: 16px; line-height: 16px;
height: 16px;
} }
} }
...@@ -194,10 +181,6 @@ ...@@ -194,10 +181,6 @@
overflow: hidden; overflow: hidden;
word-break: break-all; word-break: break-all;
&.media > *:first-child {
margin-right: 10px;
}
&.label-truncated { &.label-truncated {
position: relative; position: relative;
display: inline-block; display: inline-block;
...@@ -215,6 +198,18 @@ ...@@ -215,6 +198,18 @@
background-color: $gray-light; background-color: $gray-light;
} }
} }
}
.mr-widget-body {
@include clearfix;
&.media > *:first-child {
margin-right: 10px;
}
.approve-btn {
margin-right: 5px;
}
h4 { h4 {
float: left; float: left;
...@@ -238,10 +233,6 @@ ...@@ -238,10 +233,6 @@
margin-right: 7px; margin-right: 7px;
} }
.approve-btn {
margin-right: 5px;
}
label { label {
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
} }
...@@ -334,17 +325,6 @@ ...@@ -334,17 +325,6 @@
} }
} }
.mini-pipeline-graph-dropdown-menu .mini-pipeline-graph-dropdown-item {
display: flex;
align-items: center;
.ci-status-text,
.ci-status-icon {
top: 0;
margin-right: 10px;
}
}
.mr-widget-help { .mr-widget-help {
padding: 10px 16px 10px 48px; padding: 10px 16px 10px 48px;
font-style: italic; font-style: italic;
......
...@@ -111,10 +111,34 @@ ...@@ -111,10 +111,34 @@
margin: auto; margin: auto;
align-items: center; align-items: center;
<<<<<<< HEAD
.md-area { .md-area {
=======
.icon {
margin-right: $issuable-warning-icon-margin;
}
+ .md-area {
>>>>>>> upstream/master
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
.disabled-comment {
border: 0;
border-radius: $label-border-radius;
padding-top: $gl-vert-padding;
padding-bottom: $gl-vert-padding;
.icon svg {
position: relative;
top: 2px;
margin-right: $btn-xs-side-margin;
width: $gl-font-size;
height: $gl-font-size;
fill: $orange-600;
}
}
} }
.sidebar-item-value { .sidebar-item-value {
......
...@@ -476,6 +476,10 @@ ul.notes { ...@@ -476,6 +476,10 @@ ul.notes {
float: none; float: none;
margin-left: 0; margin-left: 0;
} }
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
} }
.note-actions { .note-actions {
......
...@@ -850,6 +850,11 @@ a.linked-pipeline-mini-item { ...@@ -850,6 +850,11 @@ a.linked-pipeline-mini-item {
margin-left: 2px; margin-left: 2px;
display: inline-block; display: inline-block;
&::after {
content: '';
display: block;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
max-width: 60%; max-width: 60%;
} }
......
...@@ -298,3 +298,7 @@ ...@@ -298,3 +298,7 @@
width: 100%; width: 100%;
} }
} }
.multi-file-table-col-name {
width: 350px;
}
...@@ -78,10 +78,6 @@ input[type="checkbox"]:hover { ...@@ -78,10 +78,6 @@ input[type="checkbox"]:hover {
} }
.search-input-wrap { .search-input-wrap {
// Fallback if flexbox is not supported
display: inline-block;
width: 100%;
.search-icon, .search-icon,
.clear-icon { .clear-icon {
position: absolute; position: absolute;
......
...@@ -9,9 +9,7 @@ module IssuableActions ...@@ -9,9 +9,7 @@ module IssuableActions
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html
render show_view
end
format.json do format.json do
render json: serializer.represent(issuable, serializer: params[:serializer]) render json: serializer.represent(issuable, serializer: params[:serializer])
end end
...@@ -57,12 +55,11 @@ module IssuableActions ...@@ -57,12 +55,11 @@ module IssuableActions
def destroy def destroy
issuable.destroy issuable.destroy
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym TodoService.new.destroy_issuable(issuable, current_user)
TodoService.new.public_send(destroy_method, issuable, current_user) # rubocop:disable GitlabSecurity/PublicSend
name = issuable.human_class_name name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted." flash[:notice] = "The #{name} was successfully deleted."
index_path = polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]) index_path = polymorphic_path([parent, issuable.class])
respond_to do |format| respond_to do |format|
format.html { redirect_to index_path } format.html { redirect_to index_path }
...@@ -154,10 +151,6 @@ module IssuableActions ...@@ -154,10 +151,6 @@ module IssuableActions
end end
end end
def show_view
'show'
end
def serializer def serializer
raise NotImplementedError raise NotImplementedError
end end
...@@ -165,4 +158,8 @@ module IssuableActions ...@@ -165,4 +158,8 @@ module IssuableActions
def update_service def update_service
raise NotImplementedError raise NotImplementedError
end end
def parent
@project || @group
end
end end
...@@ -4,58 +4,44 @@ module IssuableCollections ...@@ -4,58 +4,44 @@ module IssuableCollections
include Gitlab::IssuableMetadata include Gitlab::IssuableMetadata
included do included do
helper_method :issues_finder helper_method :finder
helper_method :merge_requests_finder
end end
private private
def set_issues_index def set_issuables_index
@collection_type = "Issue" @issuables = issuables_collection
@issues = issues_collection @issuables = @issuables.page(params[:page])
@issues = @issues.page(params[:page]) @issuable_meta_data = issuable_meta_data(@issuables, collection_type)
@issuable_meta_data = issuable_meta_data(@issues, @collection_type) @total_pages = issuable_page_count
@total_pages = issues_page_count(@issues)
return if redirect_out_of_range(@issues, @total_pages) return if redirect_out_of_range(@total_pages)
if params[:label_name].present? if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute labels_params = { project_id: @project.id, title: params[:label_name] }
@labels = LabelsFinder.new(current_user, labels_params).execute
end end
@users = [] @users = []
end if params[:assignee_id].present?
assignee = User.find_by_id(params[:assignee_id])
def issues_collection @users.push(assignee) if assignee
issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace) end
end
def merge_requests_collection
merge_requests_finder.execute.preload(
:source_project,
:target_project,
:author,
:assignee,
:labels,
:milestone,
head_pipeline: :project,
target_project: :namespace,
merge_request_diff: :merge_request_diff_commits
)
end
def issues_finder if params[:author_id].present?
@issues_finder ||= issuable_finder_for(IssuesFinder) author = User.find_by_id(params[:author_id])
@users.push(author) if author
end
end end
def merge_requests_finder def issuables_collection
@merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder) finder.execute.preload(preload_for_collection)
end end
def redirect_out_of_range(relation, total_pages) def redirect_out_of_range(total_pages)
return false if total_pages.zero? return false if total_pages.zero?
out_of_range = relation.current_page > total_pages out_of_range = @issuables.current_page > total_pages
if out_of_range if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true))) redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
...@@ -64,12 +50,8 @@ module IssuableCollections ...@@ -64,12 +50,8 @@ module IssuableCollections
out_of_range out_of_range
end end
def issues_page_count(relation) def issuable_page_count
page_count_for_relation(relation, issues_finder.row_count) page_count_for_relation(@issuables, finder.row_count)
end
def merge_requests_page_count(relation)
page_count_for_relation(relation, merge_requests_finder.row_count)
end end
def page_count_for_relation(relation, row_count) def page_count_for_relation(relation, row_count)
...@@ -147,4 +129,31 @@ module IssuableCollections ...@@ -147,4 +129,31 @@ module IssuableCollections
else value else value
end end
end end
def finder
return @finder if defined?(@finder)
@finder = issuable_finder_for(@finder_type)
end
def collection_type
@collection_type ||= case finder
when IssuesFinder
'Issue'
when MergeRequestsFinder
'MergeRequest'
end
end
def preload_for_collection
@preload_for_collection ||= case collection_type
when 'Issue'
[:project, :author, :assignees, :labels, :milestone, project: :namespace]
when 'MergeRequest'
[
:source_project, :target_project, :author, :assignee, :labels, :milestone,
head_pipeline: :project, target_project: :namespace, merge_request_diff: :merge_request_diff_commits
]
end
end
end end
...@@ -3,14 +3,14 @@ module IssuesAction ...@@ -3,14 +3,14 @@ module IssuesAction
include IssuableCollections include IssuableCollections
def issues def issues
@label = issues_finder.labels.first @finder_type = IssuesFinder
@label = finder.labels.first
@issues = issues_collection @issues = issuables_collection
.non_archived .non_archived
.page(params[:page]) .page(params[:page])
@collection_type = "Issue" @issuable_meta_data = issuable_meta_data(@issues, collection_type)
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -3,13 +3,12 @@ module MergeRequestsAction ...@@ -3,13 +3,12 @@ module MergeRequestsAction
include IssuableCollections include IssuableCollections
def merge_requests def merge_requests
@label = merge_requests_finder.labels.first @finder_type = MergeRequestsFinder
@label = finder.labels.first
@merge_requests = merge_requests_collection @merge_requests = issuables_collection.page(params[:page])
.page(params[:page])
@collection_type = "MergeRequest" @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
end end
private private
......
...@@ -109,6 +109,8 @@ module NotesActions ...@@ -109,6 +109,8 @@ module NotesActions
diff_discussion_html: diff_discussion_html(discussion), diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion) discussion_html: discussion_html(discussion)
) )
attrs[:discussion_line_code] = discussion.line_code if discussion.diff_discussion?
end end
end end
else else
......
...@@ -3,10 +3,16 @@ class MetricsController < ActionController::Base ...@@ -3,10 +3,16 @@ class MetricsController < ActionController::Base
protect_from_forgery with: :exception protect_from_forgery with: :exception
before_action :validate_prometheus_metrics
def index def index
render text: metrics_service.metrics_text, content_type: 'text/plain; version=0.0.4' response = if Gitlab::Metrics.prometheus_metrics_enabled?
metrics_service.metrics_text
else
help_page = help_page_url('administration/monitoring/prometheus/gitlab_metrics',
anchor: 'gitlab-prometheus-metrics'
)
"# Metrics are disabled, see: #{help_page}\n"
end
render text: response, content_type: 'text/plain; version=0.0.4'
end end
private private
...@@ -14,8 +20,4 @@ class MetricsController < ActionController::Base ...@@ -14,8 +20,4 @@ class MetricsController < ActionController::Base
def metrics_service def metrics_service
@metrics_service ||= MetricsService.new @metrics_service ||= MetricsService.new
end end
def validate_prometheus_metrics
render_404 unless Gitlab::Metrics.prometheus_metrics_enabled?
end
end end
class Projects::Clusters::ApplicationsController < Projects::ApplicationController
before_action :cluster
before_action :application_class, only: [:create]
before_action :authorize_read_cluster!
before_action :authorize_create_cluster!, only: [:create]
def create
Clusters::Applications::ScheduleInstallationService.new(project, current_user,
application_class: @application_class,
cluster: @cluster).execute
head :no_content
rescue StandardError
head :bad_request
end
private
def cluster
@cluster ||= project.clusters.find(params[:id]) || render_404
end
def application_class
@application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404
end
end
class Projects::ClustersController < Projects::ApplicationController class Projects::ClustersController < Projects::ApplicationController
before_action :cluster, except: [:login, :index, :new, :create] before_action :cluster, except: [:login, :index, :new, :new_gcp, :create]
before_action :authorize_read_cluster! before_action :authorize_read_cluster!
before_action :authorize_create_cluster!, only: [:new, :create] before_action :authorize_create_cluster!, only: [:new, :new_gcp, :create]
before_action :authorize_google_api, only: [:new, :create] before_action :authorize_google_api, only: [:new_gcp, :create]
before_action :authorize_update_cluster!, only: [:update] before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy] before_action :authorize_admin_cluster!, only: [:destroy]
...@@ -16,7 +16,7 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::ClustersController < Projects::ApplicationController
def login def login
begin begin
state = generate_session_key_redirect(namespace_project_clusters_url.to_s) state = generate_session_key_redirect(providers_gcp_new_namespace_project_clusters_url.to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new( @authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url, nil, callback_google_api_auth_url,
...@@ -27,18 +27,23 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -27,18 +27,23 @@ class Projects::ClustersController < Projects::ApplicationController
end end
def new def new
@cluster = project.build_cluster end
def new_gcp
@cluster = Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end
end end
def create def create
@cluster = Ci::CreateClusterService @cluster = Clusters::CreateService
.new(project, current_user, create_params) .new(project, current_user, create_params)
.execute(token_in_session) .execute(token_in_session)
if @cluster.persisted? if @cluster.persisted?
redirect_to project_cluster_path(project, @cluster) redirect_to project_cluster_path(project, @cluster)
else else
render :new render :new_gcp
end end
end end
...@@ -58,7 +63,7 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -58,7 +63,7 @@ class Projects::ClustersController < Projects::ApplicationController
end end
def update def update
Ci::UpdateClusterService Clusters::UpdateService
.new(project, current_user, update_params) .new(project, current_user, update_params)
.execute(cluster) .execute(cluster)
...@@ -88,19 +93,19 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -88,19 +93,19 @@ class Projects::ClustersController < Projects::ApplicationController
def create_params def create_params
params.require(:cluster).permit( params.require(:cluster).permit(
:gcp_project_id, :enabled,
:gcp_cluster_zone, :name,
:gcp_cluster_name, :provider_type,
:gcp_cluster_size, provider_gcp_attributes: [
:gcp_machine_type, :gcp_project_id,
:project_namespace, :zone,
:enabled) :num_nodes,
:machine_type
])
end end
def update_params def update_params
params.require(:cluster).permit( params.require(:cluster).permit(:enabled)
:project_namespace,
:enabled)
end end
def authorize_google_api def authorize_google_api
......
...@@ -16,6 +16,8 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -16,6 +16,8 @@ class Projects::CommitController < Projects::ApplicationController
before_action :define_note_vars, only: [:show, :diff_for_path] before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
BRANCH_SEARCH_LIMIT = 1000
def show def show
apply_diff_view_cookie! apply_diff_view_cookie!
...@@ -56,8 +58,14 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -56,8 +58,14 @@ class Projects::CommitController < Projects::ApplicationController
end end
def branches def branches
@branches = @project.repository.branch_names_contains(commit.id) # branch_names_contains/tag_names_contains can take a long time when there are thousands of
@tags = @project.repository.tag_names_contains(commit.id) # branches/tags - each `git branch --contains xxx` request can consume a cpu core.
# so only do the query when there are a manageable number of branches/tags
@branches_limit_exceeded = @project.repository.branch_count > BRANCH_SEARCH_LIMIT
@branches = @branches_limit_exceeded ? [] : @project.repository.branch_names_contains(commit.id)
@tags_limit_exceeded = @project.repository.tag_count > BRANCH_SEARCH_LIMIT
@tags = @tags_limit_exceeded ? [] : @project.repository.tag_names_contains(commit.id)
render layout: false render layout: false
end end
......
...@@ -9,8 +9,13 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -9,8 +9,13 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:new, :export_csv] prepend_before_action :authenticate_user!, only: [:new, :export_csv]
before_action :check_issues_available! before_action :check_issues_available!
<<<<<<< HEAD
before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv] before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv]
before_action :set_issues_index, only: [:index] before_action :set_issues_index, only: [:index]
=======
before_action :issue, except: [:index, :new, :create, :bulk_update]
before_action :set_issuables_index, only: [:index]
>>>>>>> upstream/master
# Allow write(create) issue # Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
...@@ -26,15 +31,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -26,15 +31,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html respond_to :html
def index def index
if params[:assignee_id].present? @issues = @issuables
assignee = User.find_by_id(params[:assignee_id])
@users.push(assignee) if assignee
end
if params[:author_id].present?
author = User.find_by_id(params[:author_id])
@users.push(author) if author
end
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -254,4 +251,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -254,4 +251,9 @@ class Projects::IssuesController < Projects::ApplicationController
update_params = issue_params.merge(spammable_params) update_params = issue_params.merge(spammable_params)
Issues::UpdateService.new(project, current_user, update_params) Issues::UpdateService.new(project, current_user, update_params)
end end
def set_issuables_index
@finder_type = IssuesFinder
super
end
end end
...@@ -4,7 +4,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -4,7 +4,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
before_action :check_merge_requests_available! before_action :check_merge_requests_available!
before_action :merge_request before_action :merge_request
before_action :authorize_read_merge_request! before_action :authorize_read_merge_request!
before_action :ensure_ref_fetched
private private
...@@ -12,12 +11,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -12,12 +11,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
@issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id]) @issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end end
# Make sure merge requests created before 8.0
# have head file in refs/merge-requests/
def ensure_ref_fetched
@merge_request.ensure_ref_fetched if Gitlab::Database.read_write?
end
def merge_request_params def merge_request_params
params.require(:merge_request).permit(merge_request_params_attributes) params.require(:merge_request).permit(merge_request_params_attributes)
end end
......
...@@ -6,7 +6,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -6,7 +6,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
prepend ::EE::Projects::MergeRequests::CreationsController prepend ::EE::Projects::MergeRequests::CreationsController
skip_before_action :merge_request skip_before_action :merge_request
skip_before_action :ensure_ref_fetched
before_action :authorize_create_merge_request! before_action :authorize_create_merge_request!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path] before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create] before_action :build_merge_request, except: [:create]
......
...@@ -9,37 +9,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -9,37 +9,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
prepend ::EE::Projects::MergeRequestsController prepend ::EE::Projects::MergeRequestsController
skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :merge_request, only: [:index, :bulk_update]
skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
def index def index
@collection_type = "MergeRequest" @merge_requests = @issuables
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(merge_request_diff: :merge_request)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
@total_pages = merge_requests_page_count(@merge_requests)
return if redirect_out_of_range(@merge_requests, @total_pages)
if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
@labels = LabelsFinder.new(current_user, labels_params).execute
end
@users = []
if params[:assignee_id].present?
assignee = User.find_by_id(params[:assignee_id])
@users.push(assignee) if assignee
end
if params[:author_id].present?
author = User.find_by_id(params[:author_id])
@users.push(author) if author
end
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -54,7 +32,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -54,7 +32,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def show def show
validates_merge_request validates_merge_request
ensure_ref_fetched
close_merge_request_without_source_project close_merge_request_without_source_project
check_if_can_be_merged check_if_can_be_merged
...@@ -349,4 +326,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -349,4 +326,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@target_project = @merge_request.target_project @target_project = @merge_request.target_project
@target_branches = @merge_request.target_project.repository.branch_names @target_branches = @merge_request.target_project.repository.branch_names
end end
def set_issuables_index
@finder_type = MergeRequestsFinder
super
end
end end
...@@ -60,13 +60,19 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -60,13 +60,19 @@ class Projects::RefsController < Projects::ApplicationController
file = @path ? File.join(@path, content.name) : content.name file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file) last_commit = @repo.last_commit_for_path(@commit.id, file)
commit_path = project_commit_path(@project, last_commit) if last_commit commit_path = project_commit_path(@project, last_commit) if last_commit
<<<<<<< HEAD
path_lock = show_path_locks && @project.find_path_lock(file) path_lock = show_path_locks && @project.find_path_lock(file)
=======
>>>>>>> upstream/master
{ {
file_name: content.name, file_name: content.name,
commit: last_commit, commit: last_commit,
type: content.type, type: content.type,
<<<<<<< HEAD
lock_label: path_lock && text_label_for_lock(path_lock, file), lock_label: path_lock && text_label_for_lock(path_lock, file),
=======
>>>>>>> upstream/master
commit_path: commit_path commit_path: commit_path
} }
end end
......
...@@ -278,7 +278,8 @@ class ProjectsController < Projects::ApplicationController ...@@ -278,7 +278,8 @@ class ProjectsController < Projects::ApplicationController
@project_wiki = @project.wiki @project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id]) @wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user) elsif @project.feature_available?(:issues, current_user)
@issues = issues_collection.page(params[:page]) @finder_type = IssuesFinder
@issues = issuables_collection.page(params[:page])
@collection_type = 'Issue' @collection_type = 'Issue'
@issuable_meta_data = issuable_meta_data(@issues, @collection_type) @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
end end
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
# Anonymous users will never return any `owned` groups. They will return all # Anonymous users will never return any `owned` groups. They will return all
# public groups instead, even if `all_available` is set to false. # public groups instead, even if `all_available` is set to false.
class GroupsFinder < UnionFinder class GroupsFinder < UnionFinder
include CustomAttributesFilter
def initialize(current_user = nil, params = {}) def initialize(current_user = nil, params = {})
@current_user = current_user @current_user = current_user
@params = params @params = params
...@@ -22,8 +24,12 @@ class GroupsFinder < UnionFinder ...@@ -22,8 +24,12 @@ class GroupsFinder < UnionFinder
def execute def execute
items = all_groups.map do |item| items = all_groups.map do |item|
by_parent(item) item = by_parent(item)
item = by_custom_attributes(item)
item
end end
find_union(items, Group).with_route.order_id_desc find_union(items, Group).with_route.order_id_desc
end end
......
...@@ -18,6 +18,8 @@ ...@@ -18,6 +18,8 @@
# non_archived: boolean # non_archived: boolean
# #
class ProjectsFinder < UnionFinder class ProjectsFinder < UnionFinder
include CustomAttributesFilter
attr_accessor :params attr_accessor :params
attr_reader :current_user, :project_ids_relation attr_reader :current_user, :project_ids_relation
...@@ -44,6 +46,7 @@ class ProjectsFinder < UnionFinder ...@@ -44,6 +46,7 @@ class ProjectsFinder < UnionFinder
collection = by_tags(collection) collection = by_tags(collection)
collection = by_search(collection) collection = by_search(collection)
collection = by_archived(collection) collection = by_archived(collection)
collection = by_custom_attributes(collection)
sort(collection) sort(collection)
end end
......
...@@ -60,23 +60,33 @@ module CommitsHelper ...@@ -60,23 +60,33 @@ module CommitsHelper
branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop
end end
# Returns a link formatted as a commit branch link
def commit_branch_link(url, text)
link_to(url, class: 'label label-gray ref-name branch-link') do
icon('code-fork') + " #{text}"
end
end
# Returns the sorted alphabetically links to branches, separated by a comma # Returns the sorted alphabetically links to branches, separated by a comma
def commit_branches_links(project, branches) def commit_branches_links(project, branches)
branches.sort.map do |branch| branches.sort.map do |branch|
link_to(project_ref_path(project, branch), class: "label label-gray ref-name") do commit_branch_link(project_ref_path(project, branch), branch)
icon('code-fork') + " #{branch}" end.join(' ').html_safe
end end
end.join(" ").html_safe
# Returns a link formatted as a commit tag link
def commit_tag_link(url, text)
link_to(url, class: 'label label-gray ref-name') do
icon('tag') + " #{text}"
end
end end
# Returns the sorted links to tags, separated by a comma # Returns the sorted links to tags, separated by a comma
def commit_tags_links(project, tags) def commit_tags_links(project, tags)
sorted = VersionSorter.rsort(tags) sorted = VersionSorter.rsort(tags)
sorted.map do |tag| sorted.map do |tag|
link_to(project_ref_path(project, tag), class: "label label-gray ref-name") do commit_tag_link(project_ref_path(project, tag), tag)
icon('tag') + " #{tag}" end.join(' ').html_safe
end
end.join(" ").html_safe
end end
def link_to_browse_code(project, commit) def link_to_browse_code(project, commit)
......
...@@ -250,8 +250,6 @@ module IssuablesHelper ...@@ -250,8 +250,6 @@ module IssuablesHelper
end end
def issuables_count_for_state(issuable_type, state) def issuables_count_for_state(issuable_type, state)
finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend
Gitlab::IssuablesCountForState.new(finder)[state] Gitlab::IssuablesCountForState.new(finder)[state]
end end
......
...@@ -226,7 +226,7 @@ module MarkupHelper ...@@ -226,7 +226,7 @@ module MarkupHelper
data: data, data: data,
title: options[:title], title: options[:title],
aria: { label: options[:title] } do aria: { label: options[:title] } do
icon(options[:icon]) sprite_icon(options[:icon])
end end
end end
......
module Clusters
module Applications
class Helm < ActiveRecord::Base
self.table_name = 'clusters_applications_helm'
include ::Clusters::Concerns::ApplicationStatus
belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION
validates :cluster, presence: true
after_initialize :set_initial_status
def self.application_name
self.to_s.demodulize.underscore
end
def set_initial_status
return unless not_installable?
self.status = 'installable' if cluster&.platform_kubernetes_active?
end
def name
self.class.application_name
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, true)
end
end
end
end
module Clusters
module Applications
class Ingress < ActiveRecord::Base
self.table_name = 'clusters_applications_ingress'
include ::Clusters::Concerns::ApplicationStatus
belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
validates :cluster, presence: true
default_value_for :ingress_type, :nginx
default_value_for :version, :nginx
after_initialize :set_initial_status
enum ingress_type: {
nginx: 1
}
def self.application_name
self.to_s.demodulize.underscore
end
def set_initial_status
return unless not_installable?
self.status = 'installable' if cluster&.application_helm_installed?
end
def name
self.class.application_name
end
def chart
'stable/nginx-ingress'
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, false, chart)
end
end
end
end
module Clusters
class Cluster < ActiveRecord::Base
include Presentable
self.table_name = 'clusters'
APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress
}.freeze
belongs_to :user
has_many :cluster_projects, class_name: 'Clusters::Project'
has_many :projects, through: :cluster_projects, class_name: '::Project'
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
# We have to ":destroy" it today to ensure that we clean also the Kubernetes Integration
has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
validates :name, cluster_name: true
validate :restrict_modification, on: :update
# TODO: Move back this into Clusters::Platforms::Kubernetes in 10.3
# We need callback here because `enabled` belongs to Clusters::Cluster
# Callbacks in Clusters::Platforms::Kubernetes will not be called after update
after_save :update_kubernetes_integration!
delegate :status, to: :provider, allow_nil: true
delegate :status_reason, to: :provider, allow_nil: true
delegate :on_creation?, to: :provider, allow_nil: true
delegate :update_kubernetes_integration!, to: :platform, allow_nil: true
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
enum platform_type: {
kubernetes: 1
}
enum provider_type: {
user: 0,
gcp: 1
}
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
def status_name
if provider
provider.status_name
else
:created
end
end
def applications
[
application_helm || build_application_helm,
application_ingress || build_application_ingress
]
end
def provider
return provider_gcp if gcp?
end
def platform
return platform_kubernetes if kubernetes?
end
def first_project
return @first_project if defined?(@first_project)
@first_project = projects.first
end
alias_method :project, :first_project
def kubeclient
platform_kubernetes.kubeclient if kubernetes?
end
private
def restrict_modification
if provider&.on_creation?
errors.add(:base, "cannot modify during creation")
return false
end
true
end
end
end
module Clusters
module Concerns
module ApplicationStatus
extend ActiveSupport::Concern
included do
state_machine :status, initial: :not_installable do
state :not_installable, value: -2
state :errored, value: -1
state :installable, value: 0
state :scheduled, value: 1
state :installing, value: 2
state :installed, value: 3
event :make_scheduled do
transition [:installable, :errored] => :scheduled
end
event :make_installing do
transition [:scheduled] => :installing
end
event :make_installed do
transition [:installing] => :installed
end
event :make_errored do
transition any => :errored
end
before_transition any => [:scheduled] do |app_status, _|
app_status.status_reason = nil
end
before_transition any => [:errored] do |app_status, transition|
status_reason = transition.args.first
app_status.status_reason = status_reason if status_reason
end
end
end
end
end
end
module Clusters
module Platforms
class Kubernetes < ActiveRecord::Base
self.table_name = 'cluster_platforms_kubernetes'
belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
attr_encrypted :password,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
attr_encrypted :token,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
before_validation :enforce_namespace_to_lower_case
validates :namespace,
allow_blank: true,
length: 1..63,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
# We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
validates :api_url, url: true, presence: true
validates :token, presence: true
# TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes
after_destroy :destroy_kubernetes_integration!
alias_attribute :ca_pem, :ca_cert
delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true
class << self
def namespace_for_project(project)
"#{project.path}-#{project.id}"
end
end
def actual_namespace
if namespace.present?
namespace
else
default_namespace
end
end
def default_namespace
self.class.namespace_for_project(project) if project
end
def kubeclient
@kubeclient ||= kubernetes_service.kubeclient if manages_kubernetes_service?
end
def update_kubernetes_integration!
raise 'Kubernetes service already configured' unless manages_kubernetes_service?
# This is neccesary, otheriwse enabled? returns true even though cluster updated with enabled: false
cluster.reload
ensure_kubernetes_service&.update!(
active: enabled?,
api_url: api_url,
namespace: namespace,
token: token,
ca_pem: ca_cert
)
end
def active?
manages_kubernetes_service?
end
private
def enforce_namespace_to_lower_case
self.namespace = self.namespace&.downcase
end
# TODO: glue code till we migrate Kubernetes Service into Platforms::Kubernetes class
def manages_kubernetes_service?
return true unless kubernetes_service&.active?
kubernetes_service.api_url == api_url
end
def destroy_kubernetes_integration!
return unless manages_kubernetes_service?
kubernetes_service&.destroy!
end
def kubernetes_service
@kubernetes_service ||= project&.kubernetes_service
end
def ensure_kubernetes_service
@kubernetes_service ||= kubernetes_service || project&.build_kubernetes_service
end
end
end
end
module Clusters
class Project < ActiveRecord::Base
self.table_name = 'cluster_projects'
belongs_to :cluster, class_name: 'Clusters::Cluster'
belongs_to :project, class_name: '::Project'
end
end
module Clusters
module Providers
class Gcp < ActiveRecord::Base
self.table_name = 'cluster_providers_gcp'
belongs_to :cluster, inverse_of: :provider_gcp, class_name: 'Clusters::Cluster'
default_value_for :zone, 'us-central1-a'
default_value_for :num_nodes, 3
default_value_for :machine_type, 'n1-standard-2'
attr_encrypted :access_token,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
validates :gcp_project_id,
length: 1..63,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
validates :zone, presence: true
validates :num_nodes,
presence: true,
numericality: {
only_integer: true,
greater_than: 0
}
state_machine :status, initial: :scheduled do
state :scheduled, value: 1
state :creating, value: 2
state :created, value: 3
state :errored, value: 4
event :make_creating do
transition any - [:creating] => :creating
end
event :make_created do
transition any - [:created] => :created
end
event :make_errored do
transition any - [:errored] => :errored
end
before_transition any => [:errored, :created] do |provider|
provider.access_token = nil
provider.operation_id = nil
end
before_transition any => [:creating] do |provider, transition|
operation_id = transition.args.first
raise ArgumentError.new('operation_id is required') unless operation_id.present?
provider.operation_id = operation_id
end
before_transition any => [:errored] do |provider, transition|
status_reason = transition.args.first
provider.status_reason = status_reason if status_reason
end
end
def on_creation?
scheduled? || creating?
end
def api_client
return unless access_token
@api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil)
end
end
end
end
...@@ -14,7 +14,6 @@ class CommitStatus < ActiveRecord::Base ...@@ -14,7 +14,6 @@ class CommitStatus < ActiveRecord::Base
delegate :sha, :short_sha, to: :pipeline delegate :sha, :short_sha, to: :pipeline
validates :pipeline, presence: true, unless: :importing? validates :pipeline, presence: true, unless: :importing?
validates :name, presence: true, unless: :importing? validates :name, presence: true, unless: :importing?
alias_attribute :author, :user alias_attribute :author, :user
...@@ -46,6 +45,17 @@ class CommitStatus < ActiveRecord::Base ...@@ -46,6 +45,17 @@ class CommitStatus < ActiveRecord::Base
runner_system_failure: 4 runner_system_failure: 4
} }
##
# We still create some CommitStatuses outside of CreatePipelineService.
#
# These are pages deployments and external statuses.
#
before_create unless: :importing? do
Ci::EnsureStageService.new(project, user).execute(self) do |stage|
self.run_after_commit { StageUpdateWorker.perform_async(stage.id) }
end
end
state_machine :status do state_machine :status do
event :process do event :process do
transition [:skipped, :manual] => :created transition [:skipped, :manual] => :created
......
...@@ -21,8 +21,8 @@ module IgnorableColumn ...@@ -21,8 +21,8 @@ module IgnorableColumn
@ignored_columns ||= Set.new @ignored_columns ||= Set.new
end end
def ignore_column(name) def ignore_column(*names)
ignored_columns << name.to_s ignored_columns.merge(names.map(&:to_s))
end end
end end
end end
...@@ -17,6 +17,8 @@ module Issuable ...@@ -17,6 +17,8 @@ module Issuable
include Importable include Importable
include Editable include Editable
include AfterCommitQueue include AfterCommitQueue
include Sortable
include CreatedAtFilterable
# This object is used to gather issuable meta data for displaying # This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests # upvotes, downvotes, notes and closing merge requests count for issues and merge requests
......
...@@ -30,7 +30,6 @@ class Environment < ActiveRecord::Base ...@@ -30,7 +30,6 @@ class Environment < ActiveRecord::Base
message: Gitlab::Regex.environment_slug_regex_message } message: Gitlab::Regex.environment_slug_regex_message }
validates :external_url, validates :external_url,
uniqueness: { scope: :project_id },
length: { maximum: 255 }, length: { maximum: 255 },
allow_nil: true, allow_nil: true,
addressable_url: true addressable_url: true
......
module Gcp
class Cluster < ActiveRecord::Base
extend Gitlab::Gcp::Model
include Presentable
belongs_to :project, inverse_of: :cluster
belongs_to :user
belongs_to :service
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
default_value_for :gcp_cluster_zone, 'us-central1-a'
default_value_for :gcp_cluster_size, 3
default_value_for :gcp_machine_type, 'n1-standard-4'
attr_encrypted :password,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
attr_encrypted :kubernetes_token,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
attr_encrypted :gcp_token,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
validates :gcp_project_id,
length: 1..63,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
validates :gcp_cluster_name,
length: 1..63,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
validates :gcp_cluster_zone, presence: true
validates :gcp_cluster_size,
presence: true,
numericality: {
only_integer: true,
greater_than: 0
}
validates :project_namespace,
allow_blank: true,
length: 1..63,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
# if we do not do status transition we prevent change
validate :restrict_modification, on: :update, unless: :status_changed?
state_machine :status, initial: :scheduled do
state :scheduled, value: 1
state :creating, value: 2
state :created, value: 3
state :errored, value: 4
event :make_creating do
transition any - [:creating] => :creating
end
event :make_created do
transition any - [:created] => :created
end
event :make_errored do
transition any - [:errored] => :errored
end
before_transition any => [:errored, :created] do |cluster|
cluster.gcp_token = nil
cluster.gcp_operation_id = nil
end
before_transition any => [:errored] do |cluster, transition|
status_reason = transition.args.first
cluster.status_reason = status_reason if status_reason
end
end
def project_namespace_placeholder
"#{project.path}-#{project.id}"
end
def on_creation?
scheduled? || creating?
end
def api_url
'https://' + endpoint if endpoint
end
def restrict_modification
if on_creation?
errors.add(:base, "cannot modify during creation")
return false
end
true
end
end
end
...@@ -30,6 +30,7 @@ class Group < Namespace ...@@ -30,6 +30,7 @@ class Group < Namespace
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel' has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable' has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute'
has_many :ldap_group_links, foreign_key: 'group_id', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :ldap_group_links, foreign_key: 'group_id', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :hooks, dependent: :destroy, class_name: 'GroupHook' # rubocop:disable Cop/ActiveRecordDependent has_many :hooks, dependent: :destroy, class_name: 'GroupHook' # rubocop:disable Cop/ActiveRecordDependent
......
class GroupCustomAttribute < ActiveRecord::Base
belongs_to :group
validates :group, :key, :value, presence: true
validates :key, uniqueness: { scope: [:group_id] }
end
...@@ -8,12 +8,10 @@ class Issue < ActiveRecord::Base ...@@ -8,12 +8,10 @@ class Issue < ActiveRecord::Base
include Issuable include Issuable
include Noteable include Noteable
include Referable include Referable
include Sortable
include Spammable include Spammable
include Elastic::IssuesSearch include Elastic::IssuesSearch
include FasterCacheKeys include FasterCacheKeys
include RelativePositioning include RelativePositioning
include CreatedAtFilterable
include TimeTrackable include TimeTrackable
WEIGHT_RANGE = 1..9 WEIGHT_RANGE = 1..9
......
...@@ -3,13 +3,16 @@ class MergeRequest < ActiveRecord::Base ...@@ -3,13 +3,16 @@ class MergeRequest < ActiveRecord::Base
include Issuable include Issuable
include Noteable include Noteable
include Referable include Referable
<<<<<<< HEAD
include Sortable include Sortable
include Elastic::MergeRequestsSearch include Elastic::MergeRequestsSearch
=======
>>>>>>> upstream/master
include IgnorableColumn include IgnorableColumn
include CreatedAtFilterable
include TimeTrackable include TimeTrackable
ignore_column :locked_at ignore_column :locked_at,
:ref_fetched
include ::EE::MergeRequest include ::EE::MergeRequest
...@@ -450,7 +453,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -450,7 +453,7 @@ class MergeRequest < ActiveRecord::Base
end end
def create_merge_request_diff def create_merge_request_diff
fetch_ref fetch_ref!
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
...@@ -836,29 +839,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -836,29 +839,14 @@ class MergeRequest < ActiveRecord::Base
end end
end end
def fetch_ref def fetch_ref!
write_ref target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
update_column(:ref_fetched, true)
end end
def ref_path def ref_path
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head" "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end end
def ref_fetched?
super ||
begin
computed_value = project.repository.ref_exists?(ref_path)
update_column(:ref_fetched, true) if computed_value
computed_value
end
end
def ensure_ref_fetched
fetch_ref unless ref_fetched?
end
def in_locked_state def in_locked_state
begin begin
lock_mr lock_mr
...@@ -1004,10 +992,4 @@ class MergeRequest < ActiveRecord::Base ...@@ -1004,10 +992,4 @@ class MergeRequest < ActiveRecord::Base
project.merge_requests.merged.where(author_id: author_id).empty? project.merge_requests.merged.where(author_id: author_id).empty?
end end
private
def write_ref
target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path)
end
end end
...@@ -37,7 +37,7 @@ class Namespace < ActiveRecord::Base ...@@ -37,7 +37,7 @@ class Namespace < ActiveRecord::Base
validates :path, validates :path,
presence: true, presence: true,
length: { maximum: 255 }, length: { maximum: 255 },
dynamic_path: true namespace_path: true
validate :nesting_level_allowed validate :nesting_level_allowed
......
...@@ -190,7 +190,10 @@ class Project < ActiveRecord::Base ...@@ -190,7 +190,10 @@ class Project < ActiveRecord::Base
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics' has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project
has_one :cluster_project, class_name: 'Clusters::Project'
has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry, # Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy # which is not managed by the DB. Hence we're still using dependent: :destroy
...@@ -217,6 +220,7 @@ class Project < ActiveRecord::Base ...@@ -217,6 +220,7 @@ class Project < ActiveRecord::Base
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_one :auto_devops, class_name: 'ProjectAutoDevops' has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :project_feature, update_only: true
...@@ -244,10 +248,8 @@ class Project < ActiveRecord::Base ...@@ -244,10 +248,8 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message } message: Gitlab::Regex.project_name_regex_message }
validates :path, validates :path,
presence: true, presence: true,
dynamic_path: true, project_path: true,
length: { maximum: 255 }, length: { maximum: 255 },
format: { with: Gitlab::PathRegex.project_path_format_regex,
message: Gitlab::PathRegex.project_path_format_message },
uniqueness: { scope: :namespace_id } uniqueness: { scope: :namespace_id }
validates :namespace, presence: true validates :namespace, presence: true
......
class ProjectCustomAttribute < ActiveRecord::Base
belongs_to :project
validates :project, :key, :value, presence: true
validates :key, uniqueness: { scope: [:project_id] }
end
...@@ -39,7 +39,7 @@ module ChatMessage ...@@ -39,7 +39,7 @@ module ChatMessage
private private
def message def message
if state == 'opened' if opened_issue?
"[#{project_link}] Issue #{state} by #{user_combined_name}" "[#{project_link}] Issue #{state} by #{user_combined_name}"
else else
"[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}" "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
......
...@@ -138,6 +138,10 @@ class KubernetesService < DeploymentService ...@@ -138,6 +138,10 @@ class KubernetesService < DeploymentService
{ pods: read_pods } { pods: read_pods }
end end
def kubeclient
@kubeclient ||= build_kubeclient!
end
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
private private
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment