Commit 9efb1875 authored by Matija Čupić's avatar Matija Čupić

Merge branch 'master' into 38542-application-control-panel-in-settings-page

parents 44be5883 dd6aade3
### Description ### Problem to solve
(Include problem, use cases, benefits, and/or goals) ### Further details
(Include use cases, benefits, and/or goals)
### Proposal ### Proposal
### What does success look like, and how can we measure that?
(If no way to measure success, link to an issue that will implement a way to measure this)
### Links / references ### Links / references
/label ~"feature proposal" /label ~"feature proposal"
...@@ -182,7 +182,7 @@ Assigning a team label makes sure issues get the attention of the appropriate ...@@ -182,7 +182,7 @@ Assigning a team label makes sure issues get the attention of the appropriate
people. people.
The current team labels are ~Distribution, ~"CI/CD", ~Discussion, ~Documentation, ~Quality, The current team labels are ~Distribution, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX". ~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products", ~"Configuration", and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team. responsibility of each team.
...@@ -349,7 +349,7 @@ on those issues. Please select someone with relevant experience from the ...@@ -349,7 +349,7 @@ on those issues. Please select someone with relevant experience from the
[GitLab team][team]. If there is nobody mentioned with that expertise look in [GitLab team][team]. If there is nobody mentioned with that expertise look in
the commit history for the affected files to find someone. the commit history for the affected files to find someone.
[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/ [described in our handbook]: https://about.gitlab.com/handbook/engineering/issue-triage/
[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815 [issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
### Feature proposals ### Feature proposals
...@@ -512,7 +512,7 @@ request is as follows: ...@@ -512,7 +512,7 @@ request is as follows:
1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code 1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code
1. [Generate a changelog entry with `bin/changelog`][changelog] 1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the 1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide] [documentation guidelines][doc-guidelines]
1. If you have multiple commits please combine them into a few logically 1. If you have multiple commits please combine them into a few logically
organized commits by [squashing them][git-squash] organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork 1. Push the commit(s) to your fork
...@@ -727,7 +727,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -727,7 +727,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout [rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[changelog]: doc/development/changelog.md "Generate a changelog entry" [changelog]: doc/development/changelog.md "Generate a changelog entry"
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [doc-guidelines]: doc/development/documentation/index.md "Documentation guidelines"
[js-styleguide]: doc/development/fe_guide/style_guide_js.md "JavaScript styleguide" [js-styleguide]: doc/development/fe_guide/style_guide_js.md "JavaScript styleguide"
[scss-styleguide]: doc/development/fe_guide/style_guide_scss.md "SCSS styleguide" [scss-styleguide]: doc/development/fe_guide/style_guide_scss.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
......
...@@ -93,6 +93,10 @@ gem 'grape', '~> 1.0' ...@@ -93,6 +93,10 @@ gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.7.1' gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors' gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.8.0'
gem 'graphiql-rails', '~> 1.4.10'
# 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'
...@@ -342,7 +346,7 @@ group :development, :test do ...@@ -342,7 +346,7 @@ group :development, :test do
gem 'capybara', '~> 2.15' gem 'capybara', '~> 2.15'
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'selenium-webdriver', '~> 3.5' gem 'selenium-webdriver', '~> 3.12'
gem 'spring', '~> 2.0.0' gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-rspec', '~> 1.0.4'
...@@ -374,7 +378,7 @@ end ...@@ -374,7 +378,7 @@ end
group :test do group :test do
gem 'shoulda-matchers', '~> 3.1.2', require: false gem 'shoulda-matchers', '~> 3.1.2', require: false
gem 'email_spec', '~> 1.6.0' gem 'email_spec', '~> 2.2.0'
gem 'json-schema', '~> 2.8.0' gem 'json-schema', '~> 2.8.0'
gem 'webmock', '~> 2.3.2' gem 'webmock', '~> 2.3.2'
gem 'rails-controller-testing' if rails5? # Rails5 only gem. gem 'rails-controller-testing' if rails5? # Rails5 only gem.
...@@ -384,7 +388,7 @@ group :test do ...@@ -384,7 +388,7 @@ group :test do
gem 'test-prof', '~> 0.2.5' gem 'test-prof', '~> 0.2.5'
end end
gem 'octokit', '~> 4.8' gem 'octokit', '~> 4.9'
gem 'mail_room', '~> 0.9.1' gem 'mail_room', '~> 0.9.1'
......
...@@ -115,7 +115,7 @@ GEM ...@@ -115,7 +115,7 @@ GEM
mime-types (>= 1.16) mime-types (>= 1.16)
cause (0.1) cause (0.1)
charlock_holmes (0.7.6) charlock_holmes (0.7.6)
childprocess (0.7.0) childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2) chronic (0.10.2)
chronic_duration (0.10.6) chronic_duration (0.10.6)
...@@ -172,15 +172,16 @@ GEM ...@@ -172,15 +172,16 @@ GEM
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.2) doorkeeper (4.3.2)
railties (>= 4.2) railties (>= 4.2)
doorkeeper-openid_connect (1.3.0) doorkeeper-openid_connect (1.4.0)
doorkeeper (~> 4.3) doorkeeper (~> 4.3)
json-jwt (~> 1.6) json-jwt (~> 1.6)
dropzonejs-rails (0.7.2) dropzonejs-rails (0.7.2)
rails (> 3.1) rails (> 3.1)
email_reply_trimmer (0.1.6) email_reply_trimmer (0.1.6)
email_spec (1.6.0) email_spec (2.2.0)
htmlentities (~> 4.3.3)
launchy (~> 2.1) launchy (~> 2.1)
mail (~> 2.2) mail (~> 2.7)
encryptor (3.0.0) encryptor (3.0.0)
equalizer (0.0.11) equalizer (0.0.11)
erubis (2.7.0) erubis (2.7.0)
...@@ -358,12 +359,16 @@ GEM ...@@ -358,12 +359,16 @@ GEM
grape-entity (0.7.1) grape-entity (0.7.1)
activesupport (>= 4.0) activesupport (>= 4.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grape-path-helpers (1.0.1) grape-path-helpers (1.0.2)
activesupport (~> 4) activesupport (~> 4)
grape (~> 1.0) grape (~> 1.0)
rake (~> 12) rake (~> 12)
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
graphiql-rails (1.4.10)
railties
sprockets-rails
graphql (1.8.1)
grpc (1.11.0) grpc (1.11.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
...@@ -517,7 +522,7 @@ GEM ...@@ -517,7 +522,7 @@ GEM
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
octokit (4.8.0) octokit (4.9.0)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
omniauth (1.8.1) omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0) hashie (>= 3.4.6, < 3.6.0)
...@@ -828,9 +833,9 @@ GEM ...@@ -828,9 +833,9 @@ GEM
activesupport (>= 3.1) activesupport (>= 3.1)
select2-rails (3.5.9.3) select2-rails (3.5.9.3)
thor (~> 0.14) thor (~> 0.14)
selenium-webdriver (3.5.0) selenium-webdriver (3.12.0)
childprocess (~> 0.5) childprocess (~> 0.5)
rubyzip (~> 1.0) rubyzip (~> 1.2)
sentry-raven (2.7.2) sentry-raven (2.7.2)
faraday (>= 0.7.6, < 1.0) faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9) settingslogic (2.0.9)
...@@ -1012,7 +1017,7 @@ DEPENDENCIES ...@@ -1012,7 +1017,7 @@ DEPENDENCIES
doorkeeper-openid_connect (~> 1.3) doorkeeper-openid_connect (~> 1.3)
dropzonejs-rails (~> 0.7.1) dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0) email_spec (~> 2.2.0)
factory_bot_rails (~> 4.8.2) factory_bot_rails (~> 4.8.2)
faraday (~> 0.12) faraday (~> 0.12)
fast_blank fast_blank
...@@ -1052,6 +1057,8 @@ DEPENDENCIES ...@@ -1052,6 +1057,8 @@ DEPENDENCIES
grape-entity (~> 0.7.1) grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.0) grape-path-helpers (~> 1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0)
grpc (~> 1.11.0) grpc (~> 1.11.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
...@@ -1084,7 +1091,7 @@ DEPENDENCIES ...@@ -1084,7 +1091,7 @@ DEPENDENCIES
net-ssh (~> 4.2.0) net-ssh (~> 4.2.0)
nokogiri (~> 1.8.2) nokogiri (~> 1.8.2)
oauth2 (~> 1.4) oauth2 (~> 1.4)
octokit (~> 4.8) octokit (~> 4.9)
omniauth (~> 1.8) omniauth (~> 1.8)
omniauth-auth0 (~> 2.0.0) omniauth-auth0 (~> 2.0.0)
omniauth-authentiq (~> 0.3.3) omniauth-authentiq (~> 0.3.3)
...@@ -1154,7 +1161,7 @@ DEPENDENCIES ...@@ -1154,7 +1161,7 @@ DEPENDENCIES
scss_lint (~> 0.56.0) scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7) seed-fu (~> 2.3.7)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5) selenium-webdriver (~> 3.12)
sentry-raven (~> 2.7) sentry-raven (~> 2.7)
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
......
...@@ -72,8 +72,6 @@ GEM ...@@ -72,8 +72,6 @@ GEM
attr_encrypted (3.1.0) attr_encrypted (3.1.0)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
attr_required (1.0.1) attr_required (1.0.1)
autoprefixer-rails (8.1.0.1)
execjs
awesome_print (1.2.0) awesome_print (1.2.0)
axiom-types (0.1.1) axiom-types (0.1.1)
descendants_tracker (~> 0.0.4) descendants_tracker (~> 0.0.4)
...@@ -93,9 +91,6 @@ GEM ...@@ -93,9 +91,6 @@ GEM
binding_of_caller (0.7.3) binding_of_caller (0.7.3)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
blankslate (2.1.2.4) blankslate (2.1.2.4)
bootstrap-sass (3.3.7)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
bootstrap_form (2.7.0) bootstrap_form (2.7.0)
brakeman (4.2.1) brakeman (4.2.1)
browser (2.5.3) browser (2.5.3)
...@@ -175,7 +170,7 @@ GEM ...@@ -175,7 +170,7 @@ GEM
diff-lcs (1.3) diff-lcs (1.3)
diffy (3.1.0) diffy (3.1.0)
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20170404) domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.1) doorkeeper (4.3.1)
railties (>= 4.2) railties (>= 4.2)
...@@ -185,9 +180,10 @@ GEM ...@@ -185,9 +180,10 @@ GEM
dropzonejs-rails (0.7.4) dropzonejs-rails (0.7.4)
rails (> 3.1) rails (> 3.1)
email_reply_trimmer (0.1.10) email_reply_trimmer (0.1.10)
email_spec (1.6.0) email_spec (2.2.0)
htmlentities (~> 4.3.3)
launchy (~> 2.1) launchy (~> 2.1)
mail (~> 2.2) mail (~> 2.7)
encryptor (3.0.0) encryptor (3.0.0)
equalizer (0.0.11) equalizer (0.0.11)
erubis (2.7.0) erubis (2.7.0)
...@@ -288,7 +284,7 @@ GEM ...@@ -288,7 +284,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.99.0) gitaly-proto (0.100.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -365,9 +361,9 @@ GEM ...@@ -365,9 +361,9 @@ GEM
grape-entity (0.7.1) grape-entity (0.7.1)
activesupport (>= 4.0) activesupport (>= 4.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grape-route-helpers (2.1.0) grape-path-helpers (1.0.0)
activesupport activesupport
grape (>= 0.16.0) grape (~> 1.0)
rake rake
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
...@@ -417,6 +413,7 @@ GEM ...@@ -417,6 +413,7 @@ GEM
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.0.1) i18n (1.0.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
icalendar (2.4.1)
ice_nine (0.11.2) ice_nine (0.11.2)
influxdb (0.5.3) influxdb (0.5.3)
ipaddress (0.8.3) ipaddress (0.8.3)
...@@ -450,9 +447,9 @@ GEM ...@@ -450,9 +447,9 @@ GEM
kgio (2.11.2) kgio (2.11.2)
knapsack (1.16.0) knapsack (1.16.0)
rake rake
kubeclient (3.0.0) kubeclient (3.1.1)
http (~> 2.2.2) http (~> 2.2.2)
recursive-open-struct (~> 1.0.4) recursive-open-struct (~> 1.0, >= 1.0.4)
rest-client (~> 2.0) rest-client (~> 2.0)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
...@@ -521,15 +518,16 @@ GEM ...@@ -521,15 +518,16 @@ GEM
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
octokit (4.8.0) octokit (4.9.0)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
omniauth (1.8.1) omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0) hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
omniauth-auth0 (2.0.0) omniauth-auth0 (2.0.0)
omniauth-oauth2 (~> 1.4) omniauth-oauth2 (~> 1.4)
omniauth-authentiq (0.3.1) omniauth-authentiq (0.3.3)
omniauth-oauth2 (~> 1.3, >= 1.3.1) jwt (>= 1.5)
omniauth-oauth2 (>= 1.5)
omniauth-azure-oauth2 (0.0.9) omniauth-azure-oauth2 (0.0.9)
jwt (~> 1.0) jwt (~> 1.0)
omniauth (~> 1.0) omniauth (~> 1.0)
...@@ -628,7 +626,7 @@ GEM ...@@ -628,7 +626,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.9.2) prometheus-client-mmap (0.9.3)
pry (0.11.3) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
...@@ -702,11 +700,11 @@ GEM ...@@ -702,11 +700,11 @@ GEM
ffi ffi
rbnacl-libsodium (1.0.16) rbnacl-libsodium (1.0.16)
rbnacl (>= 3.0.1) rbnacl (>= 3.0.1)
rdoc (4.3.0) rdoc (6.0.4)
re2 (1.1.1) re2 (1.1.1)
recaptcha (3.4.0) recaptcha (3.4.0)
json json
recursive-open-struct (1.0.5) recursive-open-struct (1.1.0)
redcarpet (3.4.0) redcarpet (3.4.0)
redis (3.3.5) redis (3.3.5)
redis-actionpack (5.0.2) redis-actionpack (5.0.2)
...@@ -716,8 +714,8 @@ GEM ...@@ -716,8 +714,8 @@ GEM
redis-activesupport (5.0.4) redis-activesupport (5.0.4)
activesupport (>= 3, < 6) activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2) redis-store (>= 1.3, < 2)
redis-namespace (1.5.3) redis-namespace (1.6.0)
redis (~> 3.0, >= 3.0.4) redis (>= 3.0.4)
redis-rack (2.0.4) redis-rack (2.0.4)
rack (>= 1.5, < 3) rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
...@@ -836,7 +834,7 @@ GEM ...@@ -836,7 +834,7 @@ GEM
activesupport (>= 3.1) activesupport (>= 3.1)
select2-rails (3.5.10) select2-rails (3.5.10)
thor (~> 0.14) thor (~> 0.14)
selenium-webdriver (3.11.0) selenium-webdriver (3.12.0)
childprocess (~> 0.5) childprocess (~> 0.5)
rubyzip (~> 1.2) rubyzip (~> 1.2)
sentry-raven (2.7.2) sentry-raven (2.7.2)
...@@ -986,7 +984,7 @@ DEPENDENCIES ...@@ -986,7 +984,7 @@ DEPENDENCIES
asciidoctor-plantuml (= 0.0.8) asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.4) asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0) attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0) awesome_print
babosa (~> 1.0.2) babosa (~> 1.0.2)
base32 (~> 0.3.0) base32 (~> 0.3.0)
batch-loader (~> 1.2.1) batch-loader (~> 1.2.1)
...@@ -994,7 +992,6 @@ DEPENDENCIES ...@@ -994,7 +992,6 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0) benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0) better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0) bootstrap_form (~> 2.7.0)
brakeman (~> 4.2) brakeman (~> 4.2)
browser (~> 2.2) browser (~> 2.2)
...@@ -1021,7 +1018,7 @@ DEPENDENCIES ...@@ -1021,7 +1018,7 @@ DEPENDENCIES
doorkeeper-openid_connect (~> 1.3) doorkeeper-openid_connect (~> 1.3)
dropzonejs-rails (~> 0.7.1) dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0) email_spec (~> 2.2.0)
factory_bot_rails (~> 4.8.2) factory_bot_rails (~> 4.8.2)
faraday (~> 0.12) faraday (~> 0.12)
fast_blank fast_blank
...@@ -1045,7 +1042,7 @@ DEPENDENCIES ...@@ -1045,7 +1042,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.99.0) gitaly-proto (~> 0.100.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
...@@ -1059,7 +1056,7 @@ DEPENDENCIES ...@@ -1059,7 +1056,7 @@ DEPENDENCIES
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.7.1) grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0) grape-path-helpers (~> 1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.11.0) grpc (~> 1.11.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
...@@ -1070,6 +1067,7 @@ DEPENDENCIES ...@@ -1070,6 +1067,7 @@ DEPENDENCIES
html-pipeline (~> 2.7.1) html-pipeline (~> 2.7.1)
html2text html2text
httparty (~> 0.13.3) httparty (~> 0.13.3)
icalendar
influxdb (~> 0.2) influxdb (~> 0.2)
jira-ruby (~> 1.4) jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2) jquery-atwho-rails (~> 1.3.2)
...@@ -1077,7 +1075,7 @@ DEPENDENCIES ...@@ -1077,7 +1075,7 @@ DEPENDENCIES
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 1.0) kaminari (~> 1.0)
knapsack (~> 1.16) knapsack (~> 1.16)
kubeclient (~> 3.0) kubeclient (~> 3.1.0)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
license_finder (~> 3.1) license_finder (~> 3.1)
licensee (~> 8.9) licensee (~> 8.9)
...@@ -1092,10 +1090,10 @@ DEPENDENCIES ...@@ -1092,10 +1090,10 @@ DEPENDENCIES
net-ssh (~> 4.2.0) net-ssh (~> 4.2.0)
nokogiri (~> 1.8.2) nokogiri (~> 1.8.2)
oauth2 (~> 1.4) oauth2 (~> 1.4)
octokit (~> 4.8) octokit (~> 4.9)
omniauth (~> 1.8) omniauth (~> 1.8)
omniauth-auth0 (~> 2.0.0) omniauth-auth0 (~> 2.0.0)
omniauth-authentiq (~> 0.3.1) omniauth-authentiq (~> 0.3.3)
omniauth-azure-oauth2 (~> 0.0.9) omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4) omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0) omniauth-facebook (~> 4.0.0)
...@@ -1118,7 +1116,7 @@ DEPENDENCIES ...@@ -1118,7 +1116,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
premailer-rails (~> 1.9.7) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.9.2) prometheus-client-mmap (~> 0.9.3)
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)
...@@ -1134,12 +1132,12 @@ DEPENDENCIES ...@@ -1134,12 +1132,12 @@ DEPENDENCIES
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rbnacl (~> 4.0) rbnacl (~> 4.0)
rbnacl-libsodium rbnacl-libsodium
rdoc (~> 4.2) rdoc (~> 6.0)
re2 (~> 1.1.1) re2 (~> 1.1.1)
recaptcha (~> 3.0) recaptcha (~> 3.0)
redcarpet (~> 3.4) redcarpet (~> 3.4)
redis (~> 3.2) redis (~> 3.2)
redis-namespace (~> 1.5.2) redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2) redis-rails (~> 5.0.2)
request_store (~> 1.3) request_store (~> 1.3)
responders (~> 2.0) responders (~> 2.0)
...@@ -1154,6 +1152,7 @@ DEPENDENCIES ...@@ -1154,6 +1152,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.22.1) rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.17.0) ruby-prof (~> 0.17.0)
ruby-progressbar
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4) rufus-scheduler (~> 3.4)
rugged (~> 0.27) rugged (~> 0.27)
...@@ -1162,12 +1161,12 @@ DEPENDENCIES ...@@ -1162,12 +1161,12 @@ DEPENDENCIES
scss_lint (~> 0.56.0) scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7) seed-fu (~> 2.3.7)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5) selenium-webdriver (~> 3.12)
sentry-raven (~> 2.7) sentry-raven (~> 2.7)
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 3.1.2) shoulda-matchers (~> 3.1.2)
sidekiq (~> 5.0) sidekiq (~> 5.1)
sidekiq-cron (~> 0.6.0) sidekiq-cron (~> 0.6.0)
sidekiq-limit_fetch (~> 3.4) sidekiq-limit_fetch (~> 3.4)
simple_po_parser (~> 1.1.2) simple_po_parser (~> 1.1.2)
...@@ -1199,4 +1198,4 @@ DEPENDENCIES ...@@ -1199,4 +1198,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.16.1 1.16.2
...@@ -168,6 +168,7 @@ the stable branch are: ...@@ -168,6 +168,7 @@ the stable branch are:
* Fixes for [regressions](#regressions) * Fixes for [regressions](#regressions)
* Fixes for security issues * Fixes for security issues
* Fixes or improvements to automated QA scenarios
* New or updated translations (as long as they do not touch application code) * New or updated translations (as long as they do not touch application code)
During the feature freeze all merge requests that are meant to go into the During the feature freeze all merge requests that are meant to go into the
......
...@@ -126,5 +126,5 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on ...@@ -126,5 +126,5 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome? ## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
[These people](https://twitter.com/gitlab/likes) seem to like it. [These people](https://twitter.com/gitlab/likes) seem to like it.
...@@ -187,7 +187,7 @@ ...@@ -187,7 +187,7 @@
role="row" role="row"
> >
<div <div
class="alert alert-danger alert-block append-bottom-0" class="alert alert-danger alert-block append-bottom-0 clusters-error-alert"
role="gridcell" role="gridcell"
> >
<div> <div>
......
<script> <script>
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
...@@ -20,6 +21,13 @@ export default { ...@@ -20,6 +21,13 @@ export default {
}, },
methods: { methods: {
...mapActions(['updateActivityBarView']), ...mapActions(['updateActivityBarView']),
changedActivityView(e, view) {
e.currentTarget.blur();
this.updateActivityBarView(view);
$(e.currentTarget).tooltip('hide');
},
}, },
activityBarViews, activityBarViews,
}; };
...@@ -54,7 +62,7 @@ export default { ...@@ -54,7 +62,7 @@ export default {
:class="{ :class="{
active: currentActivityView === $options.activityBarViews.edit active: currentActivityView === $options.activityBarViews.edit
}" }"
@click.prevent="updateActivityBarView($options.activityBarViews.edit)" @click.prevent="changedActivityView($event, $options.activityBarViews.edit)"
:title="s__('IDE|Edit')" :title="s__('IDE|Edit')"
:aria-label="s__('IDE|Edit')" :aria-label="s__('IDE|Edit')"
> >
...@@ -73,7 +81,7 @@ export default { ...@@ -73,7 +81,7 @@ export default {
:class="{ :class="{
active: currentActivityView === $options.activityBarViews.review active: currentActivityView === $options.activityBarViews.review
}" }"
@click.prevent="updateActivityBarView($options.activityBarViews.review)" @click.prevent="changedActivityView($event, $options.activityBarViews.review)"
:title="s__('IDE|Review')" :title="s__('IDE|Review')"
:aria-label="s__('IDE|Review')" :aria-label="s__('IDE|Review')"
> >
...@@ -92,7 +100,7 @@ export default { ...@@ -92,7 +100,7 @@ export default {
:class="{ :class="{
active: currentActivityView === $options.activityBarViews.commit active: currentActivityView === $options.activityBarViews.commit
}" }"
@click.prevent="updateActivityBarView($options.activityBarViews.commit)" @click.prevent="changedActivityView($event, $options.activityBarViews.commit)"
:title="s__('IDE|Commit')" :title="s__('IDE|Commit')"
:aria-label="s__('IDE|Commit')" :aria-label="s__('IDE|Commit')"
> >
......
...@@ -54,7 +54,7 @@ export default { ...@@ -54,7 +54,7 @@ export default {
placement: 'top', placement: 'top',
content: sprintf( content: sprintf(
__(` __(`
The character highligher helps you keep the subject line to %{titleLength} characters The character highlighter helps you keep the subject line to %{titleLength} characters
and wrap the body at %{bodyLength} so they are readable in git. and wrap the body at %{bodyLength} so they are readable in git.
`), `),
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH }, { titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
......
...@@ -11,17 +11,20 @@ export default { ...@@ -11,17 +11,20 @@ export default {
}, },
computed: { computed: {
...mapGetters(['currentMergeRequest']), ...mapGetters(['currentMergeRequest']),
...mapState(['viewer']), ...mapState(['viewer', 'currentMergeRequestId']),
showLatestChangesText() { showLatestChangesText() {
return !this.currentMergeRequest || this.viewer === viewerTypes.diff; return !this.currentMergeRequestId || this.viewer === viewerTypes.diff;
}, },
showMergeRequestText() { showMergeRequestText() {
return this.currentMergeRequest && this.viewer === viewerTypes.mr; return this.currentMergeRequestId && this.viewer === viewerTypes.mr;
},
mergeRequestId() {
return `!${this.currentMergeRequest.iid}`;
}, },
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff); this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff);
}); });
}, },
methods: { methods: {
...@@ -54,7 +57,11 @@ export default { ...@@ -54,7 +57,11 @@ export default {
</template> </template>
<template v-else-if="showMergeRequestText"> <template v-else-if="showMergeRequestText">
{{ __('Merge request') }} {{ __('Merge request') }}
(<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>) (<a
v-if="currentMergeRequest"
:href="currentMergeRequest.web_url"
v-text="mergeRequestId"
></a>)
</template> </template>
</div> </div>
</template> </template>
......
<script> <script>
import $ from 'jquery';
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -13,6 +14,7 @@ import CommitSection from './repo_commit_section.vue'; ...@@ -13,6 +14,7 @@ import CommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue'; import CommitForm from './commit_sidebar/form.vue';
import IdeReview from './ide_review.vue'; import IdeReview from './ide_review.vue';
import SuccessMessage from './commit_sidebar/success_message.vue'; import SuccessMessage from './commit_sidebar/success_message.vue';
import MergeRequestDropdown from './merge_requests/dropdown.vue';
import { activityBarViews } from '../constants'; import { activityBarViews } from '../constants';
export default { export default {
...@@ -32,10 +34,12 @@ export default { ...@@ -32,10 +34,12 @@ export default {
CommitForm, CommitForm,
IdeReview, IdeReview,
SuccessMessage, SuccessMessage,
MergeRequestDropdown,
}, },
data() { data() {
return { return {
showTooltip: false, showTooltip: false,
showMergeRequestsDropdown: false,
}; };
}, },
computed: { computed: {
...@@ -46,6 +50,7 @@ export default { ...@@ -46,6 +50,7 @@ export default {
'changedFiles', 'changedFiles',
'stagedFiles', 'stagedFiles',
'lastCommitMsg', 'lastCommitMsg',
'currentMergeRequestId',
]), ]),
...mapGetters(['currentProject', 'someUncommitedChanges']), ...mapGetters(['currentProject', 'someUncommitedChanges']),
showSuccessMessage() { showSuccessMessage() {
...@@ -61,9 +66,39 @@ export default { ...@@ -61,9 +66,39 @@ export default {
watch: { watch: {
currentBranchId() { currentBranchId() {
this.$nextTick(() => { this.$nextTick(() => {
if (!this.$refs.branchId) return;
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth; this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
}); });
}, },
loading() {
this.$nextTick(() => {
this.addDropdownListeners();
});
},
},
mounted() {
this.addDropdownListeners();
},
beforeDestroy() {
$(this.$refs.mergeRequestDropdown)
.off('show.bs.dropdown')
.off('hide.bs.dropdown');
},
methods: {
addDropdownListeners() {
if (!this.$refs.mergeRequestDropdown) return;
$(this.$refs.mergeRequestDropdown)
.on('show.bs.dropdown', () => {
this.toggleMergeRequestDropdown();
}).on('hide.bs.dropdown', () => {
this.toggleMergeRequestDropdown();
});
},
toggleMergeRequestDropdown() {
this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown;
},
}, },
}; };
</script> </script>
...@@ -88,9 +123,13 @@ export default { ...@@ -88,9 +123,13 @@ export default {
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="context-header ide-context-header"> <div
<a class="context-header ide-context-header dropdown"
:href="currentProject.web_url" ref="mergeRequestDropdown"
>
<button
type="button"
data-toggle="dropdown"
> >
<div <div
v-if="currentProject.avatar_url" v-if="currentProject.avatar_url"
...@@ -114,19 +153,41 @@ export default { ...@@ -114,19 +153,41 @@ export default {
<div class="sidebar-context-title"> <div class="sidebar-context-title">
{{ currentProject.name }} {{ currentProject.name }}
</div> </div>
<div <div class="d-flex">
class="sidebar-context-title ide-sidebar-branch-title" <div
ref="branchId" v-if="currentBranchId"
v-tooltip class="sidebar-context-title ide-sidebar-branch-title"
:title="branchTooltipTitle" ref="branchId"
> v-tooltip
<icon :title="branchTooltipTitle"
name="branch" >
css-classes="append-right-5" <icon
/>{{ currentBranchId }} name="branch"
css-classes="append-right-5"
/>{{ currentBranchId }}
</div>
<div
v-if="currentMergeRequestId"
class="sidebar-context-title ide-sidebar-branch-title"
:class="{
'prepend-left-8': currentBranchId
}"
>
<icon
name="git-merge"
css-classes="append-right-5"
/>!{{ currentMergeRequestId }}
</div>
</div> </div>
</div> </div>
</a> <icon
class="ml-auto"
name="chevron-down"
/>
</button>
<merge-request-dropdown
:show="showMergeRequestsDropdown"
/>
</div> </div>
<div class="multi-file-commit-panel-inner-scroll"> <div class="multi-file-commit-panel-inner-scroll">
<component <component
......
...@@ -35,9 +35,7 @@ export default { ...@@ -35,9 +35,7 @@ export default {
}, },
watch: { watch: {
lastCommit() { lastCommit() {
if (!this.isPollingInitialized) { this.initPipelinePolling();
this.initPipelinePolling();
}
}, },
}, },
mounted() { mounted() {
...@@ -47,9 +45,8 @@ export default { ...@@ -47,9 +45,8 @@ export default {
if (this.intervalId) { if (this.intervalId) {
clearInterval(this.intervalId); clearInterval(this.intervalId);
} }
if (this.isPollingInitialized) {
this.stopPipelinePolling(); this.stopPipelinePolling();
}
}, },
methods: { methods: {
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']), ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
...@@ -59,8 +56,9 @@ export default { ...@@ -59,8 +56,9 @@ export default {
}, 1000); }, 1000);
}, },
initPipelinePolling() { initPipelinePolling() {
this.fetchLatestPipeline(); if (this.lastCommit) {
this.isPollingInitialized = true; this.fetchLatestPipeline();
}
}, },
commitAgeUpdate() { commitAgeUpdate() {
if (this.lastCommit) { if (this.lastCommit) {
......
<script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import { __ } from '../../../locale';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import ScrollButton from './detail/scroll_button.vue';
import JobDescription from './detail/description.vue';
const scrollPositions = {
top: 0,
bottom: 1,
};
export default {
directives: {
tooltip,
},
components: {
Icon,
ScrollButton,
JobDescription,
},
data() {
return {
scrollPos: scrollPositions.top,
};
},
computed: {
...mapState('pipelines', ['detailJob']),
isScrolledToBottom() {
return this.scrollPos === scrollPositions.bottom;
},
isScrolledToTop() {
return this.scrollPos === scrollPositions.top;
},
jobOutput() {
return this.detailJob.output || __('No messages were logged');
},
},
mounted() {
this.getTrace();
},
methods: {
...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']),
scrollDown() {
if (this.$refs.buildTrace) {
this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight);
}
},
scrollUp() {
if (this.$refs.buildTrace) {
this.$refs.buildTrace.scrollTo(0, 0);
}
},
scrollBuildLog: _.throttle(function buildLogScrollDebounce() {
const { scrollTop } = this.$refs.buildTrace;
const { offsetHeight, scrollHeight } = this.$refs.buildTrace;
if (scrollTop + offsetHeight === scrollHeight) {
this.scrollPos = scrollPositions.bottom;
} else if (scrollTop === 0) {
this.scrollPos = scrollPositions.top;
} else {
this.scrollPos = '';
}
}),
getTrace() {
return this.fetchJobTrace().then(() => this.scrollDown());
},
},
};
</script>
<template>
<div class="ide-pipeline build-page d-flex flex-column flex-fill">
<header class="ide-job-header d-flex align-items-center">
<button
class="btn btn-default btn-sm d-flex"
@click="setDetailJob(null)"
>
<icon
name="chevron-left"
/>
{{ __('View jobs') }}
</button>
</header>
<div class="top-bar d-flex border-left-0">
<job-description
:job="detailJob"
/>
<div class="controllers ml-auto">
<a
v-tooltip
:title="__('Show complete raw log')"
data-placement="top"
data-container="body"
class="controllers-buttons"
:href="detailJob.rawPath"
target="_blank"
>
<i
aria-hidden="true"
class="fa fa-file-text-o"
></i>
</a>
<scroll-button
direction="up"
:disabled="isScrolledToTop"
@click="scrollUp"
/>
<scroll-button
direction="down"
:disabled="isScrolledToBottom"
@click="scrollDown"
/>
</div>
</div>
<pre
class="build-trace mb-0 h-100"
ref="buildTrace"
@scroll="scrollBuildLog"
>
<code
class="bash"
v-html="jobOutput"
>
</code>
<div
v-show="detailJob.isLoading"
class="build-loader-animation"
>
</div>
</pre>
</div>
</template>
<script>
import Icon from '../../../../vue_shared/components/icon.vue';
import CiIcon from '../../../../vue_shared/components/ci_icon.vue';
export default {
components: {
Icon,
CiIcon,
},
props: {
job: {
type: Object,
required: true,
},
},
computed: {
jobId() {
return `#${this.job.id}`;
},
},
};
</script>
<template>
<div class="d-flex align-items-center">
<ci-icon
class="d-flex"
:status="job.status"
:borderless="true"
:size="24"
/>
<span class="prepend-left-8">
{{ job.name }}
<a
:href="job.path"
target="_blank"
class="ide-external-link"
>
{{ jobId }}
<icon
name="external-link"
:size="12"
/>
</a>
</span>
</div>
</template>
<script>
import { __ } from '../../../../locale';
import Icon from '../../../../vue_shared/components/icon.vue';
import tooltip from '../../../../vue_shared/directives/tooltip';
const directions = {
up: 'up',
down: 'down',
};
export default {
directives: {
tooltip,
},
components: {
Icon,
},
props: {
direction: {
type: String,
required: true,
validator(value) {
return Object.keys(directions).includes(value);
},
},
disabled: {
type: Boolean,
required: true,
},
},
computed: {
tooltipTitle() {
return this.direction === directions.up ? __('Scroll to top') : __('Scroll to bottom');
},
iconName() {
return `scroll_${this.direction}`;
},
},
methods: {
clickedScroll() {
this.$emit('click');
},
},
};
</script>
<template>
<div
v-tooltip
class="controllers-buttons"
data-container="body"
data-placement="top"
:title="tooltipTitle"
>
<button
class="btn-scroll btn-transparent btn-blank"
type="button"
:disabled="disabled"
@click="clickedScroll"
>
<icon
:name="iconName"
/>
</button>
</div>
</template>
<script> <script>
import Icon from '../../../vue_shared/components/icon.vue'; import JobDescription from './detail/description.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
export default { export default {
components: { components: {
Icon, JobDescription,
CiIcon,
}, },
props: { props: {
job: { job: {
...@@ -18,29 +16,29 @@ export default { ...@@ -18,29 +16,29 @@ export default {
return `#${this.job.id}`; return `#${this.job.id}`;
}, },
}, },
methods: {
clickViewLog() {
this.$emit('clickViewLog', this.job);
},
},
}; };
</script> </script>
<template> <template>
<div class="ide-job-item"> <div class="ide-job-item">
<ci-icon <job-description
:status="job.status" class="append-right-default"
:borderless="true" :job="job"
:size="24"
/> />
<span class="prepend-left-8"> <div class="ml-auto align-self-center">
{{ job.name }} <button
<a v-if="job.started"
:href="job.path" type="button"
target="_blank" class="btn btn-default btn-sm"
class="ide-external-link" @click="clickViewLog"
> >
{{ jobId }} {{ __('View log') }}
<icon </button>
name="external-link" </div>
:size="12"
/>
</a>
</span>
</div> </div>
</template> </template>
...@@ -19,7 +19,7 @@ export default { ...@@ -19,7 +19,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']), ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed', 'setDetailJob']),
}, },
}; };
</script> </script>
...@@ -38,6 +38,7 @@ export default { ...@@ -38,6 +38,7 @@ export default {
:stage="stage" :stage="stage"
@fetch="fetchJobs" @fetch="fetchJobs"
@toggleCollapsed="toggleStageCollapsed" @toggleCollapsed="toggleStageCollapsed"
@clickViewLog="setDetailJob"
/> />
</template> </template>
</div> </div>
......
...@@ -48,6 +48,9 @@ export default { ...@@ -48,6 +48,9 @@ export default {
toggleCollapsed() { toggleCollapsed() {
this.$emit('toggleCollapsed', this.stage.id); this.$emit('toggleCollapsed', this.stage.id);
}, },
clickViewLog(job) {
this.$emit('clickViewLog', job);
},
}, },
}; };
</script> </script>
...@@ -101,6 +104,7 @@ export default { ...@@ -101,6 +104,7 @@ export default {
v-for="job in stage.jobs" v-for="job in stage.jobs"
:key="job.id" :key="job.id"
:job="job" :job="job"
@clickViewLog="clickViewLog"
/> />
</template> </template>
</div> </div>
......
<script>
import { mapGetters } from 'vuex';
import Tabs from '../../../vue_shared/components/tabs/tabs';
import Tab from '../../../vue_shared/components/tabs/tab.vue';
import List from './list.vue';
export default {
components: {
Tabs,
Tab,
List,
},
props: {
show: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters('mergeRequests', ['assignedData', 'createdData']),
createdMergeRequestLength() {
return this.createdData.mergeRequests.length;
},
assignedMergeRequestLength() {
return this.assignedData.mergeRequests.length;
},
},
};
</script>
<template>
<div class="dropdown-menu ide-merge-requests-dropdown p-0">
<tabs
v-if="show"
stop-propagation
>
<tab active>
<template slot="title">
{{ __('Created by me') }}
<span class="badge badge-pill">
{{ createdMergeRequestLength }}
</span>
</template>
<list
type="created"
:empty-text="__('You have not created any merge requests')"
/>
</tab>
<tab>
<template slot="title">
{{ __('Assigned to me') }}
<span class="badge badge-pill">
{{ assignedMergeRequestLength }}
</span>
</template>
<list
type="assigned"
:empty-text="__('You do not have any assigned merge requests')"
/>
</tab>
</tabs>
</div>
</template>
<script>
import Icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
item: {
type: Object,
required: true,
},
currentId: {
type: String,
required: true,
},
currentProjectId: {
type: String,
required: true,
},
},
computed: {
isActive() {
return (
this.item.iid === parseInt(this.currentId, 10) &&
this.currentProjectId === this.item.projectPathWithNamespace
);
},
pathWithID() {
return `${this.item.projectPathWithNamespace}!${this.item.iid}`;
},
},
methods: {
clickItem() {
this.$emit('click', this.item);
},
},
};
</script>
<template>
<button
type="button"
class="btn-link d-flex align-items-center"
@click="clickItem"
>
<span class="d-flex append-right-default ide-merge-request-current-icon">
<icon
v-if="isActive"
name="mobile-issue-close"
:size="18"
/>
</span>
<span>
<strong>
{{ item.title }}
</strong>
<span class="ide-merge-request-project-path d-block mt-1">
{{ pathWithID }}
</span>
</span>
</button>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Item from './item.vue';
export default {
components: {
LoadingIcon,
Item,
},
props: {
type: {
type: String,
required: true,
},
emptyText: {
type: String,
required: true,
},
},
data() {
return {
search: '',
};
},
computed: {
...mapGetters('mergeRequests', ['getData']),
...mapState(['currentMergeRequestId', 'currentProjectId']),
data() {
return this.getData(this.type);
},
isLoading() {
return this.data.isLoading;
},
mergeRequests() {
return this.data.mergeRequests;
},
hasMergeRequests() {
return this.mergeRequests.length !== 0;
},
hasNoSearchResults() {
return this.search !== '' && !this.hasMergeRequests;
},
},
watch: {
isLoading: {
handler: 'focusSearch',
},
},
mounted() {
this.loadMergeRequests();
},
methods: {
...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']),
loadMergeRequests() {
this.fetchMergeRequests({ type: this.type, search: this.search });
},
viewMergeRequest(item) {
this.openMergeRequest({
projectPath: item.projectPathWithNamespace,
id: item.iid,
});
},
searchMergeRequests: _.debounce(function debounceSearch() {
this.loadMergeRequests();
}, 250),
focusSearch() {
if (!this.isLoading) {
this.$nextTick(() => {
this.$refs.searchInput.focus();
});
}
},
},
};
</script>
<template>
<div>
<div class="dropdown-input mt-3 pb-3 mb-0 border-bottom">
<input
type="search"
class="dropdown-input-field"
:placeholder="__('Search merge requests')"
v-model="search"
@input="searchMergeRequests"
ref="searchInput"
/>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
></i>
</div>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<loading-icon
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
v-if="isLoading"
size="2"
/>
<ul
v-else
class="mb-3 w-100"
>
<template v-if="hasMergeRequests">
<li
v-for="item in mergeRequests"
:key="item.id"
>
<item
:item="item"
:current-id="currentMergeRequestId"
:current-project-id="currentProjectId"
@click="viewMergeRequest"
/>
</li>
</template>
<li
v-else
class="ide-merge-requests-empty d-flex align-items-center justify-content-center"
>
<template v-if="hasNoSearchResults">
{{ __('No merge requests found') }}
</template>
<template v-else>
{{ emptyText }}
</template>
</li>
</ul>
</div>
</div>
</template>
...@@ -4,6 +4,7 @@ import tooltip from '../../../vue_shared/directives/tooltip'; ...@@ -4,6 +4,7 @@ import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants'; import { rightSidebarViews } from '../../constants';
import PipelinesList from '../pipelines/list.vue'; import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue';
export default { export default {
directives: { directives: {
...@@ -12,9 +13,16 @@ export default { ...@@ -12,9 +13,16 @@ export default {
components: { components: {
Icon, Icon,
PipelinesList, PipelinesList,
JobsDetail,
}, },
computed: { computed: {
...mapState(['rightPane']), ...mapState(['rightPane']),
pipelinesActive() {
return (
this.rightPane === rightSidebarViews.pipelines ||
this.rightPane === rightSidebarViews.jobsDetail
);
},
}, },
methods: { methods: {
...mapActions(['setRightPane']), ...mapActions(['setRightPane']),
...@@ -48,7 +56,7 @@ export default { ...@@ -48,7 +56,7 @@ export default {
:title="__('Pipelines')" :title="__('Pipelines')"
class="ide-sidebar-link is-right" class="ide-sidebar-link is-right"
:class="{ :class="{
active: rightPane === $options.rightSidebarViews.pipelines active: pipelinesActive
}" }"
type="button" type="button"
@click="clickTab($event, $options.rightSidebarViews.pipelines)" @click="clickTab($event, $options.rightSidebarViews.pipelines)"
......
...@@ -23,4 +23,5 @@ export const viewerTypes = { ...@@ -23,4 +23,5 @@ export const viewerTypes = {
export const rightSidebarViews = { export const rightSidebarViews = {
pipelines: 'pipelines-list', pipelines: 'pipelines-list',
jobsDetail: 'jobs-detail',
}; };
...@@ -17,9 +17,7 @@ export const getMergeRequestData = ( ...@@ -17,9 +17,7 @@ export const getMergeRequestData = (
mergeRequestId, mergeRequestId,
mergeRequest: data, mergeRequest: data,
}); });
if (!state.currentMergeRequestId) { commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
}
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
......
...@@ -13,8 +13,7 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force ...@@ -13,8 +13,7 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force
.then(data => { .then(data => {
commit(types.TOGGLE_LOADING, { entry: state }); commit(types.TOGGLE_LOADING, { entry: 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}`);
commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
......
import { __ } from '../../../../locale'; import { __ } from '../../../../locale';
import Api from '../../../../api'; import Api from '../../../../api';
import flash from '../../../../flash'; import flash from '../../../../flash';
import router from '../../../ide_router';
import { scopes } from './constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
import * as rootTypes from '../../mutation_types';
export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS); export const requestMergeRequests = ({ commit }, type) =>
export const receiveMergeRequestsError = ({ commit }) => { commit(types.REQUEST_MERGE_REQUESTS, type);
export const receiveMergeRequestsError = ({ commit }, type) => {
flash(__('Error loading merge requests.')); flash(__('Error loading merge requests.'));
commit(types.RECEIVE_MERGE_REQUESTS_ERROR); commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type);
}; };
export const receiveMergeRequestsSuccess = ({ commit }, data) => export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) =>
commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data); commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data });
export const fetchMergeRequests = ({ dispatch, state: { scope, state } }, search = '') => { export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => {
dispatch('requestMergeRequests'); const scope = scopes[type];
dispatch('resetMergeRequests'); dispatch('requestMergeRequests', type);
dispatch('resetMergeRequests', type);
Api.mergeRequests({ scope, state, search }) Api.mergeRequests({ scope, state, search })
.then(({ data }) => dispatch('receiveMergeRequestsSuccess', data)) .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data }))
.catch(() => dispatch('receiveMergeRequestsError')); .catch(() => dispatch('receiveMergeRequestsError', type));
}; };
export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS); export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type);
export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => {
commit(rootTypes.CLEAR_PROJECTS, null, { root: true });
commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true });
commit(rootTypes.RESET_OPEN_FILES, null, { root: true });
dispatch('pipelines/stopPipelinePolling', null, { root: true });
dispatch('pipelines/clearEtagPoll', null, { root: true });
dispatch('pipelines/resetLatestPipeline', null, { root: true });
dispatch('setCurrentBranchId', '', { root: true });
router.push(`/project/${projectPath}/merge_requests/${id}`);
};
export default () => {}; export default () => {};
export const scopes = { export const scopes = {
assignedToMe: 'assigned-to-me', assigned: 'assigned-to-me',
createdByMe: 'created-by-me', created: 'created-by-me',
}; };
export const states = { export const states = {
......
export const getData = state => type => state[type];
export const assignedData = state => state.assigned;
export const createdData = state => state.created;
import state from './state'; import state from './state';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
export default { export default {
...@@ -7,4 +8,5 @@ export default { ...@@ -7,4 +8,5 @@ export default {
state: state(), state: state(),
actions, actions,
mutations, mutations,
getters,
}; };
...@@ -2,15 +2,15 @@ ...@@ -2,15 +2,15 @@
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.REQUEST_MERGE_REQUESTS](state) { [types.REQUEST_MERGE_REQUESTS](state, type) {
state.isLoading = true; state[type].isLoading = true;
}, },
[types.RECEIVE_MERGE_REQUESTS_ERROR](state) { [types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) {
state.isLoading = false; state[type].isLoading = false;
}, },
[types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) { [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) {
state.isLoading = false; state[type].isLoading = false;
state.mergeRequests = data.map(mergeRequest => ({ state[type].mergeRequests = data.map(mergeRequest => ({
id: mergeRequest.id, id: mergeRequest.id,
iid: mergeRequest.iid, iid: mergeRequest.iid,
title: mergeRequest.title, title: mergeRequest.title,
...@@ -20,7 +20,7 @@ export default { ...@@ -20,7 +20,7 @@ export default {
.replace(`/merge_requests/${mergeRequest.iid}`, ''), .replace(`/merge_requests/${mergeRequest.iid}`, ''),
})); }));
}, },
[types.RESET_MERGE_REQUESTS](state) { [types.RESET_MERGE_REQUESTS](state, type) {
state.mergeRequests = []; state[type].mergeRequests = [];
}, },
}; };
import { scopes, states } from './constants'; import { states } from './constants';
export default () => ({ export default () => ({
isLoading: false, created: {
mergeRequests: [], isLoading: false,
scope: scopes.assignedToMe, mergeRequests: [],
},
assigned: {
isLoading: false,
mergeRequests: [],
},
state: states.opened, state: states.opened,
}); });
...@@ -4,6 +4,7 @@ import { __ } from '../../../../locale'; ...@@ -4,6 +4,7 @@ import { __ } from '../../../../locale';
import flash from '../../../../flash'; import flash from '../../../../flash';
import Poll from '../../../../lib/utils/poll'; import Poll from '../../../../lib/utils/poll';
import service from '../../../services'; import service from '../../../services';
import { rightSidebarViews } from '../../../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
let eTagPoll; let eTagPoll;
...@@ -77,4 +78,31 @@ export const fetchJobs = ({ dispatch }, stage) => { ...@@ -77,4 +78,31 @@ export const fetchJobs = ({ dispatch }, stage) => {
export const toggleStageCollapsed = ({ commit }, stageId) => export const toggleStageCollapsed = ({ commit }, stageId) =>
commit(types.TOGGLE_STAGE_COLLAPSE, stageId); commit(types.TOGGLE_STAGE_COLLAPSE, stageId);
export const setDetailJob = ({ commit, dispatch }, job) => {
commit(types.SET_DETAIL_JOB, job);
dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, {
root: true,
});
};
export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE);
export const receiveJobTraceError = ({ commit }) => {
flash(__('Error fetching job trace'));
commit(types.RECEIVE_JOB_TRACE_ERROR);
};
export const receiveJobTraceSuccess = ({ commit }, data) =>
commit(types.RECEIVE_JOB_TRACE_SUCCESS, data);
export const fetchJobTrace = ({ dispatch, state }) => {
dispatch('requestJobTrace');
return axios
.get(`${state.detailJob.path}/trace`, { params: { format: 'json' } })
.then(({ data }) => dispatch('receiveJobTraceSuccess', data))
.catch(() => dispatch('receiveJobTraceError'));
};
export const resetLatestPipeline = ({ commit }) =>
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null);
export default () => {}; export default () => {};
...@@ -7,3 +7,9 @@ export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; ...@@ -7,3 +7,9 @@ export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE'; export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE';
export const SET_DETAIL_JOB = 'SET_DETAIL_JOB';
export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE';
export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR';
export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS';
...@@ -63,4 +63,17 @@ export default { ...@@ -63,4 +63,17 @@ export default {
isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed, isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed,
})); }));
}, },
[types.SET_DETAIL_JOB](state, job) {
state.detailJob = { ...job };
},
[types.REQUEST_JOB_TRACE](state) {
state.detailJob.isLoading = true;
},
[types.RECEIVE_JOB_TRACE_ERROR](state) {
state.detailJob.isLoading = false;
},
[types.RECEIVE_JOB_TRACE_SUCCESS](state, data) {
state.detailJob.isLoading = false;
state.detailJob.output = data.html;
},
}; };
...@@ -3,4 +3,5 @@ export default () => ({ ...@@ -3,4 +3,5 @@ export default () => ({
isLoadingJobs: false, isLoadingJobs: false,
latestPipeline: null, latestPipeline: null,
stages: [], stages: [],
detailJob: null,
}); });
...@@ -4,4 +4,8 @@ export const normalizeJob = job => ({ ...@@ -4,4 +4,8 @@ export const normalizeJob = job => ({
name: job.name, name: job.name,
status: job.status, status: job.status,
path: job.build_path, path: job.build_path,
rawPath: `${job.build_path}/raw`,
started: job.started,
output: '',
isLoading: false,
}); });
...@@ -68,3 +68,6 @@ export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; ...@@ -68,3 +68,6 @@ export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
export const SET_RIGHT_PANE = 'SET_RIGHT_PANE'; export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
export const CLEAR_PROJECTS = 'CLEAR_PROJECTS';
export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
...@@ -157,6 +157,12 @@ export default { ...@@ -157,6 +157,12 @@ export default {
[types.SET_LINKS](state, links) { [types.SET_LINKS](state, links) {
Object.assign(state, { links }); Object.assign(state, { links });
}, },
[types.CLEAR_PROJECTS](state) {
Object.assign(state, { projects: {}, trees: {} });
},
[types.RESET_OPEN_FILES](state) {
Object.assign(state, { openFiles: [] });
},
...projectMutations, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
...fileMutations, ...fileMutations,
......
...@@ -58,7 +58,7 @@ class ImporterStatus { ...@@ -58,7 +58,7 @@ class ImporterStatus {
job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`); job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
$('table.import-jobs tbody').prepend(job); $('table.import-jobs tbody').prepend(job);
job.addClass('active'); job.addClass('table-active');
const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing'); const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
job.find('.import-actions').html(sprintf( job.find('.import-actions').html(sprintf(
_.escape(__('%{loadingIcon} Started')), { _.escape(__('%{loadingIcon} Started')), {
...@@ -67,7 +67,15 @@ class ImporterStatus { ...@@ -67,7 +67,15 @@ class ImporterStatus {
false, false,
)); ));
}) })
.catch(() => flash(__('An error occurred while importing project'))); .catch((error) => {
let details = error;
if (error.response && error.response.data && error.response.data.errors) {
details = error.response.data.errors;
}
flash(__(`An error occurred while importing project: ${details}`));
});
} }
autoUpdate() { autoUpdate() {
...@@ -81,7 +89,7 @@ class ImporterStatus { ...@@ -81,7 +89,7 @@ class ImporterStatus {
switch (job.import_status) { switch (job.import_status) {
case 'finished': case 'finished':
jobItem.removeClass('active').addClass('success'); jobItem.removeClass('table-active').addClass('table-success');
statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`); statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`);
break; break;
case 'scheduled': case 'scheduled':
......
import $ from 'jquery'; import $ from 'jquery';
import stickyMonitor from './lib/utils/sticky'; import { stickyMonitor } from './lib/utils/sticky';
export default (stickyTop) => { export default (stickyTop) => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
......
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import StickyFill from 'stickyfilljs'; import { polyfillSticky } from './lib/utils/sticky';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility'; import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints'; import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils'; import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils';
import { isScrolledToBottom, scrollDown } from './lib/utils/scroll_utils';
import LogOutputBehaviours from './lib/utils/logoutput_behaviours';
export default class Job { export default class Job extends LogOutputBehaviours {
constructor(options) { constructor(options) {
super();
this.timeout = null; this.timeout = null;
this.state = null; this.state = null;
this.fetchingStatusFavicon = false; this.fetchingStatusFavicon = false;
...@@ -29,10 +32,6 @@ export default class Job { ...@@ -29,10 +32,6 @@ export default class Job {
this.$buildTraceOutput = $('.js-build-output'); this.$buildTraceOutput = $('.js-build-output');
this.$topBar = $('.js-top-bar'); this.$topBar = $('.js-top-bar');
// Scroll controllers
this.$scrollTopBtn = $('.js-scroll-up');
this.$scrollBottomBtn = $('.js-scroll-down');
clearTimeout(this.timeout); clearTimeout(this.timeout);
this.initSidebar(); this.initSidebar();
...@@ -48,23 +47,14 @@ export default class Job { ...@@ -48,23 +47,14 @@ export default class Job {
.off('click', '.stage-item') .off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown); .on('click', '.stage-item', this.updateDropdown);
// add event listeners to the scroll buttons
this.$scrollTopBtn
.off('click')
.on('click', this.scrollToTop.bind(this));
this.$scrollBottomBtn
.off('click')
.on('click', this.scrollToBottom.bind(this));
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window this.$window
.off('scroll') .off('scroll')
.on('scroll', () => { .on('scroll', () => {
if (!this.isScrolledToBottom()) { if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false); this.toggleScrollAnimation(false);
} else if (this.isScrolledToBottom() && !this.isLogComplete) { } else if (isScrolledToBottom() && !this.isLogComplete) {
this.toggleScrollAnimation(true); this.toggleScrollAnimation(true);
} }
this.scrollThrottled(); this.scrollThrottled();
...@@ -80,70 +70,11 @@ export default class Job { ...@@ -80,70 +70,11 @@ export default class Job {
} }
initAffixTopArea() { initAffixTopArea() {
/** polyfillSticky(this.$topBar);
If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not
then we use a polyfill
*/
if (this.$topBar.css('position') !== 'static') return;
StickyFill.add(this.$topBar);
}
// eslint-disable-next-line class-methods-use-this
canScroll() {
return $(document).height() > $(window).height();
}
toggleScroll() {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
if (this.canScroll()) {
if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) {
// User is in the middle of the log
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (currentPosition === 0) {
// User is at Top of Log
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (this.isScrolledToBottom()) {
// User is at the bottom of the build log.
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
} else {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
}
// eslint-disable-next-line class-methods-use-this
isScrolledToBottom() {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
return scrollHeight - currentPosition === windowHeight;
}
// eslint-disable-next-line class-methods-use-this
scrollDown() {
const $document = $(document);
$document.scrollTop($document.height());
} }
scrollToBottom() { scrollToBottom() {
this.scrollDown(); scrollDown();
this.hasBeenScrolled = true; this.hasBeenScrolled = true;
this.toggleScroll(); this.toggleScroll();
} }
...@@ -154,12 +85,6 @@ export default class Job { ...@@ -154,12 +85,6 @@ export default class Job {
this.toggleScroll(); this.toggleScroll();
} }
// eslint-disable-next-line class-methods-use-this
toggleDisableButton($button, disable) {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
}
toggleScrollAnimation(toggle) { toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle); this.$scrollBottomBtn.toggleClass('animate', toggle);
} }
...@@ -191,7 +116,7 @@ export default class Job { ...@@ -191,7 +116,7 @@ export default class Job {
this.state = log.state; this.state = log.state;
} }
this.isScrollInBottom = this.isScrolledToBottom(); this.isScrollInBottom = isScrolledToBottom();
if (log.append) { if (log.append) {
this.$buildTraceOutput.append(log.html); this.$buildTraceOutput.append(log.html);
...@@ -231,7 +156,7 @@ export default class Job { ...@@ -231,7 +156,7 @@ export default class Job {
}) })
.then(() => { .then(() => {
if (this.isScrollInBottom) { if (this.isScrollInBottom) {
this.scrollDown(); scrollDown();
} }
}) })
.then(() => this.toggleScroll()); .then(() => this.toggleScroll());
......
...@@ -42,6 +42,9 @@ export default { ...@@ -42,6 +42,9 @@ export default {
jobStarted() { jobStarted() {
return !this.job.started === false; return !this.job.started === false;
}, },
headerTime() {
return this.jobStarted ? this.job.started : this.job.created_at;
},
}, },
watch: { watch: {
job() { job() {
...@@ -73,7 +76,7 @@ export default { ...@@ -73,7 +76,7 @@ export default {
:status="status" :status="status"
item-name="Job" item-name="Job"
:item-id="job.id" :item-id="job.id"
:time="job.created_at" :time="headerTime"
:user="job.user" :user="job.user"
:actions="actions" :actions="actions"
:has-sidebar-button="true" :has-sidebar-button="true"
......
...@@ -426,7 +426,7 @@ export default class LabelsSelect { ...@@ -426,7 +426,7 @@ export default class LabelsSelect {
const tpl = _.template([ const tpl = _.template([
'<% _.each(labels, function(label){ %>', '<% _.each(labels, function(label){ %>',
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
'<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
'<%- label.title %>', '<%- label.title %>',
'</span>', '</span>',
'</a>', '</a>',
......
import $ from 'jquery';
import { canScroll, isScrolledToBottom, toggleDisableButton } from './scroll_utils';
export default class LogOutputBehaviours {
constructor() {
// Scroll buttons
this.$scrollTopBtn = $('.js-scroll-up');
this.$scrollBottomBtn = $('.js-scroll-down');
this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this));
this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this));
}
toggleScroll() {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
if (canScroll()) {
if (currentPosition > 0 && scrollHeight - currentPosition !== windowHeight) {
// User is in the middle of the log
toggleDisableButton(this.$scrollTopBtn, false);
toggleDisableButton(this.$scrollBottomBtn, false);
} else if (currentPosition === 0) {
// User is at Top of Log
toggleDisableButton(this.$scrollTopBtn, true);
toggleDisableButton(this.$scrollBottomBtn, false);
} else if (isScrolledToBottom()) {
// User is at the bottom of the build log.
toggleDisableButton(this.$scrollTopBtn, false);
toggleDisableButton(this.$scrollBottomBtn, true);
}
} else {
toggleDisableButton(this.$scrollTopBtn, true);
toggleDisableButton(this.$scrollBottomBtn, true);
}
}
toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle);
}
}
import $ from 'jquery';
export const canScroll = () => $(document).height() > $(window).height();
/**
* Checks if the entire page is scrolled down all the way to the bottom
*/
export const isScrolledToBottom = () => {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
return scrollHeight - currentPosition === windowHeight;
};
export const scrollDown = () => {
const $document = $(document);
$document.scrollTop($document.height());
};
export const toggleDisableButton = ($button, disable) => {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
};
export default {};
import StickyFill from 'stickyfilljs';
export const createPlaceholder = () => { export const createPlaceholder = () => {
const placeholder = document.createElement('div'); const placeholder = document.createElement('div');
placeholder.classList.add('sticky-placeholder'); placeholder.classList.add('sticky-placeholder');
...@@ -28,7 +30,16 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { ...@@ -28,7 +30,16 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
} }
}; };
export default (el, stickyTop, insertPlaceholder = true) => { /**
* Create a listener that will toggle a 'is-stuck' class, based on the current scroll position.
*
* - If the current environment does not support `position: sticky`, do nothing.
*
* @param {HTMLElement} el The `position: sticky` element.
* @param {Number} stickyTop Used to determine when an element is stuck.
* @param {Boolean} insertPlaceholder Should a placeholder element be created when element is stuck?
*/
export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => {
if (!el) return; if (!el) return;
if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return; if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return;
...@@ -37,3 +48,13 @@ export default (el, stickyTop, insertPlaceholder = true) => { ...@@ -37,3 +48,13 @@ export default (el, stickyTop, insertPlaceholder = true) => {
passive: true, passive: true,
}); });
}; };
/**
* Polyfill the `position: sticky` behavior.
*
* - If the current environment supports `position: sticky`, do nothing.
* - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement.
*/
export const polyfillSticky = (el) => {
StickyFill.add(el);
};
...@@ -174,7 +174,10 @@ export default { ...@@ -174,7 +174,10 @@ export default {
:tags-path="tagsPath" :tags-path="tagsPath"
:show-legend="showLegend" :show-legend="showLegend"
:small-graph="forceSmallGraph" :small-graph="forceSmallGraph"
/> >
<!-- EE content -->
{{ null }}
</graph>
</graph-group> </graph-group>
</div> </div>
<empty-state <empty-state
......
...@@ -232,9 +232,14 @@ export default { ...@@ -232,9 +232,14 @@ export default {
@mouseover="showFlagContent = true" @mouseover="showFlagContent = true"
@mouseleave="showFlagContent = false" @mouseleave="showFlagContent = false"
> >
<h5 class="text-center graph-title"> <div class="prometheus-graph-header">
{{ graphData.title }} <h5 class="prometheus-graph-title">
</h5> {{ graphData.title }}
</h5>
<div class="prometheus-graph-widgets">
<slot></slot>
</div>
</div>
<div <div
class="prometheus-svg-container" class="prometheus-svg-container"
:style="paddingBottomRootSvg" :style="paddingBottomRootSvg"
......
...@@ -14,6 +14,7 @@ export const EPIC_NOTEABLE_TYPE = 'epic'; ...@@ -14,6 +14,7 @@ export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request'; export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description';
export const NOTEABLE_TYPE_MAPPING = { export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE, Issue: ISSUE_NOTEABLE_TYPE,
......
import { n__, s__, sprintf } from '~/locale';
import { DESCRIPTION_TYPE } from '../constants';
/**
* Changes the description from a note, returns 'changed the description n number of times'
*/
export const changeDescriptionNote = (note, descriptionChangedTimes, timeDifferenceMinutes) => {
const descriptionNote = Object.assign({}, note);
descriptionNote.note_html = sprintf(
s__(`MergeRequest|
%{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}`),
{
paragraphStart: '<p dir="auto">',
paragraphEnd: '</p>',
descriptionChangedTimes,
timeDifferenceMinutes: n__('within %d minute ', 'within %d minutes ', timeDifferenceMinutes),
},
false,
);
descriptionNote.times_updated = descriptionChangedTimes;
return descriptionNote;
};
/**
* Checks the time difference between two notes from their 'created_at' dates
* returns an integer
*/
export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => {
const descriptionNoteBegin = new Date(noteBeggining.created_at);
const descriptionNoteEnd = new Date(noteEnd.created_at);
const timeDifferenceMinutes = (descriptionNoteEnd - descriptionNoteBegin) / 1000 / 60;
return Math.ceil(timeDifferenceMinutes);
};
/**
* Checks if a note is a system note and if the content is description
*
* @param {Object} note
* @returns {Boolean}
*/
export const isDescriptionSystemNote = note => note.system && note.note === DESCRIPTION_TYPE;
/**
* Collapses the system notes of a description type, e.g. Changed the description, n minutes ago
* the notes will collapse as long as they happen no more than 10 minutes away from each away
* in between the notes can be anything, another type of system note
* (such as 'changed the weight') or a comment.
*
* @param {Array} notes
* @returns {Array}
*/
export const collapseSystemNotes = notes => {
let lastDescriptionSystemNote = null;
let lastDescriptionSystemNoteIndex = -1;
let descriptionChangedTimes = 1;
return notes.slice(0).reduce((acc, currentNote) => {
const note = currentNote.notes[0];
if (isDescriptionSystemNote(note)) {
// is it the first one?
if (!lastDescriptionSystemNote) {
lastDescriptionSystemNote = note;
lastDescriptionSystemNoteIndex = acc.length;
} else if (lastDescriptionSystemNote) {
const timeDifferenceMinutes = getTimeDifferenceMinutes(
lastDescriptionSystemNote,
note,
);
// are they less than 10 minutes appart?
if (timeDifferenceMinutes > 10) {
// reset counter
descriptionChangedTimes = 1;
// update the previous system note
lastDescriptionSystemNote = note;
lastDescriptionSystemNoteIndex = acc.length;
} else {
// increase counter
descriptionChangedTimes += 1;
// delete the previous one
acc.splice(lastDescriptionSystemNoteIndex, 1);
// replace the text of the current system note with the collapsed note.
currentNote.notes.splice(
0,
1,
changeDescriptionNote(note, descriptionChangedTimes, timeDifferenceMinutes),
);
// update the previous system note index
lastDescriptionSystemNoteIndex = acc.length;
}
}
}
acc.push(currentNote);
return acc;
}, []);
};
// for babel-rewire
export default {};
import _ from 'underscore'; import _ from 'underscore';
import { collapseSystemNotes } from './collapse_utils';
export const notes = state => collapseSystemNotes(state.notes);
export const notes = state => state.notes;
export const targetNoteHash = state => state.targetNoteHash; export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData; export const getNotesData = state => state.notesData;
......
...@@ -56,6 +56,7 @@ export default { ...@@ -56,6 +56,7 @@ export default {
<gl-modal <gl-modal
:id="`modal-peek-${metric}-details`" :id="`modal-peek-${metric}-details`"
:header-title-text="header" :header-title-text="header"
modal-size="lg"
class="performance-bar-modal" class="performance-bar-modal"
> >
<table <table
...@@ -70,7 +71,7 @@ export default { ...@@ -70,7 +71,7 @@ export default {
<td <td
v-for="key in keys" v-for="key in keys"
:key="key" :key="key"
class="break-word" class="break-word all-words"
> >
{{ item[key] }} {{ item[key] }}
</td> </td>
......
...@@ -265,10 +265,10 @@ export default { ...@@ -265,10 +265,10 @@ export default {
/> />
<section <section
v-if="mr.maintainerEditAllowed" v-if="mr.allowCollaboration"
class="mr-info-list mr-links" class="mr-info-list mr-links"
> >
{{ s__("mrWidget|Allows edits from maintainers") }} {{ s__("mrWidget|Allows commits from members who can merge to the target branch") }}
</section> </section>
<mr-widget-related-links <mr-widget-related-links
......
...@@ -83,7 +83,7 @@ export default class MergeRequestStore { ...@@ -83,7 +83,7 @@ export default class MergeRequestStore {
this.canBeMerged = data.can_be_merged || false; this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false; this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing; this.mergeOngoing = data.merge_ongoing;
this.maintainerEditAllowed = data.allow_maintainer_to_push; this.allowCollaboration = data.allow_collaboration;
// Cherry-pick and Revert actions related // Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
......
<script> <script>
const buttonVariants = ['danger', 'primary', 'success', 'warning']; const buttonVariants = ['danger', 'primary', 'success', 'warning'];
const sizeVariants = ['sm', 'md', 'lg'];
export default { export default {
name: 'GlModal', name: 'GlModal',
props: { props: {
id: { id: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
}, },
modalSize: {
type: String,
required: false,
default: 'md',
validator: value => sizeVariants.includes(value),
},
headerTitleText: { headerTitleText: {
type: String, type: String,
required: false, required: false,
...@@ -27,7 +33,11 @@ export default { ...@@ -27,7 +33,11 @@ export default {
default: '', default: '',
}, },
}, },
computed: {
modalSizeClass() {
return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`;
},
},
methods: { methods: {
emitCancel(event) { emitCancel(event) {
this.$emit('cancel', event); this.$emit('cancel', event);
...@@ -48,6 +58,7 @@ export default { ...@@ -48,6 +58,7 @@ export default {
> >
<div <div
class="modal-dialog" class="modal-dialog"
:class="modalSizeClass"
role="document" role="document"
> >
<div class="modal-content"> <div class="modal-content">
......
...@@ -35,7 +35,12 @@ export default { ...@@ -35,7 +35,12 @@ export default {
</script> </script>
<template> <template>
<div class="hide-collapsed value issuable-show-labels js-value"> <div
class="hide-collapsed value issuable-show-labels js-value"
:class="{
'has-labels':!isEmpty,
}"
>
<span <span
v-if="isEmpty" v-if="isEmpty"
class="text-secondary" class="text-secondary"
...@@ -50,7 +55,7 @@ export default { ...@@ -50,7 +55,7 @@ export default {
> >
<span <span
v-tooltip v-tooltip
class="label color-label" class="badge color-label"
data-placement="bottom" data-placement="bottom"
data-container="body" data-container="body"
:style="labelStyle(label)" :style="labelStyle(label)"
......
...@@ -124,15 +124,18 @@ ...@@ -124,15 +124,18 @@
break; break;
} }
}, },
hideOnSmallScreen(item) {
return !item.first && !item.last && !item.next && !item.prev && !item.active;
},
}, },
}; };
</script> </script>
<template> <template>
<div <div
v-if="showPagination" v-if="showPagination"
class="gl-pagination" class="gl-pagination prepend-top-default"
> >
<ul class="pagination clearfix"> <ul class="pagination justify-content-center">
<li <li
v-for="(item, index) in getItems" v-for="(item, index) in getItems"
:key="index" :key="index"
...@@ -142,12 +145,17 @@ ...@@ -142,12 +145,17 @@
'js-next-button': item.next, 'js-next-button': item.next,
'js-last-button': item.last, 'js-last-button': item.last,
'js-first-button': item.first, 'js-first-button': item.first,
'd-none d-md-block': hideOnSmallScreen(item),
separator: item.separator, separator: item.separator,
active: item.active, active: item.active,
disabled: item.disabled disabled: item.disabled || item.separator
}" }"
class="page-item"
> >
<a @click.prevent="changePage(item.title, item.disabled)"> <a
@click.prevent="changePage(item.title, item.disabled)"
class="page-link"
>
{{ item.title }} {{ item.title }}
</a> </a>
</li> </li>
......
...@@ -26,6 +26,11 @@ export default { ...@@ -26,6 +26,11 @@ export default {
created() { created() {
this.isTab = true; this.isTab = true;
}, },
updated() {
if (this.$parent) {
this.$parent.$forceUpdate();
}
},
}; };
</script> </script>
......
export default { export default {
props: {
stopPropagation: {
type: Boolean,
required: false,
default: false,
},
},
data() { data() {
return { return {
currentIndex: 0, currentIndex: 0,
...@@ -13,7 +20,12 @@ export default { ...@@ -13,7 +20,12 @@ export default {
this.tabs = this.$children.filter(child => child.isTab); this.tabs = this.$children.filter(child => child.isTab);
this.currentIndex = this.tabs.findIndex(tab => tab.localActive); this.currentIndex = this.tabs.findIndex(tab => tab.localActive);
}, },
setTab(index) { setTab(e, index) {
if (this.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
this.tabs[this.currentIndex].localActive = false; this.tabs[this.currentIndex].localActive = false;
this.tabs[index].localActive = true; this.tabs[index].localActive = true;
...@@ -36,7 +48,7 @@ export default { ...@@ -36,7 +48,7 @@ export default {
href: '#', href: '#',
}, },
on: { on: {
click: () => this.setTab(i), click: e => this.setTab(e, i),
}, },
}, },
tab.$slots.title || tab.title, tab.$slots.title || tab.title,
......
...@@ -24,16 +24,60 @@ html { ...@@ -24,16 +24,60 @@ html {
font-size: 14px; font-size: 14px;
} }
legend {
border-bottom: 1px solid $border-color;
margin-bottom: 20px;
}
button, button,
html [type="button"], html [type="button"],
[type="reset"], [type="reset"],
[type="submit"] { [type="submit"],
[role="button"] {
// Override bootstrap reboot // Override bootstrap reboot
-webkit-appearance: inherit; -webkit-appearance: inherit;
cursor: pointer;
} }
[role="button"] { h1,
cursor: pointer; h2,
h3,
h4,
h5,
h6 {
color: $gl-text-color;
font-weight: 600;
}
h1,
.h1,
h2,
.h2,
h3,
.h3 {
margin-top: 20px;
margin-bottom: 10px;
}
h4,
.h4,
h5,
.h5,
h6,
.h6 {
margin-top: 10px;
margin-bottom: 10px;
}
h5,
.h5 {
font-size: $gl-font-size;
}
input[type="file"] {
// Bootstrap 4 file input height is taller by default
// which makes them look ugly
line-height: 1;
} }
b, b,
...@@ -53,10 +97,29 @@ a { ...@@ -53,10 +97,29 @@ a {
} }
} }
kbd {
display: inline-block;
}
code { code {
padding: 2px 4px; padding: 2px 4px;
color: $red-600;
background-color: $red-100; background-color: $red-100;
border-radius: 3px; border-radius: 3px;
.code & {
background-color: inherit;
padding: unset;
}
.build-trace & {
background-color: inherit;
padding: inherit;
}
}
.code {
padding: 9.5px;
} }
table { table {
...@@ -87,6 +150,16 @@ table { ...@@ -87,6 +150,16 @@ table {
color: $gl-text-color-secondary !important; color: $gl-text-color-secondary !important;
} }
.bg-success,
.bg-primary,
.bg-info,
.bg-danger,
.bg-warning {
.card-header {
color: $white-light;
}
}
// Polyfill deprecated selectors // Polyfill deprecated selectors
.hidden { .hidden {
...@@ -161,8 +234,13 @@ table { ...@@ -161,8 +234,13 @@ table {
} }
.nav-tabs { .nav-tabs {
// Override bootstrap's default border
border-bottom: 0;
.nav-link { .nav-link {
border: 0; border-top: 0;
border-left: 0;
border-right: 0;
} }
.nav-item { .nav-item {
......
...@@ -448,6 +448,10 @@ img.emoji { ...@@ -448,6 +448,10 @@ img.emoji {
.break-word { .break-word {
word-wrap: break-word; word-wrap: break-word;
&.all-words {
word-break: break-word;
}
} }
/** COMMON CLASSES **/ /** COMMON CLASSES **/
......
...@@ -26,19 +26,25 @@ ...@@ -26,19 +26,25 @@
margin-right: 2px; margin-right: 2px;
width: $contextual-sidebar-width; width: $contextual-sidebar-width;
a { > a,
> button {
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
display: flex; display: flex;
width: 100%;
align-items: center; align-items: center;
padding: 10px 16px 10px 10px; padding: 10px 16px 10px 10px;
color: $gl-text-color; color: $gl-text-color;
} background-color: transparent;
border: 0;
text-align: left;
&:hover, &:hover,
a:hover { &:focus {
background-color: $link-hover-background; background-color: $link-hover-background;
color: $gl-text-color; color: $gl-text-color;
outline: 0;
}
} }
.avatar-container { .avatar-container {
......
...@@ -35,6 +35,12 @@ ...@@ -35,6 +35,12 @@
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
width: 100%; width: 100%;
} }
&.projects-dropdown-menu {
padding: 0;
overflow-y: initial;
max-height: initial;
}
} }
.dropdown-toggle, .dropdown-toggle,
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
} }
&.active > a, &.active > a,
&.dropdown.open > a { &.dropdown.show > a {
color: $color-900; color: $color-900;
background-color: $color-alternate; background-color: $color-alternate;
} }
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
} }
&.active > a, &.active > a,
&.dropdown.open > a { &.dropdown.show > a {
color: $color-900; color: $color-900;
background-color: $color-alternate; background-color: $color-alternate;
......
...@@ -297,12 +297,6 @@ ...@@ -297,12 +297,6 @@
display: flex; display: flex;
margin: 0 0 0 6px; margin: 0 0 0 6px;
.projects-dropdown-menu {
padding: 0;
overflow-y: initial;
max-height: initial;
}
.dropdown-chevron { .dropdown-chevron {
position: relative; position: relative;
top: -1px; top: -1px;
......
...@@ -115,9 +115,3 @@ body { ...@@ -115,9 +115,3 @@ body {
.with-performance-bar .layout-page { .with-performance-bar .layout-page {
margin-top: $header-height + $performance-bar-height; margin-top: $header-height + $performance-bar-height;
} }
.vertical-center {
min-height: 100vh;
display: flex;
align-items: center;
}
.gl-pagination { .gl-pagination {
text-align: center; a {
border-top: 1px solid $border-color; color: inherit;
margin: 0; text-decoration: none;
margin-top: 0;
.pagination {
padding: 0;
margin: 20px 0;
a {
cursor: pointer;
}
.separator,
.separator:hover {
a {
cursor: default;
background-color: $gray-light;
padding: $gl-vert-padding;
}
}
}
.gap,
.gap:hover {
background-color: $gray-light;
padding: $gl-vert-padding;
cursor: default;
}
}
.card > .gl-pagination {
margin: 0;
}
/**
* Extra-small screen pagination.
*/
@media (max-width: 320px) {
.gl-pagination {
.first,
.last {
display: none;
}
.page-item {
display: none;
&.active {
display: inline;
}
}
}
}
/**
* Small screen pagination
*/
@include media-breakpoint-down(xs) {
.gl-pagination {
.pagination li a {
padding: 6px 10px;
}
.page-item {
display: none;
&.active {
display: inline;
}
}
}
}
/**
* Medium screen pagination
*/
@media (min-width: map-get($grid-breakpoints, xs)) and (max-width: map-get($grid-breakpoints, sm)) {
.gl-pagination {
.page-item {
display: none;
&.active,
&.sibling {
display: inline;
}
}
} }
} }
...@@ -11,15 +11,15 @@ ...@@ -11,15 +11,15 @@
padding-top: $gl-padding; padding-top: $gl-padding;
} }
.panel { .card {
.panel-heading { .card-header {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
line-height: $line-height-base; line-height: $line-height-base;
.title { .card-title {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -34,6 +34,8 @@ ...@@ -34,6 +34,8 @@
.navbar-collapse { .navbar-collapse {
padding-right: 0; padding-right: 0;
flex-grow: 0;
flex-basis: auto;
.navbar-nav { .navbar-nav {
margin: 0; margin: 0;
......
...@@ -42,6 +42,10 @@ ...@@ -42,6 +42,10 @@
background: none; background: none;
} }
&:focus {
outline: none;
}
.toggle-icon { .toggle-icon {
position: relative; position: relative;
display: block; display: block;
......
...@@ -114,26 +114,27 @@ ...@@ -114,26 +114,27 @@
font-size: 0.95em; font-size: 0.95em;
} }
blockquote,
.blockquote { .blockquote {
color: $gl-grayish-blue; color: $gl-grayish-blue;
font-size: inherit; font-size: inherit;
padding: 8px 24px; padding: 8px 24px;
margin: 16px 0; margin: 16px 0;
border-left: 3px solid $white-dark; border-left: 3px solid $white-dark;
}
.blockquote:dir(rtl) { &:dir(rtl) {
border-left: 0; border-left: 0;
border-right: 3px solid $white-dark; border-right: 3px solid $white-dark;
} }
.blockquote p { p {
color: $gl-grayish-blue !important; color: $gl-grayish-blue !important;
font-size: inherit; font-size: inherit;
line-height: 1.5; line-height: 1.5;
&:last-child { &:last-child {
margin: 0; margin: 0;
}
} }
} }
......
...@@ -138,6 +138,7 @@ pre { ...@@ -138,6 +138,7 @@ pre {
margin: 0; margin: 0;
} }
blockquote,
.blockquote { .blockquote {
color: $gl-grayish-blue; color: $gl-grayish-blue;
padding: 0 0 0 15px; padding: 0 0 0 15px;
......
...@@ -75,6 +75,7 @@ ...@@ -75,6 +75,7 @@
.top-bar { .top-bar {
height: 35px; height: 35px;
min-height: 35px;
background: $gray-light; background: $gray-light;
border: 1px solid $border-color; border: 1px solid $border-color;
color: $gl-text-color; color: $gl-text-color;
......
...@@ -13,6 +13,10 @@ ...@@ -13,6 +13,10 @@
max-width: 100%; max-width: 100%;
} }
.clusters-error-alert {
width: 100%;
}
.clusters-container { .clusters-container {
.nav-bar-right { .nav-bar-right {
padding: $gl-padding-top $gl-padding; padding: $gl-padding-top $gl-padding;
......
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
} }
.btn-group { .btn-group {
> a { > a {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
...@@ -245,6 +244,7 @@ ...@@ -245,6 +244,7 @@
.prometheus-graph { .prometheus-graph {
flex: 1 0 auto; flex: 1 0 auto;
min-width: 450px; min-width: 450px;
max-width: 100%;
padding: $gl-padding / 2; padding: $gl-padding / 2;
h5 { h5 {
...@@ -256,6 +256,17 @@ ...@@ -256,6 +256,17 @@
} }
} }
.prometheus-graph-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $gl-padding-8;
h5 {
margin: 0;
}
}
.prometheus-graph-cursor { .prometheus-graph-cursor {
position: absolute; position: absolute;
background: $theme-gray-600; background: $theme-gray-600;
......
...@@ -485,6 +485,15 @@ ...@@ -485,6 +485,15 @@
.sidebar-collapsed-user { .sidebar-collapsed-user {
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 10px; margin-bottom: 10px;
.author_link {
padding-left: 0;
.avatar {
position: static;
margin: 0;
}
}
} }
.issuable-header-btn { .issuable-header-btn {
......
...@@ -196,6 +196,10 @@ ...@@ -196,6 +196,10 @@
.prioritized-labels { .prioritized-labels {
margin-bottom: 30px; margin-bottom: 30px;
h5 {
font-size: $gl-font-size;
}
.add-priority { .add-priority {
display: none; display: none;
color: $gray-light; color: $gray-light;
...@@ -210,6 +214,10 @@ ...@@ -210,6 +214,10 @@
} }
.other-labels { .other-labels {
h5 {
font-size: $gl-font-size;
}
.remove-priority { .remove-priority {
display: none; display: none;
} }
......
...@@ -183,7 +183,7 @@ ...@@ -183,7 +183,7 @@
svg { svg {
position: relative; position: relative;
top: -1px; top: -2px;
} }
.ide-file-changed-icon { .ide-file-changed-icon {
...@@ -458,9 +458,9 @@ ...@@ -458,9 +458,9 @@
width: auto; width: auto;
margin-right: 0; margin-right: 0;
a:hover, > a,
a:focus { > button {
text-decoration: none; height: 60px;
} }
} }
...@@ -718,9 +718,17 @@ ...@@ -718,9 +718,17 @@
} }
.ide-new-btn { .ide-new-btn {
.btn {
padding-top: 3px;
padding-bottom: 3px;
}
.dropdown {
display: flex;
}
.dropdown-toggle svg { .dropdown-toggle svg {
margin-top: -2px; top: 0;
margin-bottom: 2px;
} }
.dropdown-menu { .dropdown-menu {
...@@ -877,6 +885,7 @@ ...@@ -877,6 +885,7 @@
border-top: 1px solid transparent; border-top: 1px solid transparent;
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
outline: 0; outline: 0;
cursor: pointer;
svg { svg {
margin: 0 auto; margin: 0 auto;
...@@ -1122,6 +1131,11 @@ ...@@ -1122,6 +1131,11 @@
.avatar { .avatar {
flex: 0 0 40px; flex: 0 0 40px;
} }
.ide-merge-requests-dropdown.dropdown-menu {
width: 385px;
max-height: initial;
}
} }
.ide-sidebar-project-title { .ide-sidebar-project-title {
...@@ -1130,11 +1144,20 @@ ...@@ -1130,11 +1144,20 @@
.sidebar-context-title { .sidebar-context-title {
white-space: nowrap; white-space: nowrap;
} }
.ide-sidebar-branch-title {
min-width: 50px;
}
} }
.ide-external-link { .ide-external-link {
position: relative;
svg { svg {
display: none; display: none;
position: absolute;
top: 2px;
right: -$gl-padding;
} }
&:hover, &:hover,
...@@ -1165,6 +1188,8 @@ ...@@ -1165,6 +1188,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
margin-top: -$grid-size;
margin-bottom: -$grid-size;
.empty-state { .empty-state {
margin-top: auto; margin-top: auto;
...@@ -1181,6 +1206,17 @@ ...@@ -1181,6 +1206,17 @@
margin: 0; margin: 0;
} }
} }
.build-trace,
.top-bar {
margin-left: -$gl-padding;
}
&.build-page .top-bar {
top: 0;
font-size: 12px;
border-top-right-radius: $border-radius-default;
}
} }
.ide-pipeline-list { .ide-pipeline-list {
...@@ -1189,7 +1225,7 @@ ...@@ -1189,7 +1225,7 @@
} }
.ide-pipeline-header { .ide-pipeline-header {
min-height: 50px; min-height: 55px;
padding-left: $gl-padding; padding-left: $gl-padding;
padding-right: $gl-padding; padding-right: $gl-padding;
...@@ -1209,8 +1245,7 @@ ...@@ -1209,8 +1245,7 @@
.ci-status-icon { .ci-status-icon {
display: flex; display: flex;
justify-content: center; justify-content: center;
height: 20px; min-width: 24px;
margin-top: -2px;
overflow: hidden; overflow: hidden;
} }
} }
...@@ -1240,3 +1275,56 @@ ...@@ -1240,3 +1275,56 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.ide-job-header {
min-height: 60px;
}
.ide-merge-requests-dropdown {
.nav-links li {
width: 50%;
padding-left: 0;
padding-right: 0;
a {
text-align: center;
&:not(.active) {
background-color: $gray-light;
}
}
}
.dropdown-input {
padding-left: $gl-padding;
padding-right: $gl-padding;
.fa {
right: 26px;
}
}
.btn-link {
padding-top: $gl-padding;
padding-bottom: $gl-padding;
}
}
.ide-merge-request-current-icon {
min-width: 18px;
}
.ide-merge-requests-empty {
height: 230px;
}
.ide-merge-requests-dropdown-content {
min-height: 230px;
max-height: 470px;
}
.ide-merge-request-project-path {
font-size: 12px;
line-height: 16px;
color: $gl-text-color-secondary;
}
...@@ -102,10 +102,6 @@ ...@@ -102,10 +102,6 @@
.form-text.text-muted { .form-text.text-muted {
margin-top: 0; margin-top: 0;
} }
.label-light {
margin-bottom: 0;
}
} }
.settings-list-icon { .settings-list-icon {
...@@ -174,7 +170,7 @@ ...@@ -174,7 +170,7 @@
.option-description, .option-description,
.option-disabled-reason { .option-disabled-reason {
margin-left: 45px; margin-left: 30px;
color: $project-option-descr-color; color: $project-option-descr-color;
} }
......
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
header, header,
nav, nav,
nav.main-nav,
nav.navbar-collapse, nav.navbar-collapse,
nav.navbar-collapse.collapse, nav.navbar-collapse.collapse,
.nav-sidebar,
.profiler-results, .profiler-results,
.tree-ref-holder, .tree-ref-holder,
.tree-holder .breadcrumb, .tree-holder .breadcrumb,
...@@ -38,7 +38,8 @@ ul.notes-form, ...@@ -38,7 +38,8 @@ ul.notes-form,
.edit-link, .edit-link,
.note-action-button, .note-action-button,
.right-sidebar, .right-sidebar,
.flash-container { .flash-container,
#js-peek {
display: none !important; display: none !important;
} }
......
...@@ -130,12 +130,17 @@ class ApplicationController < ActionController::Base ...@@ -130,12 +130,17 @@ class ApplicationController < ActionController::Base
end end
def access_denied!(message = nil) def access_denied!(message = nil)
# If we display a custom access denied message to the user, we don't want to
# hide existence of the resource, rather tell them they cannot access it using
# the provided message
status = message.present? ? :forbidden : :not_found
respond_to do |format| respond_to do |format|
format.any { head :not_found } format.any { head status }
format.html do format.html do
render "errors/access_denied", render "errors/access_denied",
layout: "errors", layout: "errors",
status: 404, status: status,
locals: { message: message } locals: { message: message }
end end
end end
......
class GraphqlController < ApplicationController
# Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user!
before_action :check_graphql_feature_flag!
def execute
variables = Gitlab::Graphql::Variables.new(params[:variables]).to_h
query = params[:query]
operation_name = params[:operationName]
context = {
current_user: current_user
}
result = GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
end
rescue_from StandardError do |exception|
log_exception(exception)
render_error("Internal server error")
end
rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
render_error(exception.message, status: :unprocessable_entity)
end
private
# Overridden from the ApplicationController to make the response look like
# a GraphQL response. That is nicely picked up in Graphiql.
def render_404
render_error("Not found!", status: :not_found)
end
def render_error(message, status: 500)
error = { errors: [message: message] }
render json: error, status: status
end
def check_graphql_feature_flag!
render_404 unless Feature.enabled?(:graphql)
end
end
...@@ -3,8 +3,12 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -3,8 +3,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
include MembersPresentation include MembersPresentation
include SortingHelper include SortingHelper
def self.admin_not_required_endpoints
%i[index leave request_access]
end
# Authorize # Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access] before_action :authorize_admin_group_member!, except: admin_not_required_endpoints
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access, skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite, :approve_access_request, :leave, :resend_invite,
......
...@@ -76,12 +76,15 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -76,12 +76,15 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestones def milestones
milestones = MilestonesFinder.new(search_params).execute milestones = MilestonesFinder.new(search_params).execute
legacy_milestones = GroupMilestone.build_collection(group, group_projects, params)
@sort = params[:sort] || 'due_date_asc' @sort = params[:sort] || 'due_date_asc'
MilestoneArray.sort(milestones + legacy_milestones, @sort) MilestoneArray.sort(milestones + legacy_milestones, @sort)
end end
def legacy_milestones
GroupMilestone.build_collection(group, group_projects, params)
end
def milestone def milestone
@milestone = @milestone =
if params[:title] if params[:title]
......
...@@ -24,7 +24,9 @@ module Groups ...@@ -24,7 +24,9 @@ module Groups
# Make the `search` param consistent for the frontend, # Make the `search` param consistent for the frontend,
# which will be using `filter`. # which will be using `filter`.
params[:search] ||= params[:filter] if params[:filter] params[:search] ||= params[:filter] if params[:filter]
params.permit(:sort, :search) # Don't show archived projects
params[:non_archived] = true
params.permit(:sort, :search, :non_archived)
end end
end end
end end
......
...@@ -25,4 +25,8 @@ class Import::BaseController < ApplicationController ...@@ -25,4 +25,8 @@ class Import::BaseController < ApplicationController
current_user.namespace current_user.namespace
end end
def project_save_error(project)
project.errors.full_messages.join(', ')
end
end end
...@@ -55,7 +55,7 @@ class Import::BitbucketController < Import::BaseController ...@@ -55,7 +55,7 @@ class Import::BitbucketController < Import::BaseController
if project.persisted? if project.persisted?
render json: ProjectSerializer.new.represent(project) render json: ProjectSerializer.new.represent(project)
else else
render json: { errors: project.errors.full_messages }, status: :unprocessable_entity render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end end
else else
render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
......
...@@ -66,7 +66,7 @@ class Import::FogbugzController < Import::BaseController ...@@ -66,7 +66,7 @@ class Import::FogbugzController < Import::BaseController
if project.persisted? if project.persisted?
render json: ProjectSerializer.new.represent(project) render json: ProjectSerializer.new.represent(project)
else else
render json: { errors: project.errors.full_messages }, status: :unprocessable_entity render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end end
end end
......
...@@ -48,7 +48,7 @@ class Import::GithubController < Import::BaseController ...@@ -48,7 +48,7 @@ class Import::GithubController < Import::BaseController
if project.persisted? if project.persisted?
render json: ProjectSerializer.new.represent(project) render json: ProjectSerializer.new.represent(project)
else else
render json: { errors: project.errors.full_messages }, status: :unprocessable_entity render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end end
else else
render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
......
...@@ -32,7 +32,7 @@ class Import::GitlabController < Import::BaseController ...@@ -32,7 +32,7 @@ class Import::GitlabController < Import::BaseController
if project.persisted? if project.persisted?
render json: ProjectSerializer.new.represent(project) render json: ProjectSerializer.new.represent(project)
else else
render json: { errors: project.errors.full_messages }, status: :unprocessable_entity render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end end
else else
render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
......
...@@ -92,7 +92,7 @@ class Import::GoogleCodeController < Import::BaseController ...@@ -92,7 +92,7 @@ class Import::GoogleCodeController < Import::BaseController
if project.persisted? if project.persisted?
render json: ProjectSerializer.new.represent(project) render json: ProjectSerializer.new.represent(project)
else else
render json: { errors: project.errors.full_messages }, status: :unprocessable_entity render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end end
end end
......
...@@ -18,7 +18,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController ...@@ -18,7 +18,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def upload_authorize def upload_authorize
set_workhorse_internal_api_content_type set_workhorse_internal_api_content_type
authorized = LfsObjectUploader.workhorse_authorize authorized = LfsObjectUploader.workhorse_authorize(has_length: true)
authorized.merge!(LfsOid: oid, LfsSize: size) authorized.merge!(LfsOid: oid, LfsSize: size)
render json: authorized render json: authorized
......
...@@ -15,7 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -15,7 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes def merge_request_params_attributes
[ [
:allow_maintainer_to_push, :allow_collaboration,
:assignee_id, :assignee_id,
:description, :description,
:force_remove_source_branch, :force_remove_source_branch,
......
...@@ -28,15 +28,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -28,15 +28,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end end
def show def show
validates_merge_request close_merge_request_if_no_source_project
close_merge_request_without_source_project mark_merge_request_mergeable
check_if_can_be_merged
# Return if the response has already been rendered
return if response_body
respond_to do |format| respond_to do |format|
format.html do format.html do
# use next to appease Rubocop
next render('invalid') if target_branch_missing?
# Build a note object for comment form # Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request) @note = @project.notes.new(noteable: @merge_request)
...@@ -234,20 +233,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -234,20 +233,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request alias_method :issuable, :merge_request
alias_method :awardable, :merge_request alias_method :awardable, :merge_request
def validates_merge_request
# Show git not found page
# if there is no saved commits between source & target branch
if @merge_request.has_no_commits?
# and if target branch doesn't exist
return invalid_mr unless @merge_request.target_branch_exists?
end
end
def invalid_mr
# Render special view for MR with removed target branch
render 'invalid'
end
def merge_params def merge_params
params.permit(merge_params_attributes) params.permit(merge_params_attributes)
end end
...@@ -261,7 +246,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -261,7 +246,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.head_pipeline && @merge_request.head_pipeline.active? @merge_request.head_pipeline && @merge_request.head_pipeline.active?
end end
def close_merge_request_without_source_project def close_merge_request_if_no_source_project
if !@merge_request.source_project && @merge_request.open? if !@merge_request.source_project && @merge_request.open?
@merge_request.close @merge_request.close
end end
...@@ -269,7 +254,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -269,7 +254,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private private
def check_if_can_be_merged def target_branch_missing?
@merge_request.has_no_commits? && !@merge_request.target_branch_exists?
end
def mark_merge_request_mergeable
@merge_request.check_if_can_be_merged @merge_request.check_if_can_be_merged
end end
......
class Projects::MilestonesController < Projects::ApplicationController class Projects::MilestonesController < Projects::ApplicationController
include Gitlab::Utils::StrongMemoize
include MilestoneActions include MilestoneActions
before_action :check_issuables_available! before_action :check_issuables_available!
...@@ -103,7 +104,7 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -103,7 +104,7 @@ class Projects::MilestonesController < Projects::ApplicationController
protected protected
def milestones def milestones
@milestones ||= begin strong_memoize(:milestones) do
MilestonesFinder.new(search_params).execute MilestonesFinder.new(search_params).execute
end end
end end
...@@ -121,10 +122,10 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -121,10 +122,10 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
def search_params def search_params
if @project.group && can?(current_user, :read_group, @project.group) if request.format.json? && @project.group && can?(current_user, :read_group, @project.group)
group = @project.group groups = @project.group.self_and_ancestors
end end
params.permit(:state).merge(project_ids: @project.id, group_ids: group&.id) params.permit(:state).merge(project_ids: @project.id, group_ids: groups&.select(:id))
end end
end end
...@@ -23,8 +23,6 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -23,8 +23,6 @@ class Projects::PipelinesController < Projects::ApplicationController
@finished_count = limited_pipelines_count(project, 'finished') @finished_count = limited_pipelines_count(project, 'finished')
@pipelines_count = limited_pipelines_count(project) @pipelines_count = limited_pipelines_count(project)
Gitlab::Ci::Pipeline::Preloader.preload(@pipelines)
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
...@@ -34,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -34,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
.with_pagination(request, response) .with_pagination(request, response)
.represent(@pipelines, disable_coverage: true), .represent(@pipelines, disable_coverage: true, preload: true),
count: { count: {
all: @pipelines_count, all: @pipelines_count,
running: @running_count, running: @running_count,
......
...@@ -13,6 +13,10 @@ module Users ...@@ -13,6 +13,10 @@ module Users
def index def index
@redirect = redirect_path @redirect = redirect_path
if @term.accepted_by_user?(current_user)
flash.now[:notice] = "You have already accepted the Terms of Service as #{current_user.to_reference}"
end
end end
def accept def accept
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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