Commit e2b759a2 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 34312-eslint-vue-plugin

* master: (140 commits)
  Add Gitter room link to I want to contribute since you always have questions
  Use workhorse 3.4.0
  chore: remove symbolic link
  Add memoization for properties
  Resolve "Allow QA tests to run with `CHROME_HEADLESS=false`"
  Resolve "Add graph value to hover"
  Fix slash commands dropdown description
  disables the shortcut to the issue boards when issues are disabled
  Fix static analysys
  Disable STI of ActiveRecord. Refactoring specs.
  Fix StaticSnalysys
  Fix change log
  Add changelog
  Revert bulk_insert and bring back AR insert(one by one)
  Add a new test for emptified params
  Use batch update for Service deactivation
  Fix query to look for proper unmanaged kubernetes service
  Fix static anylysy
  Use bulk_insert instead of AR create
  Opitmize migration process by using both unmanaged_kubernetes_service and kubernetes_service_without_template
  ...
parents 7ca5a3f9 df744610
...@@ -104,9 +104,13 @@ the remaining issues on the GitHub issue tracker. ...@@ -104,9 +104,13 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute! ## I want to contribute!
If you want to contribute to GitLab, [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight] is a great place to start. Issues with a lower weight (1 or 2) are deemed suitable for beginners. If you want to contribute to GitLab [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight]
These issues will be of reasonable size and challenge, for anyone to start is a great place to start. Issues with a lower weight (1 or 2) are deemed
contributing to GitLab. suitable for beginners. These issues will be of reasonable size and challenge,
for anyone to start contributing to GitLab. If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to
learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel
please consider we favor
[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution!
## Workflow labels ## Workflow labels
......
...@@ -78,7 +78,7 @@ gem 'github-linguist', '~> 4.7.0', require: 'linguist' ...@@ -78,7 +78,7 @@ gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API # API
gem 'grape', '~> 1.0' gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.6.0' gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# Disable strong_params so that Mash does not respond to :permitted? # Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes' gem 'hashie-forbidden_attributes'
...@@ -339,7 +339,7 @@ group :development, :test do ...@@ -339,7 +339,7 @@ group :development, :test do
gem 'rubocop', '~> 0.52.0' gem 'rubocop', '~> 0.52.0'
gem 'rubocop-rspec', '~> 1.20.1' gem 'rubocop-rspec', '~> 1.20.1'
gem 'scss_lint', '~> 0.54.0', require: false gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false gem 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.8.0', require: false gem 'flay', '~> 2.8.0', require: false
......
...@@ -651,7 +651,7 @@ GEM ...@@ -651,7 +651,7 @@ GEM
rack (>= 0.4) rack (>= 0.4)
rack-attack (4.4.1) rack-attack (4.4.1)
rack rack
rack-cors (0.4.0) rack-cors (1.0.2)
rack-oauth2 (1.2.3) rack-oauth2 (1.2.3)
activesupport (>= 2.3) activesupport (>= 2.3)
attr_required (>= 0.0.5) attr_required (>= 0.0.5)
...@@ -695,6 +695,9 @@ GEM ...@@ -695,6 +695,9 @@ GEM
rake rake
raindrops (0.18.0) raindrops (0.18.0)
rake (12.3.0) rake (12.3.0)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rbnacl (4.0.2) rbnacl (4.0.2)
...@@ -809,7 +812,11 @@ GEM ...@@ -809,7 +812,11 @@ GEM
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
sass (3.4.22) sass (3.5.5)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sass-rails (5.0.6) sass-rails (5.0.6)
railties (>= 4.0.0, < 6) railties (>= 4.0.0, < 6)
sass (~> 3.1) sass (~> 3.1)
...@@ -819,9 +826,9 @@ GEM ...@@ -819,9 +826,9 @@ GEM
sawyer (0.8.1) sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6) addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0) faraday (~> 0.8, < 1.0)
scss_lint (0.54.0) scss_lint (0.56.0)
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.4.20) sass (~> 3.5.3)
securecompare (1.0.0) securecompare (1.0.0)
seed-fu (2.3.6) seed-fu (2.3.6)
activerecord (>= 3.1) activerecord (>= 3.1)
...@@ -1126,7 +1133,7 @@ DEPENDENCIES ...@@ -1126,7 +1133,7 @@ DEPENDENCIES
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
rack-cors (~> 0.4.0) rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0) rack-proxy (~> 0.6.0)
rails (= 4.2.10) rails (= 4.2.10)
...@@ -1162,7 +1169,7 @@ DEPENDENCIES ...@@ -1162,7 +1169,7 @@ DEPENDENCIES
rugged (~> 0.26.0) rugged (~> 0.26.0)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.54.0) scss_lint (~> 0.56.0)
seed-fu (= 2.3.6) seed-fu (= 2.3.6)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5) selenium-webdriver (~> 3.5)
...@@ -1205,4 +1212,4 @@ DEPENDENCIES ...@@ -1205,4 +1212,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.16.0 1.16.1
{"iconCount":186,"spriteSize":84748,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","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","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","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-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","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"]} {"iconCount":189,"spriteSize":85766,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","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","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o-open","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","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-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","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","staged","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","unstaged","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.
<svg xmlns="http://www.w3.org/2000/svg" width="430" height="300"><g fill="none" fill-rule="evenodd" transform="translate(35 29)"><path fill="#EEE" fill-rule="nonzero" d="M90 23a2 2 0 1 1 0-4h10a2 2 0 0 1 0 4H90zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h1a11.98 11.98 0 0 1 9.457 4.612 2 2 0 0 1-3.151 2.464A7.981 7.981 0 0 0 331 23h-1zm9 11.39a2 2 0 0 1 4 0v10a2 2 0 0 1-4 0v-10zm0 180a2 2 0 1 1 4 0V223c0 .56-.038 1.114-.114 1.662a2 2 0 0 1-3.962-.55A8.21 8.21 0 0 0 339 223v-8.61zm-4.769 15.931a2 2 0 0 1 1.618 3.658A11.967 11.967 0 0 1 331 235h-5.782a2 2 0 0 1 0-4H331c1.13 0 2.224-.233 3.231-.679zm-19.013.679a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zM115 231a2 2 0 0 1 0 4h-10a2 2 0 0 1 0-4h10zm-26.2 4c.131-.646.2-1.315.2-2v-2h4a2 2 0 0 1 0 4h-4.2z"/><path fill="#EEE" fill-rule="nonzero" d="M103 211h258a6 6 0 0 0 6-6V63a6 6 0 0 0-6-6H166a5 5 0 0 1-5-5v-8.5a5.5 5.5 0 0 0-5.5-5.5H109a6 6 0 0 0-6 6v167zm62-167.5V52a1 1 0 0 0 1 1h195c5.523 0 10 4.477 10 10v142c0 5.523-4.477 10-10 10H99V44c0-5.523 4.477-10 10-10h46.5a9.5 9.5 0 0 1 9.5 9.5z"/><rect width="40" height="4" x="118" y="78" fill="#6B4FBB" rx="2"/><rect width="30" height="4" x="118" y="90" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="153" y="90" fill="#E1DBF1" rx="2"/><rect width="150" height="4" x="118" y="102" fill="#EFEDF8" rx="2"/><rect width="90" height="4" x="118" y="114" fill="#E1DBF1" rx="2"/><rect width="60" height="4" x="118" y="138" fill="#EFEDF8" rx="2"/><rect width="20" height="4" x="118" y="150" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="144" y="150" fill="#C3B8E3" rx="2"/><rect width="20" height="4" x="170" y="150" fill="#E1DBF1" rx="2"/><rect width="130" height="4" x="118" y="162" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="118" y="174" fill="#C3B8E3" rx="2"/><rect width="30" height="4" x="154" y="174" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="190" y="174" fill="#EFEDF8" rx="2"/><rect width="40" height="4" x="118" y="186" fill="#E1DBF1" rx="2"/><path fill="#F9F9F9" d="M89 24.292l11.434 19.326v170.326L89 226.336V24.292z"/><path fill="#EEE" fill-rule="nonzero" d="M89 229.286v-5.9l9.434-10.223V44.165L89 28.22v-7.856l13.434 22.707v171.655L89 229.286zM10 4a6 6 0 0 0-6 6v223a6 6 0 0 0 6 6h69a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h69c5.523 0 10 4.477 10 10v223c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><circle cx="25" cy="23" r="11" fill="#FEF0E8"/><path fill="#FEE1D3" d="M46 17h16a2 2 0 1 1 0 4H46a2 2 0 1 1 0-4zm0 8h27a2 2 0 1 1 0 4H46a2 2 0 1 1 0-4z"/><path fill="#EEE" d="M16 50h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-4 12h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H40a2 2 0 1 1 0-4zM26 78h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H40a2 2 0 1 1 0-4z"/><g transform="translate(14 110)"><rect width="8" height="8" fill="#FEE1D3" rx="2"/><rect width="28" height="4" x="14" y="2" fill="#FEF0E8" rx="2"/></g><path fill="#EEE" d="M16 140h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4z"/><g transform="translate(24 124)"><rect width="8" height="8" fill="#FEE1D3" rx="2"/><rect width="28" height="4" x="14" y="2" fill="#FEF0E8" rx="2"/></g><g fill="#FC6D26" transform="translate(24 92)"><rect width="8" height="8" rx="2"/><rect width="28" height="4" x="14" y="2" rx="2"/></g><path fill="#FDC4A8" fill-rule="nonzero" d="M152 50.5a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9zm0-3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="386" height="298" viewBox="0 0 386 298" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M4 51h16v15.997A5.003 5.003 0 0 1 15.003 72H8.997A5.005 5.005 0 0 1 4 66.997V51z"/><rect id="b" width="24" height="10" y="44" rx="3"/></defs><g fill="none" fill-rule="evenodd" transform="translate(0 3)"><g transform="rotate(15 23.151 968.24)"><rect width="53" height="44" fill="#FFF" stroke="#FDE5D8" stroke-width="3" stroke-linecap="round" rx="5"/><path fill="#FDE5D8" d="M29.5 28.3l2.758-3.861c.962-1.347 2.527-1.34 3.484 0l6.516 9.122c.962 1.347.399 2.439-1.252 2.439H17.994c-1.653 0-2.21-1.099-1.252-2.439l6.516-9.122c.962-1.347 2.527-1.34 3.484 0L29.5 28.3z" opacity=".6"/><circle cx="16" cy="16" r="6" fill="#FDB997"/></g><g transform="scale(-1 1) rotate(25 -75.08 -334.15)"><rect width="3" height="11" x="12.45" y="23.45" fill="#6B4FBB" transform="rotate(45 13.95 28.95)" rx="1.5"/><rect width="3" height="14" x="9.45" y="15.45" fill="#6B4FBB" transform="rotate(45 10.95 22.45)" rx="1.5"/><path fill="#FFF" stroke="#E1DCF1" stroke-width="3" d="M16 39.6C6.871 37.747 0 29.676 0 20 0 8.954 8.954 0 20 0s20 8.954 20 20c0 8.955-5.886 16.536-14 19.084v15.91A5.007 5.007 0 0 1 21 60c-2.761 0-5-2.244-5-5.006V39.6zm4-7.6c6.627 0 12-5.373 12-12S26.627 8 20 8 8 13.373 8 20s5.373 12 12 12z"/></g><g transform="scale(1 -1) rotate(-15 -383.616 -172.407)"><path stroke="#FDE5D8" stroke-width="3" d="M1.5 38.5h9V4c0-1.378-1.12-2.5-2.496-2.5H3.996A2.503 2.503 0 0 0 1.5 4v34.5z"/><rect width="2" height="27" x="5" y="7" fill="#FDA77D" opacity=".8" rx="1"/><path stroke="#FDE5D8" stroke-width="3" d="M2.427 41.553h7.146L6 48.699l-3.573-7.146z"/></g><g transform="rotate(-30 420.145 -545.422)"><path fill="#FFF" stroke="#FDE5D8" stroke-width="3" d="M9 3c0-1.657 1.347-3 3-3 1.657 0 3 1.352 3 3v43H9V3z"/><use fill="#FFF" xlink:href="#a"/><path stroke="#FDE5D8" stroke-width="3" d="M5.5 52.5v14.497A3.505 3.505 0 0 0 8.997 70.5h6.006a3.503 3.503 0 0 0 3.497-3.503V52.5h-13z"/><rect width="2" height="14" x="9" y="51" fill="#FDA77D" rx="1"/><rect width="2" height="14" x="13" y="51" fill="#FDA77D" rx="1"/><use fill="#FFF" xlink:href="#b"/><rect width="21" height="7" x="1.5" y="45.5" stroke="#FDE5D8" stroke-width="3" rx="3"/></g><g transform="translate(72 97.488)"><rect width="125" height="160" fill="#FFF" stroke="#E5E5E5" stroke-width="4" stroke-linecap="round" rx="10"/><rect width="125" height="160" x="125" fill="#FFF" stroke="#E5E5E5" stroke-width="4" stroke-linecap="round" rx="10"/><path fill="#FFF" stroke="#E5E5E5" stroke-width="4" d="M7 12.008C7 8.69 9.686 6 12.993 6H125v148H12.993C9.683 154 7 151.305 7 147.992V12.008zm236 0C243 8.69 240.314 6 237.007 6H125v148h112.007c3.31 0 5.993-2.695 5.993-6.008V12.008z" stroke-linecap="round"/><rect width="84" height="42" x="142" y="29" stroke="#EEE" stroke-width="4" rx="3"/><rect width="88" height="4" x="141" y="93" fill="#E5E5E5" rx="2"/><rect width="88" height="4" x="141" y="107" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="141" y="121" fill="#E5E5E5" rx="2"/><rect width="56" height="4" x="22" y="93" fill="#E5E5E5" rx="2"/><rect width="26" height="4" x="22" y="27" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="22" y="41" fill="#E5E5E5" rx="2"/><rect width="36" height="4" x="22" y="55" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="22" y="69" fill="#E5E5E5" rx="2"/><rect width="36" height="4" x="22" y="107" fill="#E5E5E5" rx="2"/><rect width="56" height="4" x="22" y="121" fill="#BFBFBF" rx="2"/></g><path stroke="#B5A7DD" stroke-width="2.5" d="M23.139 182.922l-1.347-.6a2.004 2.004 0 0 1-1.02-2.64l.815-1.831a1.995 1.995 0 0 1 2.645-1.01l1.308.583a9.959 9.959 0 0 1 2.177-1.876l-.376-1.402a2.004 2.004 0 0 1 1.41-2.455l1.937-.519a1.995 1.995 0 0 1 2.449 1.421l.375 1.402a9.959 9.959 0 0 1 2.824.536l.84-1.158a2.004 2.004 0 0 1 2.796-.448l1.622 1.178a1.995 1.995 0 0 1 .437 2.797l-.867 1.193a9.946 9.946 0 0 1 1.341 2.541l1.461-.05a2.004 2.004 0 0 1 2.075 1.926l.07 2.003a1.995 1.995 0 0 1-1.935 2.067l-1.445.05c-.256.93-.644 1.817-1.15 2.632l.944 1.087a2.004 2.004 0 0 1-.191 2.825l-1.513 1.315a1.995 1.995 0 0 1-2.824-.204l-.963-1.108a10.084 10.084 0 0 1-2.776.744l-.28 1.441a2.004 2.004 0 0 1-2.344 1.588l-1.967-.382a1.995 1.995 0 0 1-1.579-2.35l.275-1.414a10.044 10.044 0 0 1-2.312-1.704l-1.277.678a2.004 2.004 0 0 1-2.709-.822l-.94-1.77a1.995 1.995 0 0 1 .833-2.705l1.29-.687a9.946 9.946 0 0 1-.11-2.872zm10.98 4.93a4 4 0 1 0-2.07-7.727 4 4 0 0 0 2.07 7.728z"/><ellipse cx="197" cy="289.988" fill="#F9F9F9" rx="125" ry="4.5"/><path fill="#6B4FBB" d="M164 100.492a3.002 3.002 0 0 1 3.001-3.004H183a3.006 3.006 0 0 1 3.001 3.004v34.988c0 2.213-1.45 2.954-3.24 1.651l-7.76-5.643-7.76 5.643c-1.789 1.302-3.24.566-3.24-1.651v-34.988z"/><g opacity=".2"><path fill="#FC8A51" d="M5.747 234.768l-2.688 1.114c-1.017.422-1.803-.134-1.754-1.228l.128-2.907-1.115-2.688c-.422-1.017.135-1.803 1.229-1.754l2.907.128 2.687-1.115c1.018-.422 1.803.135 1.755 1.229l-.128 2.907 1.114 2.687c.422 1.018-.134 1.803-1.228 1.755l-2.907-.128zM191.564 37.953l-3.72.164c-1.326.059-1.992-.88-1.48-2.115l1.426-3.438-.164-3.72c-.059-1.326.88-1.992 2.115-1.48l3.438 1.426 3.72-.164c1.326-.059 1.992.88 1.48 2.114l-1.426 3.44.164 3.719c.059 1.326-.88 1.992-2.114 1.48l-3.44-1.426z"/><path fill="#6B4FBB" d="M348.789 75.876l-1.967-2.144c-.744-.812-.49-1.74.555-2.07l2.775-.873 2.144-1.967c.812-.744 1.74-.49 2.07.555l.873 2.775 1.967 2.144c.744.812.49 1.74-.555 2.07l-2.775.873-2.144 1.967c-.812.745-1.74.49-2.07-.555l-.873-2.775zm9.261 164.735l-2.907-.125c-1.1-.048-1.577-.884-1.07-1.855l1.344-2.58.126-2.908c.047-1.1.883-1.577 1.855-1.07l2.58 1.344 2.907.126c1.1.047 1.577.883 1.07 1.855l-1.344 2.58-.125 2.907c-.048 1.1-.884 1.577-1.856 1.07l-2.58-1.344zM88.789 75.876l-1.967-2.144c-.744-.812-.49-1.74.555-2.07l2.775-.873 2.144-1.967c.812-.744 1.74-.49 2.07.555l.873 2.775 1.967 2.144c.744.812.49 1.74-.555 2.07l-2.775.873-2.144 1.967c-.812.745-1.74.49-2.07-.555l-.873-2.775z"/></g></g></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
...@@ -57,12 +57,12 @@ class GfmAutoComplete { ...@@ -57,12 +57,12 @@ class GfmAutoComplete {
displayTpl(value) { displayTpl(value) {
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template; if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
let tpl = '<li>/${name}'; let tpl = '<li><span class="name">/${name}</span>';
if (value.aliases.length > 0) { if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>'; tpl += ' <small class="aliases">(or /<%- aliases.join(", /") %>)</small>';
} }
if (value.params.length > 0) { if (value.params.length > 0) {
tpl += ' <small><%- params.join(" ") %></small>'; tpl += ' <small class="params"><%- params.join(" ") %></small>';
} }
if (value.description !== '') { if (value.description !== '') {
tpl += '<small class="description"><i><%- description %></i></small>'; tpl += '<small class="description"><i><%- description %></i></small>';
......
<script> <script>
import { visitUrl } from '../../lib/utils/url_utility'; import { visitUrl } from '../../lib/utils/url_utility';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import itemCaret from './item_caret.vue'; import itemCaret from './item_caret.vue';
import itemTypeIcon from './item_type_icon.vue'; import itemTypeIcon from './item_type_icon.vue';
import itemStats from './item_stats.vue'; import itemStats from './item_stats.vue';
import itemActions from './item_actions.vue'; import itemActions from './item_actions.vue';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -84,11 +84,14 @@ ...@@ -84,11 +84,14 @@
:group="group" :group="group"
:parent-group="parentGroup" :parent-group="parentGroup"
/> />
<item-stats :item="group" /> <item-stats
:item="group"
/>
<div <div
class="folder-toggle-wrap" class="folder-toggle-wrap">
> <item-caret
<item-caret :is-group-open="group.isOpen" /> :is-group-open="group.isOpen"
/>
<item-type-icon <item-type-icon
:item-type="group.type" :item-type="group.type"
:is-group-open="group.isOpen" :is-group-open="group.isOpen"
...@@ -110,14 +113,13 @@ ...@@ -110,14 +113,13 @@
<identicon <identicon
v-else v-else
size-class="s24" size-class="s24"
:entity-id="group.id" :entity-id=group.id
:entity-name="group.name" :entity-name="group.name"
/> />
</a> </a>
</div> </div>
<div <div
class="title namespace-title" class="title namespace-title">
>
<a <a
v-tooltip v-tooltip
:href="group.relativePath" :href="group.relativePath"
...@@ -133,13 +135,14 @@ ...@@ -133,13 +135,14 @@
v-if="group.permission" v-if="group.permission"
class="user-access-role" class="user-access-role"
> >
{{ group.permission }} {{group.permission}}
</span> </span>
</div> </div>
<div <div
v-if="group.description" v-if="group.description"
class="description"> class="description">
{{ group.description }} <span v-html="group.description">
</span>
</div> </div>
</div> </div>
<group-folder <group-folder
......
...@@ -71,7 +71,7 @@ export default class GroupsStore { ...@@ -71,7 +71,7 @@ export default class GroupsStore {
id: rawGroupItem.id, id: rawGroupItem.id,
name: rawGroupItem.name, name: rawGroupItem.name,
fullName: rawGroupItem.full_name, fullName: rawGroupItem.full_name,
description: rawGroupItem.description, description: rawGroupItem.markdown_description,
visibility: rawGroupItem.visibility, visibility: rawGroupItem.visibility,
avatarUrl: rawGroupItem.avatar_url, avatarUrl: rawGroupItem.avatar_url,
relativePath: rawGroupItem.relative_path, relativePath: rawGroupItem.relative_path,
......
...@@ -9,14 +9,11 @@ import repoPreview from './repo_preview.vue'; ...@@ -9,14 +9,11 @@ import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue'; import repoEditor from './repo_editor.vue';
export default { export default {
components: { props: {
ideSidebar, emptyStateSvgPath: {
ideContextbar, type: String,
repoTabs, required: true,
repoFileButtons, },
ideStatusBar,
repoEditor,
repoPreview,
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -28,6 +25,15 @@ export default { ...@@ -28,6 +25,15 @@ export default {
'activeFile', 'activeFile',
]), ]),
}, },
components: {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
repoPreview,
},
mounted() { mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?'; const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => { window.onbeforeunload = (e) => {
...@@ -51,26 +57,39 @@ export default { ...@@ -51,26 +57,39 @@ export default {
class="multi-file-edit-pane" class="multi-file-edit-pane"
> >
<template <template
v-if="activeFile" v-if="activeFile">
> <repo-tabs/>
<repo-tabs />
<component <component
class="multi-file-edit-pane-content" class="multi-file-edit-pane-content"
:is="currentBlobView" :is="currentBlobView"
/> />
<repo-file-buttons /> <repo-file-buttons/>
<ide-status-bar <ide-status-bar
:file="selectedFile" :file="selectedFile"/>
/>
</template> </template>
<template <template
v-else v-else>
>
<div class="ide-empty-state"> <div class="ide-empty-state">
<h2 class="clgray">Welcome to the GitLab IDE</h2> <div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath">
</div>
</div>
<div class="col-xs-12">
<div class="text-content text-center">
<h4>
Welcome to the GitLab IDE
</h4>
<p>
You can select a file in the left sidebar to begin editing and use the right sidebar to commit your changes.
</p>
</div>
</div>
</div>
</div> </div>
</template> </template>
</div> </div>
<ide-contextbar /> <ide-contextbar/>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import RepoPreviousDirectory from './repo_prev_directory.vue'; import repoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue'; import repoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue'; import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import { treeList } from '../stores/utils'; import { treeList } from '../stores/utils';
export default { export default {
components: { components: {
'repo-previous-directory': RepoPreviousDirectory, repoPreviousDirectory,
'repo-file': RepoFile, repoFile,
'repo-loading-file': RepoLoadingFile, skeletonLoadingContainer,
}, },
props: { props: {
treeId: { treeId: {
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
}, },
computed: { computed: {
...mapState([ ...mapState([
'loading', 'trees',
'isRoot', 'isRoot',
]), ]),
...mapState({ ...mapState({
...@@ -34,28 +34,31 @@ ...@@ -34,28 +34,31 @@
return !this.isRoot && this.fetchedList.length; return !this.isRoot && this.fetchedList.length;
}, },
showLoading() { showLoading() {
return this.loading; if (this.trees[this.treeId]) {
return this.trees[this.treeId].loading;
}
return true;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<div class="ide-file-list"> <div class="ide-file-list">
<table class="table"> <table class="table">
<tbody <tbody
v-if="treeId" v-if="treeId">
>
<repo-previous-directory <repo-previous-directory
v-if="hasPreviousDirectory" v-if="hasPreviousDirectory"
/> />
<template v-if="showLoading"> <div
<repo-loading-file class="multi-file-loading-container"
v-for="n in 5" v-if="showLoading"
:key="n" v-for="n in 3"
/> :key="n">
</template> <skeleton-loading-container/>
</div>
<repo-file <repo-file
v-for="file in fetchedList" v-for="file in fetchedList"
:key="file.key" :key="file.key"
...@@ -64,5 +67,5 @@ ...@@ -64,5 +67,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</template> </template>
...@@ -3,12 +3,14 @@ import { mapState, mapActions } from 'vuex'; ...@@ -3,12 +3,14 @@ import { mapState, mapActions } from 'vuex';
import projectTree from './ide_project_tree.vue'; import projectTree from './ide_project_tree.vue';
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
import panelResizer from '../../vue_shared/components/panel_resizer.vue'; import panelResizer from '../../vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default { export default {
components: { components: {
projectTree, projectTree,
icon, icon,
panelResizer, panelResizer,
skeletonLoadingContainer,
}, },
data() { data() {
return { return {
...@@ -17,6 +19,7 @@ export default { ...@@ -17,6 +19,7 @@ export default {
}, },
computed: { computed: {
...mapState([ ...mapState([
'loading',
'projects', 'projects',
'leftPanelCollapsed', 'leftPanelCollapsed',
]), ]),
...@@ -32,6 +35,9 @@ export default { ...@@ -32,6 +35,9 @@ export default {
} }
return {}; return {};
}, },
showLoading() {
return this.loading;
},
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -63,6 +69,13 @@ export default { ...@@ -63,6 +69,13 @@ export default {
:style="panelStyle" :style="panelStyle"
> >
<div class="multi-file-commit-panel-inner"> <div class="multi-file-commit-panel-inner">
<div
class="multi-file-loading-container"
v-if="showLoading"
v-for="n in 3"
:key="n">
<skeleton-loading-container/>
</div>
<project-tree <project-tree
v-for="project in projects" v-for="project in projects"
:key="project.id" :key="project.id"
......
...@@ -8,9 +8,11 @@ export const getProjectData = ( ...@@ -8,9 +8,11 @@ export const getProjectData = (
{ namespace, projectId, force = false } = {}, { namespace, projectId, force = false } = {},
) => new Promise((resolve, reject) => { ) => new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) { if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, state);
service.getProjectData(namespace, projectId) service.getProjectData(namespace, projectId)
.then(res => res.data) .then(res => res.data)
.then((data) => { .then((data) => {
commit(types.TOGGLE_LOADING, state);
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data); resolve(data);
......
...@@ -30,6 +30,9 @@ ...@@ -30,6 +30,9 @@
shouldRenderContent() { shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length; return !this.isLoading && Object.keys(this.job).length;
}, },
jobStarted() {
return this.job.started;
},
}, },
watch: { watch: {
job() { job() {
...@@ -64,6 +67,7 @@ ...@@ -64,6 +67,7 @@
:user="job.user" :user="job.user"
:actions="actions" :actions="actions"
:has-sidebar-button="true" :has-sidebar-button="true"
:should-render-triggered-label="jobStarted"
/> />
<loading-icon <loading-icon
v-if="isLoading" v-if="isLoading"
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
import { axisLeft, axisBottom } from 'd3-axis'; import { axisLeft, axisBottom } from 'd3-axis';
import { max, extent } from 'd3-array'; import { max, extent } from 'd3-array';
import { select } from 'd3-selection'; import { select } from 'd3-selection';
import graphLegend from './graph/legend.vue'; import GraphLegend from './graph/legend.vue';
import graphFlag from './graph/flag.vue'; import GraphFlag from './graph/flag.vue';
import graphDeployment from './graph/deployment.vue'; import GraphDeployment from './graph/deployment.vue';
import graphPath from './graph/path.vue'; import GraphPath from './graph/path.vue';
import MonitoringMixin from '../mixins/monitoring_mixins'; import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import measurements from '../utils/measurements'; import measurements from '../utils/measurements';
...@@ -17,15 +17,6 @@ ...@@ -17,15 +17,6 @@
const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }; const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
export default { export default {
components: {
graphLegend,
graphFlag,
graphDeployment,
graphPath,
},
mixins: [MonitoringMixin],
props: { props: {
graphData: { graphData: {
type: Object, type: Object,
...@@ -54,6 +45,8 @@ ...@@ -54,6 +45,8 @@
}, },
}, },
mixins: [MonitoringMixin],
data() { data() {
return { return {
baseGraphHeight: 450, baseGraphHeight: 450,
...@@ -76,21 +69,25 @@ ...@@ -76,21 +69,25 @@
currentFlagPosition: 0, currentFlagPosition: 0,
showFlag: false, showFlag: false,
showFlagContent: false, showFlagContent: false,
showDeployInfo: true,
timeSeries: [], timeSeries: [],
realPixelRatio: 1,
}; };
}, },
components: {
GraphLegend,
GraphFlag,
GraphDeployment,
GraphPath,
},
computed: { computed: {
outerViewBox() { outerViewBox() {
return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
}, },
innerViewBox() { innerViewBox() {
if ((this.baseGraphWidth - 150) > 0) {
return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
}
return '0 0 0 0';
}, },
axisTransform() { axisTransform() {
...@@ -102,26 +99,10 @@ ...@@ -102,26 +99,10 @@
paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`, paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
}; };
}, },
},
watch: { deploymentFlagData() {
updateAspectRatio() { return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
if (this.updateAspectRatio) {
this.graphHeight = 450;
this.graphWidth = 600;
this.measurements = measurements.large;
this.draw();
eventHub.$emit('toggleAspectRatio');
}
}, },
hoverData() {
this.positionFlag();
},
},
mounted() {
this.draw();
}, },
methods: { methods: {
...@@ -142,6 +123,10 @@ ...@@ -142,6 +123,10 @@
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight; this.baseGraphHeight = this.graphHeight;
this.baseGraphWidth = this.graphWidth; this.baseGraphWidth = this.graphWidth;
// pixel offsets inside the svg and outside are not 1:1
this.realPixelRatio = (this.$refs.baseSvg.clientWidth / this.baseGraphWidth);
this.renderAxesPaths(); this.renderAxesPaths();
this.formatDeployments(); this.formatDeployments();
}, },
...@@ -212,6 +197,26 @@ ...@@ -212,6 +197,26 @@
}); // This will select all of the ticks once they're rendered }); // This will select all of the ticks once they're rendered
}, },
}, },
watch: {
updateAspectRatio() {
if (this.updateAspectRatio) {
this.graphHeight = 450;
this.graphWidth = 600;
this.measurements = measurements.large;
this.draw();
eventHub.$emit('toggleAspectRatio');
}
},
hoverData() {
this.positionFlag();
},
},
mounted() {
this.draw();
},
}; };
</script> </script>
...@@ -221,7 +226,7 @@ ...@@ -221,7 +226,7 @@
@mouseover="showFlagContent = true" @mouseover="showFlagContent = true"
@mouseleave="showFlagContent = false"> @mouseleave="showFlagContent = false">
<h5 class="text-center graph-title"> <h5 class="text-center graph-title">
{{ graphData.title }} {{graphData.title}}
</h5> </h5>
<div <div
class="prometheus-svg-container" class="prometheus-svg-container"
...@@ -231,12 +236,12 @@ ...@@ -231,12 +236,12 @@
ref="baseSvg"> ref="baseSvg">
<g <g
class="x-axis" class="x-axis"
:transform="axisTransform" :transform="axisTransform">
/> </g>
<g <g
class="y-axis" class="y-axis"
transform="translate(70, 20)" transform="translate(70, 20)">
/> </g>
<graph-legend <graph-legend
:graph-width="graphWidth" :graph-width="graphWidth"
:graph-height="graphHeight" :graph-height="graphHeight"
...@@ -251,8 +256,7 @@ ...@@ -251,8 +256,7 @@
<svg <svg
class="graph-data" class="graph-data"
:viewBox="innerViewBox" :viewBox="innerViewBox"
ref="graphData" ref="graphData">
>
<graph-path <graph-path
v-for="(path, index) in timeSeries" v-for="(path, index) in timeSeries"
:key="index" :key="index"
...@@ -262,32 +266,34 @@ ...@@ -262,32 +266,34 @@
:line-color="path.lineColor" :line-color="path.lineColor"
:area-color="path.areaColor" :area-color="path.areaColor"
/> />
<graph-deployment
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<rect <rect
class="prometheus-graph-overlay" class="prometheus-graph-overlay"
:width="(graphWidth - 70)" :width="(graphWidth - 70)"
:height="(graphHeight - 100)" :height="(graphHeight - 100)"
transform="translate(-5, 20)" transform="translate(-5, 20)"
ref="graphOverlay" ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)" @mousemove="handleMouseOverGraph($event)">
/> </rect>
<graph-deployment </svg>
:show-deploy-info="showDeployInfo" </svg>
:deployment-data="reducedDeploymentData"
:graph-width="graphWidth"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<graph-flag <graph-flag
v-if="showFlag" :real-pixel-ratio="realPixelRatio"
:current-x-coordinate="currentXCoordinate" :current-x-coordinate="currentXCoordinate"
:current-data="currentData" :current-data="currentData"
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight" :graph-height="graphHeight"
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent" :show-flag-content="showFlagContent"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
:legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData"
/> />
</svg>
</svg>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters';
import icon from '../../../vue_shared/components/icon.vue';
export default { export default {
components: {
icon,
},
props: { props: {
showDeployInfo: {
type: Boolean,
required: true,
},
deploymentData: { deploymentData: {
type: Array, type: Array,
required: true, required: true,
...@@ -23,10 +13,6 @@ ...@@ -23,10 +13,6 @@
type: Number, type: Number,
required: true, required: true,
}, },
graphWidth: {
type: Number,
required: true,
},
}, },
computed: { computed: {
...@@ -36,165 +22,50 @@ ...@@ -36,165 +22,50 @@
}, },
methods: { methods: {
refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 8);
},
formatTime(deploymentTime) {
return timeFormat(deploymentTime);
},
formatDate(deploymentTime) {
return dateFormatWithName(deploymentTime);
},
nameDeploymentClass(deployment) {
return `deploy-info-${deployment.id}`;
},
transformDeploymentGroup(deployment) { transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`; return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
},
positionFlag(deployment) {
let xPosition = 3;
if (deployment.xPos > (this.graphWidth - 225)) {
xPosition = -142;
}
return xPosition;
},
svgContainerHeight(tag) {
let svgHeight = 80;
if (!tag) {
svgHeight -= 20;
}
return svgHeight;
}, },
}, },
}; };
</script> </script>
<template> <template>
<g <g class="deploy-info">
class="deploy-info"
v-if="showDeployInfo">
<g <g
v-for="(deployment, index) in deploymentData" v-for="(deployment, index) in deploymentData"
:key="index" :key="index"
:class="nameDeploymentClass(deployment)" :transform="transformDeploymentGroup(deployment)">
:transform="transformDeploymentGroup(deployment)"
>
<rect <rect
x="0" x="0"
y="0" y="0"
:height="calculatedHeight" :height="calculatedHeight"
width="3" width="3"
fill="url(#shadow-gradient)" fill="url(#shadow-gradient)">
/> </rect>
<line <line
class="deployment-line" class="deployment-line"
x1="0" x1="0"
y1="0" y1="0"
x2="0" x2="0"
:y2="calculatedHeight" :y2="calculatedHeight"
stroke="#000" stroke="#000">
/> </line>
<svg
v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box"
:x="positionFlag(deployment)"
y="0"
width="134"
:height="svgContainerHeight(deployment.tag)"
>
<rect
class="rect-text-metric deploy-info-rect rect-metric"
x="1"
y="1"
rx="2"
width="132"
:height="svgContainerHeight(deployment.tag) - 2"
/>
<text
class="deploy-info-text text-metric-bold"
transform="translate(5, 2)"
>
Deployed
</text>
<!--The date info-->
<g transform="translate(5, 20)">
<text class="deploy-info-text">
{{ formatDate(deployment.time) }}
</text>
<text
class="deploy-info-text text-metric-bold"
x="62"
>
{{ formatTime(deployment.time) }}
</text>
</g>
<line
class="divider-line"
x1="0"
y1="38"
x2="132"
:y2="38"
stroke="#000"
/>
<!--Commit information-->
<g transform="translate(5, 40)">
<icon
name="commit"
:width="12"
:height="12"
:y="3"
/>
<a :xlink:href="deployment.commitUrl">
<text
class="deploy-info-text deploy-info-text-link"
transform="translate(20, 2)">
{{ refText(deployment) }}
</text>
</a>
</g>
<!--Tag information-->
<g
transform="translate(5, 55)"
v-if="deployment.tag">
<icon
name="label"
:width="12"
:height="12"
:y="5"
/>
<a :xlink:href="deployment.tagUrl">
<text
class="deploy-info-text deploy-info-text-link"
transform="translate(20, 2)"
y="2"
>
{{ deployment.tag }}
</text>
</a>
</g>
</svg>
</g> </g>
<svg <svg
height="0" height="0"
width="0" width="0">
>
<defs> <defs>
<linearGradient id="shadow-gradient"> <linearGradient
id="shadow-gradient">
<stop <stop
offset="0%" offset="0%"
stop-color="#000" stop-color="#000"
stop-opacity="0.4" stop-opacity="0.4">
/> </stop>
<stop <stop
offset="100%" offset="100%"
stop-color="#000" stop-color="#000"
stop-opacity="0" stop-opacity="0">
/> </stop>
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
......
<script> <script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
import Icon from '../../../vue_shared/components/icon.vue';
export default { export default {
props: { props: {
...@@ -7,14 +9,15 @@ ...@@ -7,14 +9,15 @@
type: Number, type: Number,
required: true, required: true,
}, },
currentFlagPosition: {
type: Number,
required: true,
},
currentData: { currentData: {
type: Object, type: Object,
required: true, required: true,
}, },
deploymentFlagData: {
type: Object,
required: false,
default: null,
},
graphHeight: { graphHeight: {
type: Number, type: Number,
required: true, required: true,
...@@ -23,74 +26,173 @@ ...@@ -23,74 +26,173 @@
type: Number, type: Number,
required: true, required: true,
}, },
realPixelRatio: {
type: Number,
required: true,
},
showFlagContent: { showFlagContent: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: {
type: String,
required: true,
},
}, },
data() { components: {
return { Icon,
circleColorRgb: '#8fbce8',
};
}, },
computed: { computed: {
formatTime() { formatTime() {
return timeFormat(this.currentData.time); return this.deploymentFlagData ?
timeFormat(this.deploymentFlagData.time) :
timeFormat(this.currentData.time);
}, },
formatDate() { formatDate() {
return dateFormat(this.currentData.time); return this.deploymentFlagData ?
dateFormat(this.deploymentFlagData.time) :
dateFormat(this.currentData.time);
},
cursorStyle() {
const xCoordinate = this.deploymentFlagData ?
this.deploymentFlagData.xPos :
this.currentXCoordinate;
const offsetTop = 20 * this.realPixelRatio;
const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
return {
top: `${offsetTop}px`,
left: `${offsetLeft}px`,
height: `${height}px`,
};
},
flagOrientation() {
if (this.currentXCoordinate * this.realPixelRatio > 120) {
return 'left';
}
return 'right';
},
},
methods: {
seriesMetricValue(series) {
const index = this.deploymentFlagData ?
this.deploymentFlagData.seriesIndex :
this.currentDataIndex;
const value = series.values[index] &&
series.values[index].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
},
seriesMetricLabel(index, series) {
if (this.timeSeries.length < 2) {
return this.legendTitle;
}
if (series.metricTag) {
return series.metricTag;
}
return `series ${index + 1}`;
}, },
calculatedHeight() { strokeDashArray(type) {
return this.graphHeight - this.graphHeightOffset; if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
}, },
}, },
}; };
</script> </script>
<template> <template>
<g class="mouse-over-flag"> <div
<line class="prometheus-graph-cursor"
class="selected-metric-line" :style="cursorStyle"
:x1="currentXCoordinate" >
:y1="0" <div
:x2="currentXCoordinate"
:y2="calculatedHeight"
transform="translate(-5, 20)"
/>
<svg
v-if="showFlagContent" v-if="showFlagContent"
class="rect-text-metric" class="prometheus-graph-flag popover"
:x="currentFlagPosition" :class="flagOrientation"
y="0"
> >
<rect <div class="arrow"></div>
class="rect-metric" <div class="popover-title">
x="4" <h5 v-if="this.deploymentFlagData">
y="1" Deployed
rx="2" </h5>
width="90" {{formatDate}} at
height="40" <strong>{{formatTime}}</strong>
transform="translate(-3, 20)" </div>
/> <div
<text v-if="this.deploymentFlagData"
class="text-metric text-metric-bold" class="popover-content deploy-meta-content"
x="16"
y="35"
transform="translate(-5, 20)"
> >
{{ formatTime }} <div>
</text> <icon
<text name="commit"
class="text-metric" :size="12">
x="16" </icon>
y="15" <a :href="deploymentFlagData.commitUrl">
transform="translate(-5, 20)" {{deploymentFlagData.sha.slice(0, 8)}}
</a>
</div>
<div
v-if="deploymentFlagData.tag">
<icon
name="label"
:size="12">
</icon>
<a :href="deploymentFlagData.tagUrl">
{{deploymentFlagData.ref}}
</a>
</div>
</div>
<div class="popover-content">
<table>
<tr
v-for="(series, index) in timeSeries"
:key="index"
> >
{{ formatDate }} <td>
</text> <svg width="15" height="6">
<line
:stroke="series.lineColor"
:stroke-dasharray="strokeDashArray(series.lineStyle)"
stroke-width="4"
x1="0"
x2="15"
y1="2"
y2="2">
</line>
</svg> </svg>
</g> </td>
<td>{{seriesMetricLabel(index, series)}}</td>
<td>
<strong>{{seriesMetricValue(series)}}</strong>
</td>
</tr>
</table>
</div>
</div>
</div>
</template> </template>
...@@ -29,15 +29,18 @@ const mixins = { ...@@ -29,15 +29,18 @@ const mixins = {
time.setSeconds(this.timeSeries[0].values[0].time.getSeconds()); time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
if (xPos >= 0) { if (xPos >= 0) {
const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1);
deploymentDataArray.push({ deploymentDataArray.push({
id: deployment.id, id: deployment.id,
time, time,
sha: deployment.sha, sha: deployment.sha,
commitUrl: `${this.projectPath}/commit/${deployment.sha}`, commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
tag: deployment.tag, tag: deployment.tag,
tagUrl: `${this.tagsPath}/${deployment.tag}`, tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
ref: deployment.ref.name, ref: deployment.ref.name,
xPos, xPos,
seriesIndex,
showDeploymentFlag: false, showDeploymentFlag: false,
}); });
} }
......
...@@ -14,7 +14,7 @@ const d3 = { ...@@ -14,7 +14,7 @@ const d3 = {
timeYear, timeYear,
}; };
export const dateFormat = d3.time('%b %-d, %Y'); export const dateFormat = d3.time('%a, %b %-d');
export const timeFormat = d3.time('%-I:%M%p'); export const timeFormat = d3.time('%-I:%M%p');
export const dateFormatWithName = d3.time('%a, %b %-d'); export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left; export const bisectDate = d3.bisector(d => d.time).left;
......
...@@ -17,6 +17,11 @@ export default { ...@@ -17,6 +17,11 @@ export default {
required: true, required: true,
}, },
resetCachePath: {
type: String,
required: true,
},
ciLintPath: { ciLintPath: {
type: String, type: String,
required: true, required: true,
...@@ -45,6 +50,14 @@ export default { ...@@ -45,6 +50,14 @@ export default {
Get started with Pipelines Get started with Pipelines
</a> </a>
<a
data-method="post"
rel="nofollow"
:href="resetCachePath"
class="btn btn-default">
Clear runner caches
</a>
<a <a
:href="ciLintPath" :href="ciLintPath"
class="btn btn-default"> class="btn btn-default">
......
...@@ -50,6 +50,7 @@ ...@@ -50,6 +50,7 @@
canCreatePipeline: pipelinesData.canCreatePipeline, canCreatePipeline: pipelinesData.canCreatePipeline,
hasCi: pipelinesData.hasCi, hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath, ciLintPath: pipelinesData.ciLintPath,
resetCachePath: pipelinesData.resetCachePath,
state: this.store.state, state: this.store.state,
scope: getParameterByName('scope') || 'all', scope: getParameterByName('scope') || 'all',
page: getParameterByName('page') || '1', page: getParameterByName('page') || '1',
...@@ -220,6 +221,7 @@ ...@@ -220,6 +221,7 @@
:new-pipeline-path="newPipelinePath" :new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled" :has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:resetCachePath="resetCachePath"
:ci-lint-path="ciLintPath" :ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed " :can-create-pipeline="canCreatePipelineParsed "
/> />
......
...@@ -541,7 +541,6 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -541,7 +541,6 @@ function UsersSelect(currentUser, els, options = {}) {
options.projectId = $(select).data('project-id'); options.projectId = $(select).data('project-id');
options.groupId = $(select).data('group-id'); options.groupId = $(select).data('group-id');
options.showCurrentUser = $(select).data('current-user'); options.showCurrentUser = $(select).data('current-user');
options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches');
options.authorId = $(select).data('author-id'); options.authorId = $(select).data('author-id');
options.skipUsers = $(select).data('skip-users'); options.skipUsers = $(select).data('skip-users');
showNullUser = $(select).data('null-user'); showNullUser = $(select).data('null-user');
...@@ -688,7 +687,6 @@ UsersSelect.prototype.users = function(query, options, callback) { ...@@ -688,7 +687,6 @@ UsersSelect.prototype.users = function(query, options, callback) {
todo_filter: options.todoFilter || null, todo_filter: options.todoFilter || null,
todo_state_filter: options.todoStateFilter || null, todo_state_filter: options.todoStateFilter || null,
current_user: options.showCurrentUser || null, current_user: options.showCurrentUser || null,
push_code_to_protected_branches: options.pushCodeToProtectedBranches || null,
author_id: options.authorId || null, author_id: options.authorId || null,
skip_users: options.skipUsers || null skip_users: options.skipUsers || null
}, },
......
<script> <script>
import ciIconBadge from './ci_badge_link.vue'; import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue'; import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue'; import timeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue'; import userAvatarImage from './user_avatar/user_avatar_image.vue';
/** /**
* Renders header component for job and pipeline page based on UI mockups * Renders header component for job and pipeline page based on UI mockups
* *
* Used in: * Used in:
* - job show page * - job show page
* - pipeline show page * - pipeline show page
*/ */
export default { export default {
directives: {
tooltip,
},
components: {
ciIconBadge,
loadingIcon,
timeagoTooltip,
userAvatarImage,
},
props: { props: {
status: { status: {
type: Object, type: Object,
...@@ -55,6 +45,22 @@ ...@@ -55,6 +45,22 @@
required: false, required: false,
default: false, default: false,
}, },
shouldRenderTriggeredLabel: {
type: Boolean,
required: false,
default: true,
},
},
directives: {
tooltip,
},
components: {
ciIconBadge,
loadingIcon,
timeagoTooltip,
userAvatarImage,
}, },
computed: { computed: {
...@@ -68,7 +74,7 @@ ...@@ -68,7 +74,7 @@
this.$emit('actionClicked', action); this.$emit('actionClicked', action);
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -78,10 +84,15 @@ ...@@ -78,10 +84,15 @@
<ci-icon-badge :status="status" /> <ci-icon-badge :status="status" />
<strong> <strong>
{{ itemName }} #{{ itemId }} {{itemName}} #{{itemId}}
</strong> </strong>
<template v-if="shouldRenderTriggeredLabel">
triggered triggered
</template>
<template v-else>
created
</template>
<timeago-tooltip :time="time" /> <timeago-tooltip :time="time" />
...@@ -92,8 +103,7 @@ ...@@ -92,8 +103,7 @@
v-tooltip v-tooltip
:href="user.path" :href="user.path"
:title="user.email" :title="user.email"
class="js-user-link commit-committer-link" class="js-user-link commit-committer-link">
>
<user-avatar-image <user-avatar-image
:img-src="user.avatar_url" :img-src="user.avatar_url"
...@@ -102,25 +112,21 @@ ...@@ -102,25 +112,21 @@
:img-size="24" :img-size="24"
/> />
{{ user.name }} {{user.name}}
</a> </a>
</template> </template>
</section> </section>
<section <section
class="header-action-buttons" class="header-action-buttons"
v-if="actions.length" v-if="actions.length">
>
<template <template
v-for="(action, i) in actions" v-for="action in actions">
>
<a <a
v-if="action.type === 'link'" v-if="action.type === 'link'"
:href="action.path" :href="action.path"
:class="action.cssClass" :class="action.cssClass">
:key="i" {{action.label}}
>
{{ action.label }}
</a> </a>
<a <a
...@@ -128,10 +134,8 @@ ...@@ -128,10 +134,8 @@
:href="action.path" :href="action.path"
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
:class="action.cssClass" :class="action.cssClass">
:key="i" {{action.label}}
>
{{ action.label }}
</a> </a>
<button <button
...@@ -139,15 +143,12 @@ ...@@ -139,15 +143,12 @@
@click="onClickAction(action)" @click="onClickAction(action)"
:disabled="action.isLoading" :disabled="action.isLoading"
:class="action.cssClass" :class="action.cssClass"
type="button" type="button">
:key="i" {{action.label}}
>
{{ action.label }}
<i <i
v-show="action.isLoading" v-show="action.isLoading"
class="fa fa-spin fa-spinner" class="fa fa-spin fa-spinner"
aria-hidden="true" aria-hidden="true">
>
</i> </i>
</button> </button>
</template> </template>
...@@ -156,13 +157,11 @@ ...@@ -156,13 +157,11 @@
type="button" type="button"
class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-label="Toggle Sidebar" aria-label="Toggle Sidebar"
id="toggleSidebar" id="toggleSidebar">
>
<i <i
class="fa fa-angle-double-left" class="fa fa-angle-double-left"
aria-hidden="true" aria-hidden="true"
aria-labelledby="toggleSidebar" aria-labelledby="toggleSidebar">
>
</i> </i>
</button> </button>
</section> </section>
......
...@@ -20,10 +20,13 @@ ...@@ -20,10 +20,13 @@
width: 100%; width: 100%;
} }
&.svg-250 { $image-widths: 250 306 394;
@each $width in $image-widths {
&.svg-#{$width} {
img, img,
svg { svg {
width: 250px; width: #{$width + 'px'};
}
} }
} }
} }
......
...@@ -192,6 +192,17 @@ ...@@ -192,6 +192,17 @@
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
.name,
small.aliases,
small.params {
float: left;
}
small.aliases,
small.params {
padding: 2px 5px;
}
small.description { small.description {
float: right; float: right;
padding: 3px 5px; padding: 3px 5px;
...@@ -209,6 +220,7 @@ ...@@ -209,6 +220,7 @@
} }
ul > li { ul > li {
@include clearfix;
white-space: nowrap; white-space: nowrap;
} }
......
...@@ -248,6 +248,73 @@ ...@@ -248,6 +248,73 @@
} }
} }
.prometheus-graph-cursor {
position: absolute;
background: $theme-gray-600;
width: 1px;
}
.prometheus-graph-flag {
display: block;
min-width: 160px;
h5 {
padding: 0;
margin: 0;
font-size: 14px;
line-height: 1.2;
}
table {
border-collapse: collapse;
padding: 0;
margin: 0;
}
td {
vertical-align: middle;
+ td {
padding-left: 5px;
vertical-align: top;
}
}
.deploy-meta-content {
border-bottom: 1px solid $white-dark;
svg {
height: 15px;
vertical-align: bottom;
}
}
&.popover {
&.left {
left: auto;
right: 0;
margin-right: 10px;
}
&.right {
left: 0;
right: auto;
margin-left: 10px;
}
> .arrow {
top: 40px;
}
> .popover-title,
> .popover-content {
padding: 5px 8px;
font-size: 12px;
white-space: nowrap;
}
}
}
.prometheus-svg-container { .prometheus-svg-container {
position: relative; position: relative;
height: 0; height: 0;
......
...@@ -92,6 +92,19 @@ ...@@ -92,6 +92,19 @@
padding: 6px 12px; padding: 6px 12px;
} }
.multi-file-loading-container {
margin-top: 10px;
padding: 10px;
.animation-container {
background: $gray-light;
div {
background: $gray-light;
}
}
}
table.table tr td.multi-file-table-name { table.table tr td.multi-file-table-name {
width: 350px; width: 350px;
padding: 6px 12px; padding: 6px 12px;
......
...@@ -5,8 +5,6 @@ module IssuesAction ...@@ -5,8 +5,6 @@ module IssuesAction
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def issues def issues
@finder_type = IssuesFinder @finder_type = IssuesFinder
@label = finder.labels.first
@issues = issuables_collection @issues = issuables_collection
.non_archived .non_archived
.page(params[:page]) .page(params[:page])
......
...@@ -5,7 +5,6 @@ module MergeRequestsAction ...@@ -5,7 +5,6 @@ module MergeRequestsAction
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def merge_requests def merge_requests
@finder_type = MergeRequestsFinder @finder_type = MergeRequestsFinder
@label = finder.labels.first
@merge_requests = issuables_collection.page(params[:page]) @merge_requests = issuables_collection.page(params[:page])
......
...@@ -86,4 +86,8 @@ class Projects::ApplicationController < ApplicationController ...@@ -86,4 +86,8 @@ class Projects::ApplicationController < ApplicationController
def require_pages_enabled! def require_pages_enabled!
not_found unless @project.pages_available? not_found unless @project.pages_available?
end end
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
end end
...@@ -2,6 +2,7 @@ class Projects::BoardsController < Projects::ApplicationController ...@@ -2,6 +2,7 @@ class Projects::BoardsController < Projects::ApplicationController
include BoardsResponses include BoardsResponses
include IssuableCollections include IssuableCollections
before_action :check_issues_available!
before_action :authorize_read_board!, only: [:index, :show] before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
......
class Projects::Clusters::GcpController < Projects::ApplicationController class Projects::Clusters::GcpController < Projects::ApplicationController
before_action :authorize_read_cluster! before_action :authorize_read_cluster!
before_action :authorize_google_api, except: [:login] before_action :authorize_google_api, except: [:login]
before_action :authorize_google_project_billing, only: [:new]
before_action :authorize_create_cluster!, only: [:new, :create] before_action :authorize_create_cluster!, only: [:new, :create]
def login def login
...@@ -22,15 +23,20 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -22,15 +23,20 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end end
def create def create
case google_project_billing_status
when 'true'
@cluster = ::Clusters::CreateService @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? return redirect_to project_cluster_path(project, @cluster) if @cluster.persisted?
redirect_to project_cluster_path(project, @cluster) when 'false'
flash[:error] = _('Please enable billing for one of your projects to be able to create a cluster.')
else else
render :new flash[:error] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
end end
render :new
end end
private private
...@@ -58,6 +64,17 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -58,6 +64,17 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end end
end end
def authorize_google_project_billing
redis_token_key = CheckGcpProjectBillingWorker.store_session_token(token_in_session)
CheckGcpProjectBillingWorker.perform_async(redis_token_key)
end
def google_project_billing_status
Gitlab::Redis::SharedState.with do |redis|
redis.get(CheckGcpProjectBillingWorker.redis_shared_state_key_for(token_in_session))
end
end
def token_in_session def token_in_session
@token_in_session ||= @token_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_token] session[GoogleApi::CloudPlatform::Client.session_key_for_token]
......
...@@ -194,10 +194,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -194,10 +194,6 @@ class Projects::IssuesController < Projects::ApplicationController
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end end
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
def render_issue_json def render_issue_json
if @issue.valid? if @issue.valid?
render json: serializer.represent(@issue) render json: serializer.represent(@issue)
......
...@@ -11,6 +11,16 @@ module Projects ...@@ -11,6 +11,16 @@ module Projects
define_auto_devops_variables define_auto_devops_variables
end end
def reset_cache
if ResetProjectCacheService.new(@project, current_user).execute
flash[:notice] = _("Project cache successfully reset.")
else
flash[:error] = _("Unable to reset project cache.")
end
redirect_to project_pipelines_path(@project)
end
private private
def define_runners_variables def define_runners_variables
......
...@@ -374,18 +374,13 @@ class IssuableFinder ...@@ -374,18 +374,13 @@ class IssuableFinder
end end
def by_label(items) def by_label(items)
if labels? return items unless labels?
items =
if filter_by_no_label? if filter_by_no_label?
items = items.without_label items.without_label
else else
items = items.with_label(label_names, params[:sort]) items.with_label(label_names, params[:sort])
items_projects = projects(items)
if items_projects
label_ids = LabelsFinder.new(current_user, project_ids: items_projects).execute(skip_authorization: true).select(:id)
items = items.where(labels: { id: label_ids })
end
end
end end
items items
......
...@@ -73,7 +73,6 @@ module SelectsHelper ...@@ -73,7 +73,6 @@ module SelectsHelper
email_user: opts[:email_user] || false, email_user: opts[:email_user] || false,
first_user: opts[:first_user] && current_user ? current_user.username : false, first_user: opts[:first_user] && current_user ? current_user.username : false,
current_user: opts[:current_user] || false, current_user: opts[:current_user] || false,
"push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
author_id: opts[:author_id] || '', author_id: opts[:author_id] || '',
skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil
} }
......
...@@ -79,7 +79,7 @@ module Ci ...@@ -79,7 +79,7 @@ module Ci
before_save :ensure_token before_save :ensure_token
before_destroy { unscoped_project } before_destroy { unscoped_project }
after_create do |build| after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) } run_after_commit { BuildHooksWorker.perform_async(build.id) }
end end
...@@ -461,7 +461,14 @@ module Ci ...@@ -461,7 +461,14 @@ module Ci
end end
def cache def cache
[options[:cache]] cache = options[:cache]
if cache && project.jobs_cache_index
cache = cache.merge(
key: "#{cache[:key]}:#{project.jobs_cache_index}")
end
[cache]
end end
def credentials def credentials
......
...@@ -371,7 +371,7 @@ class Commit ...@@ -371,7 +371,7 @@ class Commit
# #
# Returns a symbol # Returns a symbol
def uri_type(path) def uri_type(path)
entry = @raw.tree.path(path) entry = @raw.rugged_tree_entry(path)
if entry[:type] == :blob if entry[:type] == :blob
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project) blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
blob.image? || blob.video? ? :raw : :blob blob.image? || blob.video? ? :raw : :blob
......
...@@ -27,7 +27,7 @@ class PagesDomain < ActiveRecord::Base ...@@ -27,7 +27,7 @@ class PagesDomain < ActiveRecord::Base
def url def url
return unless domain return unless domain
if certificate if certificate.present?
"https://#{domain}" "https://#{domain}"
else else
"http://#{domain}" "http://#{domain}"
......
...@@ -1450,6 +1450,7 @@ class Project < ActiveRecord::Base ...@@ -1450,6 +1450,7 @@ class Project < ActiveRecord::Base
import_finish import_finish
remove_import_jid remove_import_jid
update_project_counter_caches update_project_counter_caches
after_create_default_branch
end end
def update_project_counter_caches def update_project_counter_caches
...@@ -1463,6 +1464,27 @@ class Project < ActiveRecord::Base ...@@ -1463,6 +1464,27 @@ class Project < ActiveRecord::Base
end end
end end
def after_create_default_branch
return unless default_branch
# Ensure HEAD points to the default branch in case it is not master
change_head(default_branch)
if current_application_settings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
params = {
name: default_branch,
push_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}],
merge_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}]
}
ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true)
end
end
def remove_import_jid def remove_import_jid
return unless import_jid return unless import_jid
......
...@@ -783,34 +783,30 @@ class Repository ...@@ -783,34 +783,30 @@ class Repository
end end
def create_dir(user, path, **options) def create_dir(user, path, **options)
options[:user] = user
options[:actions] = [{ action: :create_dir, file_path: path }] options[:actions] = [{ action: :create_dir, file_path: path }]
multi_action(**options) multi_action(user, **options)
end end
def create_file(user, path, content, **options) def create_file(user, path, content, **options)
options[:user] = user
options[:actions] = [{ action: :create, file_path: path, content: content }] options[:actions] = [{ action: :create, file_path: path, content: content }]
multi_action(**options) multi_action(user, **options)
end end
def update_file(user, path, content, **options) def update_file(user, path, content, **options)
previous_path = options.delete(:previous_path) previous_path = options.delete(:previous_path)
action = previous_path && previous_path != path ? :move : :update action = previous_path && previous_path != path ? :move : :update
options[:user] = user
options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }] options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }]
multi_action(**options) multi_action(user, **options)
end end
def delete_file(user, path, **options) def delete_file(user, path, **options)
options[:user] = user
options[:actions] = [{ action: :delete, file_path: path }] options[:actions] = [{ action: :delete, file_path: path }]
multi_action(**options) multi_action(user, **options)
end end
def with_cache_hooks def with_cache_hooks
...@@ -824,59 +820,14 @@ class Repository ...@@ -824,59 +820,14 @@ class Repository
result.newrev result.newrev
end end
def with_branch(user, *args) def multi_action(user, **options)
with_cache_hooks do start_project = options.delete(:start_project)
Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
yield start_commit
end
end
end
# rubocop:disable Metrics/ParameterLists
def multi_action(
user:, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
with_branch( if start_project
user, options[:start_repository] = start_project.repository.raw_repository
branch_name,
start_branch_name: start_branch_name,
start_repository: start_project.repository.raw_repository) do |start_commit|
index = Gitlab::Git::Index.new(raw_repository)
if start_commit
index.read_tree(start_commit.rugged_commit.tree)
parents = [start_commit.sha]
else
parents = []
end end
actions.each do |options| with_cache_hooks { raw.multi_action(user, **options) }
index.public_send(options.delete(:action), options) # rubocop:disable GitlabSecurity/PublicSend
end
options = {
tree: index.write_tree,
message: message,
parents: parents
}
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
create_commit(options)
end
end
# rubocop:enable Metrics/ParameterLists
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
author = Gitlab::Git.committer_hash(email: email, name: name) || committer
{
author: author,
committer: committer
}
end end
def can_be_merged?(source_sha, target_branch) def can_be_merged?(source_sha, target_branch)
......
...@@ -241,7 +241,6 @@ class ProjectPolicy < BasePolicy ...@@ -241,7 +241,6 @@ class ProjectPolicy < BasePolicy
rule { repository_disabled }.policy do rule { repository_disabled }.policy do
prevent :push_code prevent :push_code
prevent :push_code_to_protected_branches
prevent :download_code prevent :download_code
prevent :fork_project prevent :fork_project
prevent :read_commit_status prevent :read_commit_status
......
class GroupChildEntity < Grape::Entity class GroupChildEntity < Grape::Entity
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
include RequestAwareEntity include RequestAwareEntity
include MarkupHelper
expose :id, :name, :description, :visibility, :full_name, expose :id, :name, :description, :visibility, :full_name,
:created_at, :updated_at, :avatar_url :created_at, :updated_at, :avatar_url
...@@ -59,6 +60,10 @@ class GroupChildEntity < Grape::Entity ...@@ -59,6 +60,10 @@ class GroupChildEntity < Grape::Entity
number_with_delimiter(instance.member_count) number_with_delimiter(instance.member_count)
end end
expose :markdown_description do |instance|
markdown_description
end
private private
def membership def membership
...@@ -74,4 +79,8 @@ class GroupChildEntity < Grape::Entity ...@@ -74,4 +79,8 @@ class GroupChildEntity < Grape::Entity
def type def type
object.class.name.downcase object.class.name.downcase
end end
def markdown_description
markdown_field(object, :description)
end
end end
...@@ -4,6 +4,8 @@ class JobEntity < Grape::Entity ...@@ -4,6 +4,8 @@ class JobEntity < Grape::Entity
expose :id expose :id
expose :name expose :name
expose :started?, as: :started
expose :build_path do |build| expose :build_path do |build|
build.target_url || path_to(:namespace_project_job, build) build.target_url || path_to(:namespace_project_job, build)
end end
......
class CheckGcpProjectBillingService
def execute(token)
client = GoogleApi::CloudPlatform::Client.new(token, nil)
client.projects_list.select do |project|
client.projects_get_billing_info(project.name).billingEnabled
end
end
end
...@@ -4,7 +4,7 @@ module Files ...@@ -4,7 +4,7 @@ module Files
def create_commit! def create_commit!
repository.multi_action( repository.multi_action(
user: current_user, current_user,
message: @commit_message, message: @commit_message,
branch_name: @branch_name, branch_name: @branch_name,
actions: params[:actions], actions: params[:actions],
...@@ -13,6 +13,8 @@ module Files ...@@ -13,6 +13,8 @@ module Files
start_project: @start_project, start_project: @start_project,
start_branch_name: @start_branch start_branch_name: @start_branch
) )
rescue ArgumentError => e
raise_error(e)
end end
private private
...@@ -20,16 +22,7 @@ module Files ...@@ -20,16 +22,7 @@ module Files
def validate! def validate!
super super
params[:actions].each do |action| params[:actions].each { |action| validate_file_status!(action) }
validate_action!(action)
validate_file_status!(action)
end
end
def validate_action!(action)
unless Gitlab::Git::Index::ACTIONS.include?(action[:action].to_s)
raise_error("Unknown action '#{action[:action]}'")
end
end end
def validate_file_status!(action) def validate_file_status!(action)
......
...@@ -154,24 +154,7 @@ class GitPushService < BaseService ...@@ -154,24 +154,7 @@ class GitPushService < BaseService
offset = [@push_commits_count - PROCESS_COMMIT_LIMIT, 0].max offset = [@push_commits_count - PROCESS_COMMIT_LIMIT, 0].max
@push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT) @push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
# Ensure HEAD points to the default branch in case it is not master @project.after_create_default_branch
project.change_head(branch_name)
# Set protection on the default branch if configured
if current_application_settings.default_branch_protection != PROTECTION_NONE && !ProtectedBranch.protected?(@project, @project.default_branch)
params = {
name: @project.default_branch,
push_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}],
merge_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}]
}
ProtectedBranches::CreateService.new(@project, current_user, params).execute
end
end end
def build_push_data def build_push_data
......
...@@ -2,8 +2,8 @@ module ProtectedBranches ...@@ -2,8 +2,8 @@ module ProtectedBranches
class CreateService < BaseService class CreateService < BaseService
attr_reader :protected_branch attr_reader :protected_branch
def execute def execute(skip_authorization: false)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project)
project.protected_branches.create(params) project.protected_branches.create(params)
end end
......
class ResetProjectCacheService < BaseService
def execute
@project.increment!(:jobs_cache_index)
end
end
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.ide-flash-container.flash-container .ide-flash-container.flash-container
#ide.ide-loading #ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
.text-center .text-center
= icon('spinner spin 2x') = icon('spinner spin 2x')
%h2.clgray= _('IDE Loading ...') %h2.clgray= _('Loading the GitLab IDE...')
...@@ -49,6 +49,12 @@ ...@@ -49,6 +49,12 @@
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
Projects Projects
- if current_controller?('ide')
%li.line-separator.hidden-xs
= nav_link(controller: 'ide') do
= link_to '#', class: 'dashboard-shortcuts-web-ide', title: 'Web IDE' do
Web IDE
- if current_user.admin? || Gitlab::Sherlock.enabled? - if current_user.admin? || Gitlab::Sherlock.enabled?
%li.line-separator.hidden-xs %li.line-separator.hidden-xs
- if current_user.admin? - if current_user.admin?
......
...@@ -299,6 +299,7 @@ ...@@ -299,6 +299,7 @@
Charts Charts
-# Shortcut to Issues > New Issue -# Shortcut to Issues > New Issue
- if project_nav_tab?(:issues)
%li.hidden %li.hidden
= link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
Create a new issue Create a new issue
...@@ -316,5 +317,6 @@ ...@@ -316,5 +317,6 @@
Commits Commits
-# Shortcut to issue boards -# Shortcut to issue boards
- if project_nav_tab?(:issues)
%li.hidden %li.hidden
= link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
...@@ -10,5 +10,5 @@ ...@@ -10,5 +10,5 @@
- link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer') - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements } = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
%li %li
- link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), target: '_blank', rel: 'noopener noreferrer') - link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project } = s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
...@@ -12,6 +12,8 @@ ...@@ -12,6 +12,8 @@
- if @authorize_url - if @authorize_url
= link_to @authorize_url do = link_to @authorize_url do
= image_tag('auth_buttons/signin_with_google.png', width: '191px') = image_tag('auth_buttons/signin_with_google.png', width: '191px')
= _('or')
= link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer')
- else - else
- link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer') - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
= s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link } = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
- illustration = local_assigns.fetch(:illustration)
- illustration_size = local_assigns.fetch(:illustration_size)
- title = local_assigns.fetch(:title)
- content = local_assigns.fetch(:content)
- action = local_assigns.fetch(:action, nil)
.row.empty-state
.col-xs-12
.svg-content{ class: illustration_size }
= image_tag illustration
.col-xs-12
.text-content
%h4.text-center= title
%p= content
- if action
.text-center
= action
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else - else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)} Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- if @build.started?
.build-trace-container.prepend-top-default .build-trace-container.prepend-top-default
.top-bar.js-top-bar .top-bar.js-top-bar
.js-truncated-info.truncated-info.hidden-xs.pull-left.hidden< .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
...@@ -88,7 +88,19 @@ ...@@ -88,7 +88,19 @@
%pre.build-trace#build-trace %pre.build-trace#build-trace
%code.bash.js-build-output %code.bash.js-build-output
.build-loader-animation.js-build-refresh .build-loader-animation.js-build-refresh
- elsif @build.playable?
= render 'empty_state',
illustration: 'illustrations/manual_action.svg',
illustration_size: 'svg-394',
title: _('This job requires a manual action'),
content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments.'),
action: ( link_to _('Trigger this manual action'), play_project_job_path(@project, @build), class: 'btn btn-primary', title: _('Trigger this manual action') )
- else
= render 'empty_state',
illustration: 'illustrations/job_not_triggered.svg',
illustration_size: 'svg-306',
title: _('This job has not been triggered yet'),
content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered.')
= render "sidebar" = render "sidebar"
......
...@@ -10,7 +10,8 @@ ...@@ -10,7 +10,8 @@
"new-pipeline-path" => new_project_pipeline_path(@project), "new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"has-ci" => @repository.gitlab_ci_yml, "has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path } } "ci-lint-path" => ci_lint_path,
"reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } }
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pipelines') = page_specific_javascript_bundle_tag('pipelines')
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
- gcp_cluster:cluster_provision - gcp_cluster:cluster_provision
- gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation - gcp_cluster:wait_for_cluster_creation
- gcp_cluster:check_gcp_project_billing
- github_import_advance_stage - github_import_advance_stage
- github_importer:github_import_import_diff_note - github_importer:github_import_import_diff_note
......
class BackgroundMigrationWorker class BackgroundMigrationWorker
include ApplicationWorker include ApplicationWorker
# The minimum amount of time between processing two jobs of the same migration
# class.
#
# This interval is set to 5 minutes so autovacuuming and other maintenance
# related tasks have plenty of time to clean up after a migration has been
# performed.
MIN_INTERVAL = 5.minutes.to_i
# Performs the background migration. # Performs the background migration.
# #
# See Gitlab::BackgroundMigration.perform for more information. # See Gitlab::BackgroundMigration.perform for more information.
#
# class_name - The class name of the background migration to run.
# arguments - The arguments to pass to the migration class.
def perform(class_name, arguments = []) def perform(class_name, arguments = [])
should_perform, ttl = perform_and_ttl(class_name)
if should_perform
Gitlab::BackgroundMigration.perform(class_name, arguments) Gitlab::BackgroundMigration.perform(class_name, arguments)
else
# If the lease could not be obtained this means either another process is
# running a migration of this class or we ran one recently. In this case
# we'll reschedule the job in such a way that it is picked up again around
# the time the lease expires.
self.class.perform_in(ttl || MIN_INTERVAL, class_name, arguments)
end
end
def perform_and_ttl(class_name)
if always_perform?
# In test environments `perform_in` will run right away. This can then
# lead to stack level errors in the above `#perform`. To work around this
# we'll just perform the migration right away in the test environment.
[true, nil]
else
lease = lease_for(class_name)
[lease.try_obtain, lease.ttl]
end
end
def lease_for(class_name)
Gitlab::ExclusiveLease
.new("#{self.class.name}:#{class_name}", timeout: MIN_INTERVAL)
end
def always_perform?
Rails.env.test?
end end
end end
require 'securerandom'
class CheckGcpProjectBillingWorker
include ApplicationWorker
include ClusterQueue
LEASE_TIMEOUT = 15.seconds.to_i
SESSION_KEY_TIMEOUT = 5.minutes
BILLING_TIMEOUT = 1.hour
def self.get_session_token(token_key)
Gitlab::Redis::SharedState.with do |redis|
redis.get(get_redis_session_key(token_key))
end
end
def self.store_session_token(token)
generate_token_key.tap do |token_key|
Gitlab::Redis::SharedState.with do |redis|
redis.set(get_redis_session_key(token_key), token, ex: SESSION_KEY_TIMEOUT)
end
end
end
def self.redis_shared_state_key_for(token)
"gitlab:gcp:#{token.hash}:billing_enabled"
end
def perform(token_key)
return unless token_key
token = self.get_session_token(token_key)
return unless token
return unless try_obtain_lease_for(token)
billing_enabled_projects = CheckGcpProjectBillingService.new.execute(token)
Gitlab::Redis::SharedState.with do |redis|
redis.set(self.class.redis_shared_state_key_for(token),
!billing_enabled_projects.empty?,
ex: BILLING_TIMEOUT)
end
end
private
def self.generate_token_key
SecureRandom.uuid
end
def self.get_redis_session_key(token_key)
"gitlab:gcp:session:#{token_key}"
end
def try_obtain_lease_for(token)
Gitlab::ExclusiveLease
.new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT)
.try_obtain
end
end
---
title: Generate HTTP URLs for custom Pages domains when appropriate
merge_request: 16279
author:
type: fixed
---
title: Display graph values on hover within monitoring page
merge_request: 16261
author:
type: changed
---
title: Protected branch is now created for default branch on import
merge_request: 16198
author:
type: fixed
---
title: Implement checking GCP project billing status in cluster creation form.
merge_request: 15665
author:
type: changed
---
title: "Fix slash commands dropdown description mis-alignment on Firefox"
merge_request: 16125
author: Maurizio De Santis
type: fixed
---
title: Migrate existing data from KubernetesService to Clusters::Platforms::Kubernetes
merge_request: 15589
author:
type: changed
---
title: Rendering of emoji's in Group-Overview
merge_request: 16098
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: disables shortcut to issue boards when issues are not enabled
merge_request: 16020
author: Christiaan Van den Poel
type: fixed
---
title: Implement project jobs cache reset
merge_request: 16067
author:
type: added
---
title: Update scss-lint to 0.56.0
merge_request: 16278
author: Takuya Noguchi
type: other
---
title: Run background migrations with a minimum interval
merge_request:
author:
type: changed
---
title: Fix missing references to pipeline objects when restoring project with import/export
feature
merge_request: 16221
author:
type: fixed
---
title: Fix timeout when filtering issues by label
merge_request:
author:
type: performance
...@@ -408,7 +408,9 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -408,7 +408,9 @@ constraints(ProjectUrlConstrainer.new) do
end end
namespace :settings do namespace :settings do
get :members, to: redirect("%{namespace_id}/%{project_id}/project_members") get :members, to: redirect("%{namespace_id}/%{project_id}/project_members")
resource :ci_cd, only: [:show], controller: 'ci_cd' resource :ci_cd, only: [:show], controller: 'ci_cd' do
post :reset_cache
end
resource :integrations, only: [:show] resource :integrations, only: [:show]
resource :repository, only: [:show], controller: :repository resource :repository, only: [:show], controller: :repository
end end
......
...@@ -63,7 +63,8 @@ Sidekiq::Testing.inline! do ...@@ -63,7 +63,8 @@ Sidekiq::Testing.inline! do
namespace_id: group.id, namespace_id: group.id,
name: project_path.titleize, name: project_path.titleize,
description: FFaker::Lorem.sentence, description: FFaker::Lorem.sentence,
visibility_level: Gitlab::VisibilityLevel.values.sample visibility_level: Gitlab::VisibilityLevel.values.sample,
skip_disk_validation: true
} }
project = Projects::CreateService.new(User.first, params).execute project = Projects::CreateService.new(User.first, params).execute
......
# This migration is a duplicate of 20171230123729_add_rebase_commit_sha_to_merge_requests_ce.rb
#
# We backported this feature from EE using the same migration, but with a new
# timestamp, which caused an error when the backport was then to be merged back
# into EE.
#
# See discussion at https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3932
class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration
DOWNTIME = false
def up
unless column_exists?(:merge_requests, :rebase_commit_sha)
add_column :merge_requests, :rebase_commit_sha, :string
end
end
def down
if column_exists?(:merge_requests, :rebase_commit_sha)
remove_column :merge_requests, :rebase_commit_sha
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddJobsCacheIndexToProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
add_column :projects, :jobs_cache_index, :integer
end
end
class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :merge_requests, :rebase_commit_sha, :string
end
end
class AddRebaseCommitShaToMergeRequestsCe < ActiveRecord::Migration
DOWNTIME = false
def up
unless column_exists?(:merge_requests, :rebase_commit_sha)
add_column :merge_requests, :rebase_commit_sha, :string
end
end
def down
if column_exists?(:merge_requests, :rebase_commit_sha)
remove_column :merge_requests, :rebase_commit_sha
end
end
end
class MigrateKubernetesServiceToNewClustersArchitectures < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
DEFAULT_KUBERNETES_SERVICE_CLUSTER_NAME = 'KubernetesService'.freeze
disable_ddl_transaction!
class Project < ActiveRecord::Base
self.table_name = 'projects'
has_many :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::ClustersProject'
has_many :clusters, through: :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
has_many :services, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Service'
has_one :kubernetes_service, -> { where(category: 'deployment', type: 'KubernetesService') }, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Service', inverse_of: :project, foreign_key: :project_id
end
class Cluster < ActiveRecord::Base
self.table_name = 'clusters'
has_many :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::ClustersProject'
has_many :projects, through: :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project'
has_one :platform_kubernetes, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::PlatformsKubernetes'
accepts_nested_attributes_for :platform_kubernetes
enum platform_type: {
kubernetes: 1
}
enum provider_type: {
user: 0,
gcp: 1
}
end
class ClustersProject < ActiveRecord::Base
self.table_name = 'cluster_projects'
belongs_to :cluster, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
belongs_to :project, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project'
end
class PlatformsKubernetes < ActiveRecord::Base
self.table_name = 'cluster_platforms_kubernetes'
belongs_to :cluster, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
attr_encrypted :token,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
end
class Service < ActiveRecord::Base
include EachBatch
self.table_name = 'services'
self.inheritance_column = :_type_disabled # Disable STI, otherwise KubernetesModel will be looked up
belongs_to :project, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project', foreign_key: :project_id
scope :unmanaged_kubernetes_service, -> do
joins('LEFT JOIN projects ON projects.id = services.project_id')
.joins('LEFT JOIN cluster_projects ON cluster_projects.project_id = projects.id')
.joins('LEFT JOIN cluster_platforms_kubernetes ON cluster_platforms_kubernetes.cluster_id = cluster_projects.cluster_id')
.where(category: 'deployment', type: 'KubernetesService', template: false)
.where("services.properties LIKE '%api_url%'")
.where("(services.properties NOT LIKE CONCAT('%', cluster_platforms_kubernetes.api_url, '%')) OR cluster_platforms_kubernetes.api_url IS NULL")
.group(:id)
.order(id: :asc)
end
scope :kubernetes_service_without_template, -> do
where(category: 'deployment', type: 'KubernetesService', template: false)
end
def api_url
parsed_properties['api_url']
end
def ca_pem
parsed_properties['ca_pem']
end
def namespace
parsed_properties['namespace']
end
def token
parsed_properties['token']
end
private
def parsed_properties
@parsed_properties ||= JSON.parse(self.properties)
end
end
def find_dedicated_environement_scope(project)
environment_scopes = project.clusters.map(&:environment_scope)
return '*' if environment_scopes.exclude?('*') # KubernetesService should be added as a default cluster (environment_scope: '*') at first place
return 'migrated/*' if environment_scopes.exclude?('migrated/*') # If it's conflicted, the KubernetesService added as a migrated cluster
unique_iid = 0
# If it's still conflicted, finding an unique environment scope incrementaly
loop do
candidate = "migrated#{unique_iid}/*"
return candidate if environment_scopes.exclude?(candidate)
unique_iid += 1
end
end
def up
ActiveRecord::Base.transaction do
MigrateKubernetesServiceToNewClustersArchitectures::Service
.unmanaged_kubernetes_service.find_each(batch_size: 1) do |kubernetes_service|
MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create(
enabled: kubernetes_service.active,
user_id: nil, # KubernetesService doesn't have
name: DEFAULT_KUBERNETES_SERVICE_CLUSTER_NAME,
provider_type: MigrateKubernetesServiceToNewClustersArchitectures::Cluster.provider_types[:user],
platform_type: MigrateKubernetesServiceToNewClustersArchitectures::Cluster.platform_types[:kubernetes],
projects: [kubernetes_service.project],
environment_scope: find_dedicated_environement_scope(kubernetes_service.project),
platform_kubernetes_attributes: {
api_url: kubernetes_service.api_url,
ca_cert: kubernetes_service.ca_pem,
namespace: kubernetes_service.namespace,
username: nil, # KubernetesService doesn't have
encrypted_password: nil, # KubernetesService doesn't have
encrypted_password_iv: nil, # KubernetesService doesn't have
token: kubernetes_service.token # encrypted_token and encrypted_token_iv
} )
end
end
MigrateKubernetesServiceToNewClustersArchitectures::Service
.kubernetes_service_without_template.each_batch(of: 100) do |kubernetes_service|
kubernetes_service.update_all(active: false)
end
end
def down
# noop
end
end
...@@ -1451,6 +1451,7 @@ ActiveRecord::Schema.define(version: 20171230123729) do ...@@ -1451,6 +1451,7 @@ ActiveRecord::Schema.define(version: 20171230123729) do
t.boolean "repository_read_only" t.boolean "repository_read_only"
t.boolean "merge_requests_ff_only_enabled", default: false t.boolean "merge_requests_ff_only_enabled", default: false
t.boolean "merge_requests_rebase_enabled", default: false, null: false t.boolean "merge_requests_rebase_enabled", default: false, null: false
t.integer "jobs_cache_index"
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
...@@ -15,7 +15,7 @@ request introducing these changes must be accompanied by the documentation ...@@ -15,7 +15,7 @@ request introducing these changes must be accompanied by the documentation
(either updating existing ones or creating new ones). This is also valid when (either updating existing ones or creating new ones). This is also valid when
changes are introduced to the UI. changes are introduced to the UI.
The one resposible for writing the first piece of documentation is the developer who The one responsible for writing the first piece of documentation is the developer who
wrote the code. It's the job of the Product Manager to ensure all features are wrote the code. It's the job of the Product Manager to ensure all features are
shipped with its docs, whether is a small or big change. At the pace GitLab evolves, shipped with its docs, whether is a small or big change. At the pace GitLab evolves,
this is the only way to keep the docs up-to-date. If you have any questions about it, this is the only way to keep the docs up-to-date. If you have any questions about it,
...@@ -31,7 +31,7 @@ Every major feature (regardless if present in GitLab Community or Enterprise edi ...@@ -31,7 +31,7 @@ Every major feature (regardless if present in GitLab Community or Enterprise edi
should present, at the beginning of the document, two main sections: **overview** and should present, at the beginning of the document, two main sections: **overview** and
**use cases**. Every GitLab EE-only feature should also contain these sections. **use cases**. Every GitLab EE-only feature should also contain these sections.
**Overview**: at the name suggests, the goal here is to provide an overview of the feature. **Overview**: as the name suggests, the goal here is to provide an overview of the feature.
Describe what is it, what it does, why it is important/cool/nice-to-have, Describe what is it, what it does, why it is important/cool/nice-to-have,
what problem it solves, and what you can do with this feature that you couldn't what problem it solves, and what you can do with this feature that you couldn't
do before. do before.
......
...@@ -299,9 +299,9 @@ sudo usermod -aG redis git ...@@ -299,9 +299,9 @@ sudo usermod -aG redis git
### Clone the Source ### Clone the Source
# Clone GitLab repository # Clone GitLab repository
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-3-stable gitlab sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-4-stable gitlab
**Note:** You can change `10-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! **Note:** You can change `10-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It ### Configure It
......
...@@ -30,7 +30,8 @@ with all their related data and be moved into a new GitLab instance. ...@@ -30,7 +30,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version | | GitLab version | Import/Export version |
| ---------------- | --------------------- | | ---------------- | --------------------- |
| 10.3 to current | 0.2.1 | | 10.4 to current | 0.2.2 |
| 10.3 | 0.2.1 |
| 10.0 | 0.2.0 | | 10.0 | 0.2.0 |
| 9.4.0 | 0.1.8 | | 9.4.0 | 0.1.8 |
| 9.2.0 | 0.1.7 | | 9.2.0 | 0.1.7 |
......
@public
Feature: Explore Groups
Background:
Given group "TestGroup" has private project "Enterprise"
@javascript
Scenario: I should see group with private and internal projects as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
And I visit group "TestGroup" page
Then I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group issues for internal project as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
And I visit group "TestGroup" issues page
Then I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group merge requests for internal project as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
And I visit group "TestGroup" merge requests page
Then I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group with private, internal and public projects as visitor
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I visit group "TestGroup" page
Then I should see project "Community" items
And I should not see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group issues for public project as visitor
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I visit group "TestGroup" issues page
Then I should see project "Community" items
And I should not see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group merge requests for public project as visitor
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I visit group "TestGroup" merge requests page
Then I should see project "Community" items
And I should not see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group with private, internal and public projects as user
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I sign in as a user
And I visit group "TestGroup" page
Then I should see project "Community" items
And I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group issues for internal and public projects as user
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I sign in as a user
And I visit group "TestGroup" issues page
Then I should see project "Community" items
And I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group merge requests for internal and public projects as user
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
When I sign in as a user
And I visit group "TestGroup" merge requests page
Then I should see project "Community" items
And I should see project "Internal" items
And I should not see project "Enterprise" items
@javascript
Scenario: I should see group with public project in public groups area
Given group "TestGroup" has public project "Community"
When I visit the public groups area
Then I should see group "TestGroup"
@javascript
Scenario: I should see group with public project in public groups area as user
Given group "TestGroup" has public project "Community"
When I sign in as a user
And I visit the public groups area
Then I should see group "TestGroup"
@javascript
Scenario: I should see group with internal project in public groups area as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
And I visit the public groups area
Then I should see group "TestGroup"
Feature: Invites
Background:
Given "John Doe" is owner of group "Owned"
And "John Doe" has invited "user@example.com" to group "Owned"
Scenario: Viewing invitation when signed out
When I visit the invitation page
Then I should be redirected to the sign in page
And I should see a notice telling me to sign in
Scenario: Signing in to view invitation
When I visit the invitation page
And I sign in as "Mary Jane"
Then I should be redirected to the invitation page
Scenario: Viewing invitation when signed in
Given I sign in as "Mary Jane"
And I visit the invitation page
Then I should see the invitation details
And I should see an "Accept invitation" button
And I should see a "Decline" button
Scenario: Viewing invitation as an existing member
Given I sign in as "John Doe"
And I visit the invitation page
Then I should see a message telling me I'm already a member
Scenario: Accepting the invitation
Given I sign in as "Mary Jane"
And I visit the invitation page
And I click the "Accept invitation" button
Then I should be redirected to the group page
And I should see a notice telling me I have access
Scenario: Declining the application when signed in
Given I sign in as "Mary Jane"
And I visit the invitation page
And I click the "Decline" button
Then I should be redirected to the dashboard
And I should see a notice telling me I have declined
Scenario: Declining the application when signed out
When I visit the invitation's decline page
Then I should be redirected to the sign in page
And I should see a notice telling me I have declined
class Spinach::Features::ExploreGroups < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedGroup
include SharedProject
step 'group "TestGroup" has private project "Enterprise"' do
group_has_project("TestGroup", "Enterprise", Gitlab::VisibilityLevel::PRIVATE)
end
step 'group "TestGroup" has internal project "Internal"' do
group_has_project("TestGroup", "Internal", Gitlab::VisibilityLevel::INTERNAL)
end
step 'group "TestGroup" has public project "Community"' do
group_has_project("TestGroup", "Community", Gitlab::VisibilityLevel::PUBLIC)
end
step '"John Doe" is owner of group "TestGroup"' do
group = Group.find_by(name: "TestGroup") || create(:group, name: "TestGroup")
user = create(:user, name: "John Doe")
group.add_owner(user)
end
step 'I visit group "TestGroup" page' do
visit group_path(Group.find_by(name: "TestGroup"))
end
step 'I visit group "TestGroup" issues page' do
visit issues_group_path(Group.find_by(name: "TestGroup"))
end
step 'I visit group "TestGroup" merge requests page' do
visit merge_requests_group_path(Group.find_by(name: "TestGroup"))
end
step 'I visit group "TestGroup" members page' do
visit group_group_members_path(Group.find_by(name: "TestGroup"))
end
step 'I should not see project "Enterprise" items' do
expect(page).not_to have_content "Enterprise"
end
step 'I should see project "Internal" items' do
expect(page).to have_content "Internal"
end
step 'I should not see project "Internal" items' do
expect(page).not_to have_content "Internal"
end
step 'I should see project "Community" items' do
expect(page).to have_content "Community"
end
step 'I change filter to Everyone\'s' do
click_link "Everyone's"
end
step 'I should see group member "John Doe"' do
expect(page).to have_content "John Doe"
end
protected
def group_has_project(groupname, projectname, visibility_level)
group = Group.find_by(name: groupname) || create(:group, name: groupname)
project = create(:project,
namespace: group,
name: projectname,
path: "#{groupname}-#{projectname}",
visibility_level: visibility_level
)
create(:issue,
title: "#{projectname} feature",
project: project
)
create(:merge_request,
title: "#{projectname} feature implemented",
source_project: project,
target_project: project
)
create(:closed_issue_event,
project: project
)
end
end
class Spinach::Features::Invites < Spinach::FeatureSteps
include SharedAuthentication
include SharedUser
include SharedGroup
step '"John Doe" has invited "user@example.com" to group "Owned"' do
user = User.find_by(name: "John Doe")
group = Group.find_by(name: "Owned")
group.add_developer("user@example.com", user)
end
step 'I visit the invitation page' do
group = Group.find_by(name: "Owned")
invite = group.group_members.invite.last
invite.generate_invite_token!
@raw_invite_token = invite.raw_invite_token
visit invite_path(@raw_invite_token)
end
step 'I should be redirected to the sign in page' do
expect(current_path).to eq(new_user_session_path)
end
step 'I should see a notice telling me to sign in' do
expect(page).to have_content "To accept this invitation, sign in"
end
step 'I should be redirected to the invitation page' do
expect(current_path).to eq(invite_path(@raw_invite_token))
end
step 'I should see the invitation details' do
expect(page).to have_content("You have been invited by John Doe to join group Owned as Developer.")
end
step "I should see a message telling me I'm already a member" do
expect(page).to have_content("However, you are already a member of this group.")
end
step 'I should see an "Accept invitation" button' do
expect(page).to have_link("Accept invitation")
end
step 'I should see a "Decline" button' do
expect(page).to have_link("Decline")
end
step 'I click the "Accept invitation" button' do
page.click_link "Accept invitation"
end
step 'I should be redirected to the group page' do
group = Group.find_by(name: "Owned")
expect(current_path).to eq(group_path(group))
end
step 'I should see a notice telling me I have access' do
expect(page).to have_content("You have been granted Developer access to group Owned.")
end
step 'I click the "Decline" button' do
page.click_link "Decline"
end
step 'I should be redirected to the dashboard' do
expect(current_path).to eq(dashboard_projects_path)
end
step 'I should see a notice telling me I have declined' do
expect(page).to have_content("You have declined the invitation to join group Owned.")
end
step "I visit the invitation's decline page" do
group = Group.find_by(name: "Owned")
invite = group.group_members.invite.last
invite.generate_invite_token!
@raw_invite_token = invite.raw_invite_token
visit decline_invite_path(@raw_invite_token)
end
end
...@@ -11,7 +11,7 @@ module SharedBuilds ...@@ -11,7 +11,7 @@ module SharedBuilds
step 'project has a recent build' do step 'project has a recent build' do
@pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
@build = create(:ci_build, :coverage, pipeline: @pipeline) @build = create(:ci_build, :running, :coverage, pipeline: @pipeline)
end end
step 'recent build is successful' do step 'recent build is successful' do
......
...@@ -842,6 +842,12 @@ into similar problems in the future (e.g. when new tables are created). ...@@ -842,6 +842,12 @@ into similar problems in the future (e.g. when new tables are created).
def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE) def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id') raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
# To not overload the worker too much we enforce a minimum interval both
# when scheduling and performing jobs.
if delay_interval < BackgroundMigrationWorker::MIN_INTERVAL
delay_interval = BackgroundMigrationWorker::MIN_INTERVAL
end
model_class.each_batch(of: batch_size) do |relation, index| model_class.each_batch(of: batch_size) do |relation, index|
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
......
...@@ -71,5 +71,16 @@ module Gitlab ...@@ -71,5 +71,16 @@ module Gitlab
redis.exists(@redis_shared_state_key) redis.exists(@redis_shared_state_key)
end end
end end
# Returns the TTL of the Redis key.
#
# This method will return `nil` if no TTL could be obtained.
def ttl
Gitlab::Redis::SharedState.with do |redis|
ttl = redis.ttl(@redis_shared_state_key)
ttl if ttl.positive?
end
end
end end
end end
...@@ -173,8 +173,8 @@ module Gitlab ...@@ -173,8 +173,8 @@ module Gitlab
end end
def find_by_rugged(repository, sha, path, limit:) def find_by_rugged(repository, sha, path, limit:)
commit = repository.lookup(sha) rugged_commit = repository.lookup(sha)
root_tree = commit.tree root_tree = rugged_commit.tree
blob_entry = find_entry_by_path(repository, root_tree.oid, path) blob_entry = find_entry_by_path(repository, root_tree.oid, path)
......
...@@ -15,8 +15,6 @@ module Gitlab ...@@ -15,8 +15,6 @@ module Gitlab
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
delegate :tree, to: :rugged_commit
def ==(other) def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit) return false unless other.is_a?(Gitlab::Git::Commit)
...@@ -452,6 +450,11 @@ module Gitlab ...@@ -452,6 +450,11 @@ module Gitlab
) )
end end
# Is this the same as Blob.find_entry_by_path ?
def rugged_tree_entry(path)
rugged_commit.tree.path(path)
end
private private
def init_from_hash(hash) def init_from_hash(hash)
......
...@@ -10,6 +10,7 @@ module Gitlab ...@@ -10,6 +10,7 @@ module Gitlab
DEFAULT_MODE = 0o100644 DEFAULT_MODE = 0o100644
ACTIONS = %w(create create_dir update move delete).freeze ACTIONS = %w(create create_dir update move delete).freeze
ACTION_OPTIONS = %i(file_path previous_path content encoding).freeze
attr_reader :repository, :raw_index attr_reader :repository, :raw_index
...@@ -20,6 +21,11 @@ module Gitlab ...@@ -20,6 +21,11 @@ module Gitlab
delegate :read_tree, :get, to: :raw_index delegate :read_tree, :get, to: :raw_index
def apply(action, options)
validate_action!(action)
public_send(action, options.slice(*ACTION_OPTIONS)) # rubocop:disable GitlabSecurity/PublicSend
end
def write_tree def write_tree
raw_index.write_tree(repository.rugged) raw_index.write_tree(repository.rugged)
end end
...@@ -140,6 +146,12 @@ module Gitlab ...@@ -140,6 +146,12 @@ module Gitlab
rescue Rugged::IndexError => e rescue Rugged::IndexError => e
raise IndexError, e.message raise IndexError, e.message
end end
def validate_action!(action)
unless ACTIONS.include?(action.to_s)
raise ArgumentError, "Unknown action '#{action}'"
end
end
end end
end end
end end
...@@ -1163,23 +1163,13 @@ module Gitlab ...@@ -1163,23 +1163,13 @@ module Gitlab
end end
def fetch_repository_as_mirror(repository) def fetch_repository_as_mirror(repository)
remote_name = "tmp-#{SecureRandom.hex}" gitaly_migrate(:remote_fetch_internal_remote) do |is_enabled|
# Notice that this feature flag is not for `fetch_repository_as_mirror`
# as a whole but for the fetching mechanism (file path or gitaly-ssh).
url, env = gitaly_migrate(:fetch_internal) do |is_enabled|
if is_enabled if is_enabled
repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository) gitaly_remote_client.fetch_internal_remote(repository)
[GITALY_INTERNAL_URL, repository.fetch_env]
else else
[repository.path, nil] rugged_fetch_repository_as_mirror(repository)
end end
end end
add_remote(remote_name, url, mirror_refmap: :all_refs)
fetch_remote(remote_name, env: env)
ensure
remove_remote(remote_name)
end end
def blob_at(sha, path) def blob_at(sha, path)
...@@ -1187,7 +1177,7 @@ module Gitlab ...@@ -1187,7 +1177,7 @@ module Gitlab
end end
# Items should be of format [[commit_id, path], [commit_id1, path1]] # Items should be of format [[commit_id, path], [commit_id1, path1]]
def batch_blobs(items, blob_size_limit: nil) def batch_blobs(items, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit) Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit)
end end
...@@ -1300,6 +1290,42 @@ module Gitlab ...@@ -1300,6 +1290,42 @@ module Gitlab
success || gitlab_projects_error success || gitlab_projects_error
end end
# rubocop:disable Metrics/ParameterLists
def multi_action(
user, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_repository: self)
OperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_repository: start_repository
) do |start_commit|
index = Gitlab::Git::Index.new(self)
parents = []
if start_commit
index.read_tree(start_commit.rugged_commit.tree)
parents = [start_commit.sha]
end
actions.each { |opts| index.apply(opts.delete(:action), opts) }
committer = user_to_committer(user)
author = Gitlab::Git.committer_hash(email: author_email, name: author_name) || committer
options = {
tree: index.write_tree,
message: message,
parents: parents,
author: author,
committer: committer
}
create_commit(options)
end
end
# rubocop:enable Metrics/ParameterLists
def gitaly_repository def gitaly_repository
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository) Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
end end
...@@ -2034,6 +2060,16 @@ module Gitlab ...@@ -2034,6 +2060,16 @@ module Gitlab
false false
end end
def rugged_fetch_repository_as_mirror(repository)
remote_name = "tmp-#{SecureRandom.hex}"
repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository)
add_remote(remote_name, GITALY_INTERNAL_URL, mirror_refmap: :all_refs)
fetch_remote(remote_name, env: repository.fetch_env)
ensure
remove_remote(remote_name)
end
def fetch_remote(remote_name = 'origin', env: nil) def fetch_remote(remote_name = 'origin', env: nil)
run_git(['fetch', remote_name], env: env).last.zero? run_git(['fetch', remote_name], env: env).last.zero?
end end
......
...@@ -23,6 +23,19 @@ module Gitlab ...@@ -23,6 +23,19 @@ module Gitlab
response.result response.result
end end
def fetch_internal_remote(repository)
request = Gitaly::FetchInternalRemoteRequest.new(
repository: @gitaly_repo,
remote_repository: repository.gitaly_repository
)
response = GitalyClient.call(@storage, :remote_service,
:fetch_internal_remote, request,
remote_storage: repository.storage)
response.result
end
end end
end end
end end
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