diff options
270 files changed, 3639 insertions, 1508 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 997d8268008..ad643b41a8f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,6 +12,8 @@ variables: GIT_DEPTH: "20" PHANTOMJS_VERSION: "2.1.1" GET_SOURCES_ATTEMPTS: "3" + KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-${CI_COMMIT_REF_SLUG}.json + KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-${CI_COMMIT_REF_SLUG}.json before_script: - bundle --version @@ -34,14 +36,15 @@ stages: variables: SETUP_DB: "false" USE_BUNDLE_INSTALL: "false" + KNAPSACK_S3_BUCKET: "gitlab-ce-cache" cache: key: "knapsack" paths: - - knapsack/ + - knapsack/ artifacts: expire_in: 31d paths: - - knapsack/ + - knapsack/ .use-pg: &use-pg services: @@ -74,17 +77,17 @@ stages: - JOB_NAME=( $CI_JOB_NAME ) - export CI_NODE_INDEX=${JOB_NAME[-2]} - export CI_NODE_TOTAL=${JOB_NAME[-1]} - - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_GENERATE_REPORT=true - - cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH} + - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - knapsack rspec "--color --format documentation" artifacts: expire_in: 31d when: always paths: - - coverage/ - - knapsack/ - - tmp/capybara/ + - coverage/ + - knapsack/ + - tmp/capybara/ .rspec-knapsack-pg: &rspec-knapsack-pg <<: *rspec-knapsack @@ -102,17 +105,17 @@ stages: - JOB_NAME=( $CI_JOB_NAME ) - export CI_NODE_INDEX=${JOB_NAME[-2]} - export CI_NODE_TOTAL=${JOB_NAME[-1]} - - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_GENERATE_REPORT=true - - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH} + - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' artifacts: expire_in: 31d when: always paths: - - coverage/ - - knapsack/ - - tmp/capybara/ + - coverage/ + - knapsack/ + - tmp/capybara/ .spinach-knapsack-pg: &spinach-knapsack-pg <<: *spinach-knapsack @@ -124,15 +127,31 @@ stages: <<: *only-master-and-ee-or-mysql # Prepare and merge knapsack tests - knapsack: <<: *knapsack-state <<: *dedicated-runner stage: prepare script: - - mkdir -p knapsack/ - - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json' - - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json' + - mkdir -p knapsack/${CI_PROJECT_NAME}/ + - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || true + - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || true + - '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}' + - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}' + +update-knapsack: + <<: *knapsack-state + <<: *dedicated-runner + stage: post-test + script: + - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_node_*.json + - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_node_*.json + - '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' + - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json + only: + - master@gitlab-org/gitlab-ce + - master@gitlab-org/gitlab-ee + - master@gitlab/gitlabhq + - master@gitlab/gitlab-ee setup-test-env: <<: *use-pg @@ -152,20 +171,6 @@ setup-test-env: - public/assets - tmp/tests -update-knapsack: - <<: *knapsack-state - <<: *dedicated-runner - stage: post-test - script: - - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json - - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json - - rm -f knapsack/*_node_*.json - only: - - master@gitlab-org/gitlab-ce - - master@gitlab-org/gitlab-ee - - master@gitlab/gitlabhq - - master@gitlab/gitlab-ee - rspec pg 0 20: *rspec-knapsack-pg rspec pg 1 20: *rspec-knapsack-pg rspec pg 2 20: *rspec-knapsack-pg @@ -268,10 +273,9 @@ rake ee_compat_check: - /^[\d-]+-stable(-ee)?$/ allow_failure: yes cache: - key: "ruby233-ee_compat_check_repo" + key: "ee_compat_check_repo" paths: - - ee_compat_check/repo/ - - vendor/ruby + - ee_compat_check/ee-repo/ artifacts: name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}" when: on_failure @@ -371,6 +375,7 @@ rake karma: BABEL_ENV: "coverage" script: - bundle exec rake karma + coverage: '/^Statements *: (\d+\.\d+%)/' artifacts: name: coverage-javascript expire_in: 31d @@ -402,7 +407,7 @@ bundler:audit: - master@gitlab/gitlabhq - master@gitlab/gitlab-ee script: - - "bundle exec bundle-audit check --update" + - "bundle exec bundle-audit check --update --ignore CVE-2016-4658" .migration-paths: &migration-paths stage: test @@ -447,6 +452,7 @@ coverage: USE_BUNDLE_INSTALL: "true" script: - bundle exec scripts/merge-simplecov + coverage: '/LOC \((\d+\.\d+%)\) covered.$/' artifacts: name: coverage expire_in: 31d diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 0d91a54c7d4..1d0ba9ea182 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.3.0 +0.4.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 347f5833ee6..9df886c42a1 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.4.1 +1.4.2 @@ -244,7 +244,7 @@ gem 'net-ssh', '~> 3.0.1' gem 'base32', '~> 0.3.0' # Sentry integration -gem 'sentry-raven', '~> 2.0.0' +gem 'sentry-raven', '~> 2.4.0' gem 'premailer-rails', '~> 1.9.0' @@ -257,15 +257,14 @@ end group :development do gem 'foreman', '~> 0.78.0' - gem 'brakeman', '~> 3.4.0', require: false + gem 'brakeman', '~> 3.6.0', require: false gem 'letter_opener_web', '~> 1.3.0' - gem 'bullet', '~> 5.2.0', require: false + gem 'bullet', '~> 5.5.0', require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false - gem 'web-console', '~> 2.0' # Better errors handler - gem 'better_errors', '~> 1.0.1' + gem 'better_errors', '~> 2.1.0' gem 'binding_of_caller', '~> 0.7.2' # thin instead webrick @@ -297,7 +296,7 @@ group :development, :test do gem 'capybara-screenshot', '~> 1.0.0' gem 'poltergeist', '~> 1.9.0' - gem 'spring', '~> 1.7.0' + gem 'spring', '~> 2.0.0' gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-spinach', '~> 1.1.0' @@ -305,8 +304,8 @@ group :development, :test do gem 'rubocop-rspec', '~> 1.12.0', require: false gem 'scss_lint', '~> 0.47.0', require: false gem 'haml_lint', '~> 0.21.0', require: false - gem 'simplecov', '0.12.0', require: false - gem 'flay', '~> 2.6.1', require: false + gem 'simplecov', '~> 0.14.0', require: false + gem 'flay', '~> 2.8.0', require: false gem 'bundler-audit', '~> 0.5.0', require: false gem 'benchmark-ips', '~> 2.3.0', require: false @@ -323,7 +322,7 @@ group :test do gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'email_spec', '~> 1.6.0' gem 'json-schema', '~> 2.6.2' - gem 'webmock', '~> 1.21.0' + gem 'webmock', '~> 1.24.0' gem 'test_after_commit', '~> 1.1' gem 'sham_rack', '~> 1.3.6' gem 'timecop', '~> 0.8.0' diff --git a/Gemfile.lock b/Gemfile.lock index 07be5d7aded..2cb0e88962a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,19 +75,20 @@ GEM base32 (0.3.2) bcrypt (3.1.11) benchmark-ips (2.3.0) - better_errors (1.0.1) + better_errors (2.1.1) coderay (>= 1.0.0) erubis (>= 2.6.6) + rack (>= 0.9.0) bindata (2.3.5) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) - brakeman (3.4.1) + brakeman (3.6.1) browser (2.2.0) builder (3.2.3) - bullet (5.2.0) + bullet (5.5.1) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) bundler-audit (0.5.0) @@ -101,7 +102,7 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - capybara-screenshot (1.0.11) + capybara-screenshot (1.0.14) capybara (>= 1.0, < 3) launchy carrierwave (0.11.2) @@ -117,7 +118,7 @@ GEM numerizer (~> 0.1.1) chunky_png (1.3.5) cliver (0.3.2) - coderay (1.1.0) + coderay (1.1.1) coercible (1.0.0) descendants_tracker (~> 0.0.1) coffee-rails (4.1.1) @@ -200,7 +201,9 @@ GEM multi_json ffaker (2.4.0) ffi (1.9.10) - flay (2.6.1) + flay (2.8.1) + erubis (~> 2.7.0) + path_expander (~> 1.0) ruby_parser (~> 3.0) sexp_processor (~> 4.0) flowdock (0.7.1) @@ -340,6 +343,7 @@ GEM temple (~> 0.7.6) thor tilt + hashdiff (0.3.2) hashie (3.5.5) health_check (2.6.0) rails (>= 4.0) @@ -518,6 +522,7 @@ GEM activerecord (>= 4.0, < 5.1) parser (2.4.0.0) ast (~> 2.2) + path_expander (1.0.1) pg (0.18.4) poltergeist (1.9.0) capybara (~> 2.1) @@ -532,14 +537,14 @@ GEM premailer-rails (1.9.2) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) - pry (0.10.3) + pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - pry-byebug (3.4.1) + pry-byebug (3.4.2) byebug (~> 9.0) pry (~> 0.10) - pry-rails (0.3.4) + pry-rails (0.3.5) pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) rack (1.6.5) @@ -671,7 +676,7 @@ GEM ruby-progressbar (1.8.1) ruby-saml (1.4.1) nokogiri (>= 1.5.10) - ruby_parser (3.8.2) + ruby_parser (3.8.4) sexp_processor (~> 4.1) rubyntlm (0.5.2) rubypants (0.2.0) @@ -700,10 +705,10 @@ GEM activesupport (>= 3.1) select2-rails (3.5.9.3) thor (~> 0.14) - sentry-raven (2.0.2) - faraday (>= 0.7.6, < 0.10.x) + sentry-raven (2.4.0) + faraday (>= 0.7.6, < 1.0) settingslogic (2.0.9) - sexp_processor (4.7.0) + sexp_processor (4.8.0) sham_rack (1.3.6) rack shoulda-matchers (2.8.0) @@ -724,7 +729,7 @@ GEM faraday (~> 0.9) jwt (~> 1.5) multi_json (~> 1.10) - simplecov (0.12.0) + simplecov (0.14.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) @@ -741,7 +746,8 @@ GEM spinach (>= 0.4) spinach-rerun-reporter (0.0.2) spinach (~> 0.8) - spring (1.7.2) + spring (2.0.1) + activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) spring-commands-spinach (1.1.0) @@ -813,14 +819,10 @@ GEM vmstat (2.3.0) warden (1.2.6) rack (>= 1.0) - web-console (2.3.0) - activemodel (>= 4.0) - binding_of_caller (>= 0.7.2) - railties (>= 4.0) - sprockets-rails (>= 2.0, < 4.0) - webmock (1.21.0) + webmock (1.24.6) addressable (>= 2.3.6) crack (>= 0.3.2) + hashdiff webpack-rails (0.9.9) rails (>= 3.2.0) websocket-driver (0.6.3) @@ -854,12 +856,12 @@ DEPENDENCIES babosa (~> 1.0.2) base32 (~> 0.3.0) benchmark-ips (~> 2.3.0) - better_errors (~> 1.0.1) + better_errors (~> 2.1.0) binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) - brakeman (~> 3.4.0) + brakeman (~> 3.6.0) browser (~> 2.2) - bullet (~> 5.2.0) + bullet (~> 5.5.0) bundler-audit (~> 0.5.0) capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) @@ -885,7 +887,7 @@ DEPENDENCIES email_spec (~> 1.6.0) factory_girl_rails (~> 4.7.0) ffaker (~> 2.4) - flay (~> 2.6.1) + flay (~> 2.8.0) fog-aws (~> 0.9) fog-core (~> 1.40) fog-google (~> 0.5) @@ -991,18 +993,18 @@ DEPENDENCIES scss_lint (~> 0.47.0) seed-fu (~> 2.3.5) select2-rails (~> 3.5.9) - sentry-raven (~> 2.0.0) + sentry-raven (~> 2.4.0) settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) shoulda-matchers (~> 2.8.0) sidekiq (~> 4.2.7) sidekiq-cron (~> 0.4.4) sidekiq-limit_fetch (~> 3.4) - simplecov (= 0.12.0) + simplecov (~> 0.14.0) slack-notifier (~> 1.5.1) spinach-rails (~> 0.2.1) spinach-rerun-reporter (~> 0.0.2) - spring (~> 1.7.0) + spring (~> 2.0.0) spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.1.0) sprockets (~> 3.7.0) @@ -1023,8 +1025,7 @@ DEPENDENCIES version_sorter (~> 2.1.0) virtus (~> 1.0.1) vmstat (~> 2.3.0) - web-console (~> 2.0) - webmock (~> 1.21.0) + webmock (~> 1.24.0) webpack-rails (~> 0.9.9) wikicloth (= 0.8.1) diff --git a/README.md b/README.md index 09e08adbb73..f0e3b52ef6f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ # GitLab [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) -[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) +[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) +## Test coverage + +- [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby +- [![JavaScript coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=rake+karma)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-javascript) JavaScript + ## Canonical source The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js deleted file mode 100644 index ec1c018424d..00000000000 --- a/app/assets/javascripts/blob/blob_ci_yaml.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable no-param-reassign, comma-dangle */ -/* global Api */ - -require('./template_selector'); - -((global) => { - class BlobCiYamlSelector extends gl.TemplateSelector { - requestFile(query) { - return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); - } - - requestFileSuccess(file) { - return super.requestFileSuccess(file); - } - } - - global.BlobCiYamlSelector = BlobCiYamlSelector; - - class BlobCiYamlSelectors { - constructor({ editor, $dropdowns } = {}) { - this.editor = editor; - this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector'); - this.initSelectors(); - } - - initSelectors() { - const editor = this.editor; - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new BlobCiYamlSelector({ - editor, - pattern: /(.gitlab-ci.yml)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), - dropdown: $dropdown - }); - }); - } - } - - global.BlobCiYamlSelectors = BlobCiYamlSelectors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js b/app/assets/javascripts/blob/blob_dockerfile_selector.js deleted file mode 100644 index d4f60cc6ecd..00000000000 --- a/app/assets/javascripts/blob/blob_dockerfile_selector.js +++ /dev/null @@ -1,19 +0,0 @@ -/* global Api */ - -require('./template_selector'); - -(() => { - const global = window.gl || (window.gl = {}); - - class BlobDockerfileSelector extends gl.TemplateSelector { - requestFile(query) { - return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this)); - } - - requestFileSuccess(file) { - return super.requestFileSuccess(file); - } - } - - global.BlobDockerfileSelector = BlobDockerfileSelector; -})(); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/blob_dockerfile_selectors.js deleted file mode 100644 index 9cee79fa5d5..00000000000 --- a/app/assets/javascripts/blob/blob_dockerfile_selectors.js +++ /dev/null @@ -1,27 +0,0 @@ -(() => { - const global = window.gl || (window.gl = {}); - - class BlobDockerfileSelectors { - constructor({ editor, $dropdowns } = {}) { - this.editor = editor; - this.$dropdowns = $dropdowns || $('.js-dockerfile-selector'); - this.initSelectors(); - } - - initSelectors() { - const editor = this.editor; - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new gl.BlobDockerfileSelector({ - editor, - pattern: /(Dockerfile)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'), - dropdown: $dropdown, - }); - }); - } - } - - global.BlobDockerfileSelectors = BlobDockerfileSelectors; -})(); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 8f6bf162d6e..c9fe23aec75 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,66 +1,63 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, max-len */ +/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */ /* global Dropzone */ -(function() { - this.BlobFileDropzone = (function() { - function BlobFileDropzone(form, method) { - var dropzone, form_dropzone, submitButton; - form_dropzone = form.find('.dropzone'); - Dropzone.autoDiscover = false; - dropzone = form_dropzone.dropzone({ - autoDiscover: false, - autoProcessQueue: false, - url: form.attr('action'), - // Rails uses a hidden input field for PUT - // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails - method: method, - clickable: true, - uploadMultiple: false, - paramName: "file", - maxFilesize: gon.max_file_size || 10, - parallelUploads: 1, - maxFiles: 1, - addRemoveLinks: true, - previewsContainer: '.dropzone-previews', - headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - }, - init: function() { - this.on('addedfile', function(file) { - $('.dropzone-alerts').html('').hide(); - }); - this.on('success', function(header, response) { - window.location.href = response.filePath; - }); - this.on('maxfilesexceeded', function(file) { - this.removeFile(file); - }); - return this.on('sending', function(file, xhr, formData) { - formData.append('target_branch', form.find('input[name="target_branch"]').val()); - formData.append('create_merge_request', form.find('.js-create-merge-request').val()); - formData.append('commit_message', form.find('.js-commit-message').val()); - }); - }, - // Override behavior of adding error underneath preview - error: function(file, errorMessage) { - var stripped; - stripped = $("<div/>").html(errorMessage).text(); - $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show(); +export default class BlobFileDropzone { + constructor(form, method) { + const formDropzone = form.find('.dropzone'); + Dropzone.autoDiscover = false; + + const dropzone = formDropzone.dropzone({ + autoDiscover: false, + autoProcessQueue: false, + url: form.attr('action'), + // Rails uses a hidden input field for PUT + // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails + method: method, + clickable: true, + uploadMultiple: false, + paramName: 'file', + maxFilesize: gon.max_file_size || 10, + parallelUploads: 1, + maxFiles: 1, + addRemoveLinks: true, + previewsContainer: '.dropzone-previews', + headers: { + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'), + }, + init: function () { + this.on('addedfile', function () { + $('.dropzone-alerts').html('').hide(); + }); + this.on('success', function (header, response) { + window.location.href = response.filePath; + }); + this.on('maxfilesexceeded', function (file) { this.removeFile(file); - } - }); - submitButton = form.find('#submit-all')[0]; - submitButton.addEventListener('click', function(e) { - e.preventDefault(); - e.stopPropagation(); - if (dropzone[0].dropzone.getQueuedFiles().length === 0) { - alert("Please select a file"); - } - dropzone[0].dropzone.processQueue(); - return false; - }); - } + }); + this.on('sending', function (file, xhr, formData) { + formData.append('target_branch', form.find('input[name="target_branch"]').val()); + formData.append('create_merge_request', form.find('.js-create-merge-request').val()); + formData.append('commit_message', form.find('.js-commit-message').val()); + }); + }, + // Override behavior of adding error underneath preview + error: function (file, errorMessage) { + const stripped = $('<div/>').html(errorMessage).text(); + $('.dropzone-alerts').html(`Error uploading file: "${stripped}"`).show(); + this.removeFile(file); + }, + }); - return BlobFileDropzone; - })(); -}).call(window); + const submitButton = form.find('#submit-all')[0]; + submitButton.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + if (dropzone[0].dropzone.getQueuedFiles().length === 0) { + // eslint-disable-next-line no-alert + alert('Please select a file'); + } + dropzone[0].dropzone.processQueue(); + return false; + }); + } +} diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js deleted file mode 100644 index de20eab9cd1..00000000000 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params */ -/* global Api */ - -require('./template_selector'); - -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.BlobGitignoreSelector = (function(superClass) { - extend(BlobGitignoreSelector, superClass); - - function BlobGitignoreSelector() { - return BlobGitignoreSelector.__super__.constructor.apply(this, arguments); - } - - BlobGitignoreSelector.prototype.requestFile = function(query) { - return Api.gitignoreText(query.name, this.requestFileSuccess.bind(this)); - }; - - return BlobGitignoreSelector; - })(gl.TemplateSelector); -}).call(window); diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js deleted file mode 100644 index 43e5c0a5641..00000000000 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, max-len */ -/* global BlobGitignoreSelector */ - -(function() { - this.BlobGitignoreSelectors = (function() { - function BlobGitignoreSelectors(opts) { - var ref; - this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitignore-selector'), this.editor = opts.editor; - this.$dropdowns.each((function(_this) { - return function(i, dropdown) { - var $dropdown; - $dropdown = $(dropdown); - return new BlobGitignoreSelector({ - pattern: /(.gitignore)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitignore-selector-wrap'), - dropdown: $dropdown, - editor: _this.editor - }); - }; - })(this)); - } - - return BlobGitignoreSelectors; - })(); -}).call(window); diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js deleted file mode 100644 index b582052a76e..00000000000 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle */ -/* global Api */ - -require('./template_selector'); - -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.BlobLicenseSelector = (function(superClass) { - extend(BlobLicenseSelector, superClass); - - function BlobLicenseSelector() { - return BlobLicenseSelector.__super__.constructor.apply(this, arguments); - } - - BlobLicenseSelector.prototype.requestFile = function(query) { - var data; - data = { - project: this.dropdown.data('project'), - fullname: this.dropdown.data('fullname') - }; - return Api.licenseText(query.id, data, this.requestFileSuccess.bind(this)); - }; - - return BlobLicenseSelector; - })(gl.TemplateSelector); -}).call(window); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js deleted file mode 100644 index c5067b0feae..00000000000 --- a/app/assets/javascripts/blob/blob_license_selectors.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable no-unused-vars, no-param-reassign */ -/* global BlobLicenseSelector */ - -((global) => { - class BlobLicenseSelectors { - constructor({ $dropdowns, editor }) { - this.$dropdowns = $('.js-license-selector'); - this.editor = editor; - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new BlobLicenseSelector({ - editor, - pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-license-selector-wrap'), - dropdown: $dropdown, - }); - }); - } - } - - global.BlobLicenseSelectors = BlobLicenseSelectors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js deleted file mode 100644 index 7e03ec3b391..00000000000 --- a/app/assets/javascripts/blob/template_selector.js +++ /dev/null @@ -1,101 +0,0 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, no-param-reassign, max-len */ - -((global) => { - class TemplateSelector { - constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) { - this.onClick = this.onClick.bind(this); - this.dropdown = dropdown; - this.data = data; - this.pattern = pattern; - this.wrapper = wrapper; - this.editor = editor; - this.fileEndpoint = fileEndpoint; - this.$input = $input || $('#file_name'); - this.dropdownIcon = $('.fa-chevron-down', this.dropdown); - this.buildDropdown(); - this.bindEvents(); - this.onFilenameUpdate(); - - this.autosizeUpdateEvent = document.createEvent('Event'); - this.autosizeUpdateEvent.initEvent('autosize:update', true, false); - } - - buildDropdown() { - return this.dropdown.glDropdown({ - data: this.data, - filterable: true, - selectable: true, - toggleLabel: this.toggleLabel, - search: { - fields: ['name'] - }, - clicked: this.onClick, - text: function(item) { - return item.name; - } - }); - } - - bindEvents() { - return this.$input.on('keyup blur', (e) => this.onFilenameUpdate()); - } - - toggleLabel(item) { - return item.name; - } - - onFilenameUpdate() { - var filenameMatches; - if (!this.$input.length) { - return; - } - filenameMatches = this.pattern.test(this.$input.val().trim()); - if (!filenameMatches) { - this.wrapper.addClass('hidden'); - return; - } - return this.wrapper.removeClass('hidden'); - } - - onClick(item, el, e) { - e.preventDefault(); - return this.requestFile(item); - } - - requestFile(item) { - // This `requestFile` method is an abstract method that should - // be added by all subclasses. - } - - // To be implemented on the extending class - // e.g. - // Api.gitignoreText item.name, @requestFileSuccess.bind(@) - requestFileSuccess(file, { skipFocus } = {}) { - if (!file) return; - - const oldValue = this.editor.getValue(); - const newValue = file.content; - - this.editor.setValue(newValue, 1); - if (!skipFocus) this.editor.focus(); - - if (this.editor instanceof jQuery) { - this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); - } - } - - startLoadingSpinner() { - this.dropdownIcon - .addClass('fa-spinner fa-spin') - .removeClass('fa-chevron-down'); - } - - stopLoadingSpinner() { - this.dropdownIcon - .addClass('fa-chevron-down') - .removeClass('fa-spinner fa-spin'); - } - } - - global.TemplateSelector = TemplateSelector; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js new file mode 100644 index 00000000000..5a5954e7751 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js @@ -0,0 +1,9 @@ +/* global Api */ + +import TemplateSelector from './template_selector'; + +export default class BlobCiYamlSelector extends TemplateSelector { + requestFile(query) { + return Api.gitlabCiYml(query.name, (file, config) => this.setEditorContent(file, config)); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js new file mode 100644 index 00000000000..7a4d6a42a03 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js @@ -0,0 +1,23 @@ +/* global Api */ + +import BlobCiYamlSelector from './blob_ci_yaml_selector'; + +export default class BlobCiYamlSelectors { + constructor({ editor, $dropdowns }) { + this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector'); + this.initSelectors(editor); + } + + initSelectors(editor) { + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new BlobCiYamlSelector({ + editor, + pattern: /(.gitlab-ci.yml)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), + dropdown: $dropdown, + }); + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js new file mode 100644 index 00000000000..19f8820a0cb --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js @@ -0,0 +1,9 @@ +/* global Api */ + +import TemplateSelector from './template_selector'; + +export default class BlobDockerfileSelector extends TemplateSelector { + requestFile(query) { + return Api.dockerfileYml(query.name, (file, config) => this.setEditorContent(file, config)); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js new file mode 100644 index 00000000000..da067035b43 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js @@ -0,0 +1,23 @@ +import BlobDockerfileSelector from './blob_dockerfile_selector'; + +export default class BlobDockerfileSelectors { + constructor({ editor, $dropdowns }) { + this.editor = editor; + this.$dropdowns = $dropdowns || $('.js-dockerfile-selector'); + this.initSelectors(); + } + + initSelectors() { + const editor = this.editor; + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new BlobDockerfileSelector({ + editor, + pattern: /(Dockerfile)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'), + dropdown: $dropdown, + }); + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js new file mode 100644 index 00000000000..0b6b02fc2b3 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js @@ -0,0 +1,9 @@ +/* global Api */ + +import TemplateSelector from './template_selector'; + +export default class BlobGitignoreSelector extends TemplateSelector { + requestFile(query) { + return Api.gitignoreText(query.name, (file, config) => this.setEditorContent(file, config)); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js new file mode 100644 index 00000000000..dc485d97677 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js @@ -0,0 +1,23 @@ +import BlobGitignoreSelector from './blob_gitignore_selector'; + +export default class BlobGitignoreSelectors { + constructor({ editor, $dropdowns }) { + this.$dropdowns = $dropdowns || $('.js-gitignore-selector'); + this.editor = editor; + this.initSelectors(); + } + + initSelectors() { + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + + return new BlobGitignoreSelector({ + pattern: /(.gitignore)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitignore-selector-wrap'), + dropdown: $dropdown, + editor: this.editor, + }); + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selector.js b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js new file mode 100644 index 00000000000..e9cb31cc2dc --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js @@ -0,0 +1,13 @@ +/* global Api */ + +import TemplateSelector from './template_selector'; + +export default class BlobLicenseSelector extends TemplateSelector { + requestFile(query) { + const data = { + project: this.dropdown.data('project'), + fullname: this.dropdown.data('fullname'), + }; + return Api.licenseText(query.id, data, (file, config) => this.setEditorContent(file, config)); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js new file mode 100644 index 00000000000..a44f4f78b2d --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js @@ -0,0 +1,24 @@ +/* eslint-disable no-unused-vars, no-param-reassign */ + +import BlobLicenseSelector from './blob_license_selector'; + +export default class BlobLicenseSelectors { + constructor({ $dropdowns, editor }) { + this.$dropdowns = $dropdowns || $('.js-license-selector'); + this.initSelectors(editor); + } + + initSelectors(editor) { + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + + return new BlobLicenseSelector({ + editor, + pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-license-selector-wrap'), + dropdown: $dropdown, + }); + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/template_selector.js b/app/assets/javascripts/blob/template_selectors/template_selector.js new file mode 100644 index 00000000000..d7c1c32efbd --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/template_selector.js @@ -0,0 +1,92 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +export default class TemplateSelector { + constructor({ dropdown, data, pattern, wrapper, editor, $input } = {}) { + this.pattern = pattern; + this.editor = editor; + this.dropdown = dropdown; + this.$dropdownContainer = wrapper; + this.$filenameInput = $input || $('#file_name'); + this.$dropdownIcon = $('.fa-chevron-down', dropdown); + + this.initDropdown(dropdown, data); + this.listenForFilenameInput(); + this.renderMatchedDropdown(); + this.initAutosizeUpdateEvent(); + } + + initDropdown(dropdown, data) { + return $(dropdown).glDropdown({ + data, + filterable: true, + selectable: true, + toggleLabel: item => item.name, + search: { + fields: ['name'], + }, + clicked: (item, el, e) => this.fetchFileTemplate(item, el, e), + text: item => item.name, + }); + } + + initAutosizeUpdateEvent() { + this.autosizeUpdateEvent = document.createEvent('Event'); + this.autosizeUpdateEvent.initEvent('autosize:update', true, false); + } + + listenForFilenameInput() { + return this.$filenameInput.on('keyup blur', e => this.renderMatchedDropdown(e)); + } + + renderMatchedDropdown() { + if (!this.$filenameInput.length) { + return null; + } + + const filenameMatches = this.pattern.test(this.$filenameInput.val().trim()); + + if (!filenameMatches) { + return this.$dropdownContainer.addClass('hidden'); + } + return this.$dropdownContainer.removeClass('hidden'); + } + + fetchFileTemplate(item, el, e) { + e.preventDefault(); + return this.requestFile(item); + } + + requestFile(item) { + // This `requestFile` method is an abstract method that should + // be added by all subclasses. + } + + // To be implemented on the extending class + // e.g. Api.gitlabCiYml(query.name, file => this.setEditorContent(file)); + + setEditorContent(file, { skipFocus } = {}) { + if (!file) return; + + const newValue = file.content; + + this.editor.setValue(newValue, 1); + + if (!skipFocus) this.editor.focus(); + + if (this.editor instanceof jQuery) { + this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); + } + } + + startLoadingSpinner() { + this.$dropdownIcon + .addClass('fa-spinner fa-spin') + .removeClass('fa-chevron-down'); + } + + stopLoadingSpinner() { + this.$dropdownIcon + .addClass('fa-chevron-down') + .removeClass('fa-spinner fa-spin'); + } +} diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js new file mode 100644 index 00000000000..c5deccf631e --- /dev/null +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -0,0 +1,32 @@ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */ +/* global EditBlob */ +/* global NewCommitForm */ + +import EditBlob from './edit_blob'; +import BlobFileDropzone from '../blob/blob_file_dropzone'; + +$(() => { + const editBlobForm = $('.js-edit-blob-form'); + const uploadBlobForm = $('.js-upload-blob-form'); + + if (editBlobForm.length) { + const urlRoot = editBlobForm.data('relative-url-root'); + const assetsPath = editBlobForm.data('assets-prefix'); + const blobLanguage = editBlobForm.data('blob-language'); + + new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage); + new NewCommitForm(editBlobForm); + } + + if (uploadBlobForm.length) { + const method = uploadBlobForm.data('method'); + + new BlobFileDropzone(uploadBlobForm, method); + new NewCommitForm(uploadBlobForm); + + window.gl.utils.disableButtonIfEmptyField( + uploadBlobForm.find('.js-commit-message'), + '.btn-upload-file', + ); + } +}); diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js deleted file mode 100644 index 0436bbb0eaf..00000000000 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */ -/* global EditBlob */ -/* global NewCommitForm */ - -require('./edit_blob'); - -(function() { - $(function() { - var url = $(".js-edit-blob-form").data("relative-url-root"); - url += $(".js-edit-blob-form").data("assets-prefix"); - - var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language')); - new NewCommitForm($('.js-edit-blob-form')); - }); -}).call(window); diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index a1127b9e30e..d3560d5df3b 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,88 +1,99 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, max-len */ /* global ace */ -/* global BlobGitignoreSelectors */ - -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; - - this.EditBlob = (function() { - function EditBlob(assets_path, ace_mode) { - if (ace_mode == null) { - ace_mode = null; - } - this.editModeLinkClickHandler = bind(this.editModeLinkClickHandler, this); - ace.config.set("modePath", assets_path + "/ace"); - ace.config.loadModule("ace/ext/searchbox"); - this.editor = ace.edit("editor"); - this.editor.focus(); - if (ace_mode) { - this.editor.getSession().setMode("ace/mode/" + ace_mode); - } - $('form').submit((function(_this) { - return function() { - return $("#file-content").val(_this.editor.getValue()); - }; - // Before a form submission, move the content from the Ace editor into the - // submitted textarea - })(this)); - this.initModePanesAndLinks(); - this.initSoftWrap(); - new gl.BlobLicenseSelectors({ - editor: this.editor - }); + +import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors'; +import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors'; +import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors'; +import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors'; + +export default class EditBlob { + constructor(assetsPath, aceMode) { + this.configureAceEditor(aceMode, assetsPath); + this.prepFileContentForSubmit(); + this.initModePanesAndLinks(); + this.initSoftWrap(); + this.initFileSelectors(); + } + + configureAceEditor(aceMode, assetsPath) { + ace.config.set('modePath', `${assetsPath}/ace`); + ace.config.loadModule('ace/ext/searchbox'); + + this.editor = ace.edit('editor'); + this.editor.focus(); + + if (aceMode) { + this.editor.getSession().setMode(`ace/mode/${aceMode}`); + } + } + + prepFileContentForSubmit() { + $('form').submit(() => { + $('#file-content').val(this.editor.getValue()); + }); + } + + initFileSelectors() { + this.blobTemplateSelectors = [ + new BlobLicenseSelectors({ + editor: this.editor, + }), new BlobGitignoreSelectors({ - editor: this.editor - }); - new gl.BlobCiYamlSelectors({ - editor: this.editor - }); - new gl.BlobDockerfileSelectors({ - editor: this.editor + editor: this.editor, + }), + new BlobCiYamlSelectors({ + editor: this.editor, + }), + new BlobDockerfileSelectors({ + editor: this.editor, + }), + ]; + } + + initModePanesAndLinks() { + this.$editModePanes = $('.js-edit-mode-pane'); + this.$editModeLinks = $('.js-edit-mode a'); + this.$editModeLinks.on('click', e => this.editModeLinkClickHandler(e)); + } + + editModeLinkClickHandler(e) { + e.preventDefault(); + + const currentLink = $(e.target); + const paneId = currentLink.attr('href'); + const currentPane = this.$editModePanes.filter(paneId); + + this.$editModeLinks.parent().removeClass('active hover'); + + currentLink.parent().addClass('active hover'); + + this.$editModePanes.hide(); + + currentPane.fadeIn(200); + + if (paneId === '#preview') { + this.$toggleButton.hide(); + return $.post(currentLink.data('preview-url'), { + content: this.editor.getValue(), + }, (response) => { + currentPane.empty().append(response); + return currentPane.renderGFM(); }); } - EditBlob.prototype.initModePanesAndLinks = function() { - this.$editModePanes = $(".js-edit-mode-pane"); - this.$editModeLinks = $(".js-edit-mode a"); - return this.$editModeLinks.click(this.editModeLinkClickHandler); - }; - - EditBlob.prototype.editModeLinkClickHandler = function(event) { - var currentLink, currentPane, paneId; - event.preventDefault(); - currentLink = $(event.target); - paneId = currentLink.attr("href"); - currentPane = this.$editModePanes.filter(paneId); - this.$editModeLinks.parent().removeClass("active hover"); - currentLink.parent().addClass("active hover"); - this.$editModePanes.hide(); - currentPane.fadeIn(200); - if (paneId === "#preview") { - this.$toggleButton.hide(); - return $.post(currentLink.data("preview-url"), { - content: this.editor.getValue() - }, function(response) { - currentPane.empty().append(response); - return currentPane.renderGFM(); - }); - } else { - this.$toggleButton.show(); - return this.editor.focus(); - } - }; - - EditBlob.prototype.initSoftWrap = function() { - this.isSoftWrapped = false; - this.$toggleButton = $('.soft-wrap-toggle'); - this.$toggleButton.on('click', this.toggleSoftWrap.bind(this)); - }; - - EditBlob.prototype.toggleSoftWrap = function(e) { - this.isSoftWrapped = !this.isSoftWrapped; - this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped); - this.editor.getSession().setUseWrapMode(this.isSoftWrapped); - }; - - return EditBlob; - })(); -}).call(window); + this.$toggleButton.show(); + + return this.editor.focus(); + } + + initSoftWrap() { + this.isSoftWrapped = false; + this.$toggleButton = $('.soft-wrap-toggle'); + this.$toggleButton.on('click', () => this.toggleSoftWrap()); + } + + toggleSoftWrap() { + this.isSoftWrapped = !this.isSoftWrapped; + this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped); + this.editor.getSession().setUseWrapMode(this.isSoftWrapped); + } +} diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js index 2e22b1eca47..b214b5a7199 100644 --- a/app/assets/javascripts/boards/components/modal/filters.js +++ b/app/assets/javascripts/boards/components/modal/filters.js @@ -17,7 +17,7 @@ export default { this.filteredSearch.handleInputPlaceholder(); this.filteredSearch.toggleClearSearchButton(); }, - beforeDestroy() { + destroyed() { this.filteredSearch.cleanup(); FilteredSearchContainer.container = document; this.store.path = ''; diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index 4800407be1c..91c08cde13a 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -65,8 +65,15 @@ require('./empty_state'); }, filter: { handler() { - this.page = 1; - this.loadIssues(true); + if (this.$el.tagName) { + this.page = 1; + this.filterLoading = true; + + this.loadIssues(true) + .then(() => { + this.filterLoading = false; + }); + } }, deep: true, }, @@ -140,14 +147,14 @@ require('./empty_state'); :image="blankStateImage" :issue-link-base="issueLinkBase" :root-path="rootPath" - v-if="!loading && showList"></modal-list> + v-if="!loading && showList && !filterLoading"></modal-list> <empty-state v-if="showEmptyState" :image="blankStateImage" :new-issue-path="newIssuePath"></empty-state> <section class="add-issues-list text-center" - v-if="loading"> + v-if="loading || filterLoading"> <div class="add-issues-list-loading"> <i class="fa fa-spinner fa-spin"></i> </div> diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js index 7ee266a831f..9b009483a3c 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -15,6 +15,7 @@ searchTerm: '', loading: false, loadingNewPage: false, + filterLoading: false, page: 1, perPage: 50, filter: { diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 360fb39dc9c..a20e5bc3b1b 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -1,10 +1,10 @@ -/* eslint-disable no-new*/ -/* global Flash */ import Vue from 'vue'; import PipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import PipelinesService from '../../vue_pipelines_index/services/pipelines_service'; import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store'; import eventHub from '../../vue_pipelines_index/event_hub'; +import EmptyState from '../../vue_pipelines_index/components/empty_state'; +import ErrorState from '../../vue_pipelines_index/components/error_state'; import '../../lib/utils/common_utils'; import '../../vue_shared/vue_resource_interceptor'; @@ -22,6 +22,8 @@ import '../../vue_shared/vue_resource_interceptor'; export default Vue.component('pipelines-table', { components: { 'pipelines-table-component': PipelinesTableComponent, + 'error-state': ErrorState, + 'empty-state': EmptyState, }, /** @@ -36,12 +38,24 @@ export default Vue.component('pipelines-table', { return { endpoint: pipelinesTableData.endpoint, + helpPagePath: pipelinesTableData.helpPagePath, store, state: store.state, isLoading: false, + hasError: false, }; }, + computed: { + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + + shouldRenderEmptyState() { + return !this.state.pipelines.length && !this.isLoading; + }, + }, + /** * When the component is about to be mounted, tell the service to fetch the data * @@ -80,8 +94,8 @@ export default Vue.component('pipelines-table', { this.isLoading = false; }) .catch(() => { + this.hasError = true; this.isLoading = false; - new Flash('An error occurred while fetching the pipelines, please reload the page again.'); }); }, }, @@ -92,12 +106,11 @@ export default Vue.component('pipelines-table', { <i class="fa fa-spinner fa-spin"></i> </div> - <div class="blank-state blank-state-no-icon" - v-if="!isLoading && state.pipelines.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - No pipelines to show - </h2> - </div> + <empty-state + v-if="shouldRenderEmptyState" + :help-page-path="helpPagePath" /> + + <error-state v-if="shouldRenderErrorState" /> <div class="table-holder" v-if="!isLoading && state.pipelines.length > 0"> diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index fbd0db64ca7..3253eebd9b5 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -1,9 +1,11 @@ // ECMAScript polyfills import 'core-js/fn/array/find'; +import 'core-js/fn/array/from'; import 'core-js/fn/object/assign'; import 'core-js/fn/promise'; import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/from-code-point'; +import 'core-js/fn/symbol'; // Browser polyfills import './polyfills/custom_event'; diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js new file mode 100644 index 00000000000..abe48572347 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js @@ -0,0 +1,17 @@ +export default { + props: { + count: { + type: Number, + required: true, + }, + }, + template: ` + <span v-if="count === 50" class="events-info pull-right"> + <i class="fa fa-warning has-tooltip" + aria-hidden="true" + title="Limited to showing 50 events at most" + data-placement="top"></i> + Showing 50 events + </span> + `, +}; diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js index 9947f355aca..3f419a96ff9 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js @@ -14,6 +14,7 @@ import Vue from 'vue'; <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="mergeRequest in items" class="stage-event-item"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js index 6ad4805e8c5..7ffa38edd9e 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js @@ -14,6 +14,7 @@ import Vue from 'vue'; <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="issue in items" class="stage-event-item"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js index 42e1bbce744..d736c8b0c28 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js @@ -19,12 +19,7 @@ import iconCommit from '../svg/icon_commit.svg'; <div> <div class="events-description"> {{ stage.description }} - <span v-if="items.length === 50" class="events-info pull-right"> - <i class="fa fa-warning has-tooltip" - title="Limited to showing 50 events at most" - data-placement="top"></i> - Showing 50 events - </span> + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="commit in items" class="stage-event-item"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js index da80450a32c..698a79ca68c 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js @@ -14,6 +14,7 @@ import Vue from 'vue'; <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="issue in items" class="stage-event-item"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js index 2200f43914f..e63c41f2a57 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js @@ -14,6 +14,7 @@ import Vue from 'vue'; <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="mergeRequest in items" class="stage-event-item"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js index 8fa63734cf1..d51f7134e25 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js @@ -17,6 +17,7 @@ import iconBranch from '../svg/icon_branch.svg'; <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="build in items" class="stage-event-item item-build-component"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js index 0015249cfaa..17ae3a9ddc1 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js @@ -18,6 +18,7 @@ import iconBranch from '../svg/icon_branch.svg'; <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="build in items" class="stage-event-item item-build-component"> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index ae17d05e679..b099b39e58f 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; +import LimitWarningComponent from './components/limit_warning_component'; require('./components/stage_code_component'); require('./components/stage_issue_component'); @@ -130,5 +131,6 @@ $(() => { }); // Register global components + Vue.component('limit-warning', LimitWarningComponent); Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); }); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index cfa60325fcc..88180149715 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -33,11 +33,7 @@ class Diff { handleClickUnfold(e) { const $target = $(e.target); - // current babel config relies on iterators implementation, so we cannot simply do: - // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); - const ref = this.lineNumbers($target.parent()); - const oldLineNumber = ref[0]; - const newLineNumber = ref[1]; + const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); const offset = newLineNumber - oldLineNumber; const bottom = $target.hasClass('js-unfold-bottom'); let since; @@ -105,10 +101,11 @@ class Diff { } lineNumbers(line) { - if (!line.children().length) { + const children = line.find('.diff-line-num').toArray(); + if (children.length !== 2) { return [0, 0]; } - return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10)); + return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0); } highlightSelectedLine() { diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js index 9b40a3f20a4..7f7d93f3e27 100644 --- a/app/assets/javascripts/droplab/droplab_filter.js +++ b/app/assets/javascripts/droplab/droplab_filter.js @@ -56,10 +56,12 @@ require('../window')(function(w){ this.hookInput = hookInput; this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper); + this.hookInput.trigger.addEventListener('mousedown.dl', this.keydownWrapper); }, destroy: function destroy(){ this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper); + this.hookInput.trigger.removeEventListener('mousedown.dl', this.keydownWrapper); } }; }); diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js index 6a028f299b1..62675d7e67e 100644 --- a/app/assets/javascripts/group_name.js +++ b/app/assets/javascripts/group_name.js @@ -1,40 +1,64 @@ -const GROUP_LIMIT = 2; + +import _ from 'underscore'; export default class GroupName { constructor() { - this.titleContainer = document.querySelector('.title'); - this.groups = document.querySelectorAll('.group-path'); + this.titleContainer = document.querySelector('.title-container'); + this.title = document.querySelector('.title'); + this.titleWidth = this.title.offsetWidth; this.groupTitle = document.querySelector('.group-title'); + this.groups = document.querySelectorAll('.group-path'); this.toggle = null; this.isHidden = false; this.init(); } init() { - if (this.groups.length > GROUP_LIMIT) { + if (this.groups.length > 0) { this.groups[this.groups.length - 1].classList.remove('hidable'); - this.addToggle(); + this.toggleHandler(); + window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100)); } this.render(); } - addToggle() { - const header = document.querySelector('.header-content'); + toggleHandler() { + if (this.titleWidth > this.titleContainer.offsetWidth) { + if (!this.toggle) this.createToggle(); + this.showToggle(); + } else if (this.toggle) { + this.hideToggle(); + } + } + + createToggle() { this.toggle = document.createElement('button'); this.toggle.className = 'text-expander group-name-toggle'; this.toggle.setAttribute('aria-label', 'Toggle full path'); this.toggle.innerHTML = '...'; this.toggle.addEventListener('click', this.toggleGroups.bind(this)); - header.insertBefore(this.toggle, this.titleContainer); + this.titleContainer.insertBefore(this.toggle, this.title); this.toggleGroups(); } + showToggle() { + this.title.classList.add('wrap'); + this.toggle.classList.remove('hidden'); + if (this.isHidden) this.groupTitle.classList.add('is-hidden'); + } + + hideToggle() { + this.title.classList.remove('wrap'); + this.toggle.classList.add('hidden'); + if (this.isHidden) this.groupTitle.classList.remove('is-hidden'); + } + toggleGroups() { this.isHidden = !this.isHidden; this.groupTitle.classList.toggle('is-hidden'); } render() { - this.titleContainer.classList.remove('initializing'); + this.title.classList.remove('initializing'); } } diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 938cf9912a8..c30a1fcb5da 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -36,20 +36,21 @@ export default class Poll { this.options.data = options.data || {}; this.intervalHeader = 'POLL-INTERVAL'; + this.timeoutID = null; + this.canPoll = true; } checkConditions(response) { const headers = gl.utils.normalizeHeaders(response.headers); const pollInterval = headers[this.intervalHeader]; - if (pollInterval > 0 && response.status === httpStatusCodes.OK) { - this.options.successCallback(response); - setTimeout(() => { + if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { + this.timeoutID = setTimeout(() => { this.makeRequest(); }, pollInterval); - } else { - this.options.successCallback(response); } + + this.options.successCallback(response); } makeRequest() { @@ -59,4 +60,14 @@ export default class Poll { .then(response => this.checkConditions(response)) .catch(error => errorCallback(error)); } + + /** + * Stops the polling recursive chain + * and guarantees if the timeout is already running it won't make another request by + * cancelling the previously established timeout. + */ + stop() { + this.canPoll = false; + clearTimeout(this.timeoutID); + } } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index b1ca0dc091d..9007d661d01 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -47,15 +47,6 @@ import { installGlEmojiElement } from './behaviors/gl_emoji'; installGlEmojiElement(); // blob -import './blob/blob_ci_yaml'; -import './blob/blob_dockerfile_selector'; -import './blob/blob_dockerfile_selectors'; -import './blob/blob_file_dropzone'; -import './blob/blob_gitignore_selector'; -import './blob/blob_gitignore_selectors'; -import './blob/blob_license_selector'; -import './blob/blob_license_selectors'; -import './blob/template_selector'; import './blob/create_branch_dropdown'; import './blob/target_branch_dropdown'; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index d9692269c38..811f90c5a87 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -127,9 +127,6 @@ require('./flash'); if (this.diffViewType() === 'parallel') { this.expandViewContainer(); } - $.scrollTo('.merge-request-details .merge-request-tabs', { - offset: 0, - }); } else if (action === 'pipelines') { if (this.pipelinesLoaded) { return; diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 7298a7d5347..64a68d56962 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -200,7 +200,7 @@ import Cookies from 'js-cookie'; Sidebar.prototype.setSidebarHeight = function() { const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); const $rightSidebar = $('.js-right-sidebar'); - const diff = $navHeight - $('body').scrollTop(); + const diff = $navHeight - $(window).scrollTop(); if (diff > 0) { $rightSidebar.outerHeight($(window).height() - diff); } else { diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index e9e9aafd71a..32067ed1fee 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -1,15 +1,15 @@ /* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ /* global Api */ -require('../blob/template_selector'); +import TemplateSelector from '../blob/template_selectors/template_selector'; ((global) => { - class IssuableTemplateSelector extends gl.TemplateSelector { + class IssuableTemplateSelector extends TemplateSelector { constructor(...args) { super(...args); this.projectPath = this.dropdown.data('project-path'); this.namespacePath = this.dropdown.data('namespace-path'); - this.issuableType = this.wrapper.data('issuable-type'); + this.issuableType = this.$dropdownContainer.data('issuable-type'); this.titleInput = $(`#${this.issuableType}_title`); const initialQuery = { @@ -41,16 +41,16 @@ require('../blob/template_selector'); } setInputValueToTemplateContent() { - // `this.requestFileSuccess` sets the value of the description input field + // `this.setEditorContent` sets the value of the description input field // to the content of the template selected. if (this.titleInput.val() === '') { // If the title has not yet been set, focus the title input and // skip focusing the description input by setting `true` as the - // `skipFocus` option to `requestFileSuccess`. - this.requestFileSuccess(this.currentTemplate, { skipFocus: true }); + // `skipFocus` option to `setEditorContent`. + this.setEditorContent(this.currentTemplate, { skipFocus: true }); this.titleInput.focus(); } else { - this.requestFileSuccess(this.currentTemplate, { skipFocus: false }); + this.setEditorContent(this.currentTemplate, { skipFocus: false }); } return; } diff --git a/app/assets/javascripts/vue_pipelines_index/components/empty_state.js b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js new file mode 100644 index 00000000000..56b4858f4b4 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js @@ -0,0 +1,33 @@ +import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg'; + +export default { + props: { + helpPagePath: { + type: String, + required: true, + }, + }, + + template: ` + <div class="row empty-state"> + <div class="col-xs-12"> + <div class="svg-content"> + ${pipelinesEmptyStateSVG} + </div> + </div> + + <div class="col-xs-12 text-center"> + <div class="text-content"> + <h4>Build with confidence</h4> + <p> + Continous Integration can help catch bugs by running your tests automatically, + while Continuous Deployment can help you deliver code to your product environment. + </p> + <a :href="helpPagePath" class="btn btn-info"> + Get started with Pipelines + </a> + </div> + </div> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.js b/app/assets/javascripts/vue_pipelines_index/components/error_state.js new file mode 100644 index 00000000000..e5d228bddf8 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/error_state.js @@ -0,0 +1,19 @@ +import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg'; + +export default { + template: ` + <div class="row empty-state js-pipelines-error-state"> + <div class="col-xs-12"> + <div class="svg-content"> + ${pipelinesErrorStateSVG} + </div> + </div> + + <div class="col-xs-12 text-center"> + <div class="text-content"> + <h4>The API failed to fetch the pipelines.</h4> + </div> + </div> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js new file mode 100644 index 00000000000..6aa10531034 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js @@ -0,0 +1,52 @@ +export default { + props: { + newPipelinePath: { + type: String, + required: true, + }, + + hasCiEnabled: { + type: Boolean, + required: true, + }, + + helpPagePath: { + type: String, + required: true, + }, + + ciLintPath: { + type: String, + required: true, + }, + + canCreatePipeline: { + type: Boolean, + required: true, + }, + }, + + template: ` + <div class="nav-controls"> + <a + v-if="canCreatePipeline" + :href="newPipelinePath" + class="btn btn-create"> + Run Pipeline + </a> + + <a + v-if="!hasCiEnabled" + :href="helpPagePath" + class="btn btn-info"> + Get started with Pipelines + </a> + + <a + :href="ciLintPath" + class="btn btn-default"> + CI Lint + </a> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js new file mode 100644 index 00000000000..b4480bd98c7 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js @@ -0,0 +1,68 @@ +export default { + props: { + scope: { + type: String, + required: true, + }, + + count: { + type: Object, + required: true, + }, + + paths: { + type: Object, + required: true, + }, + }, + + template: ` + <ul class="nav-links"> + <li + class="js-pipelines-tab-all" + :class="{ 'active': scope === 'all'}"> + <a :href="paths.allPath"> + All + <span class="badge js-totalbuilds-count"> + {{count.all}} + </span> + </a> + </li> + <li class="js-pipelines-tab-pending" + :class="{ 'active': scope === 'pending'}"> + <a :href="paths.pendingPath"> + Pending + <span class="badge"> + {{count.pending}} + </span> + </a> + </li> + <li class="js-pipelines-tab-running" + :class="{ 'active': scope === 'running'}"> + <a :href="paths.runningPath"> + Running + <span class="badge"> + {{count.running}} + </span> + </a> + </li> + <li class="js-pipelines-tab-finished" + :class="{ 'active': scope === 'finished'}"> + <a :href="paths.finishedPath"> + Finished + <span class="badge"> + {{count.finished}} + </span> + </a> + </li> + <li class="js-pipelines-tab-branches" + :class="{ 'active': scope === 'branches'}"> + <a :href="paths.branchesPath">Branches</a> + </li> + <li class="js-pipelines-tab-tags" + :class="{ 'active': scope === 'tags'}"> + <a :href="paths.tagsPath">Tags</a> + </li> + </ul> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js index 104154a715b..48f9181a8d9 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js +++ b/app/assets/javascripts/vue_pipelines_index/index.js @@ -4,23 +4,19 @@ import PipelinesComponent from './pipelines'; import '../vue_shared/vue_resource_interceptor'; $(() => new Vue({ - el: document.querySelector('.vue-pipelines-index'), + el: document.querySelector('#pipelines-list-vue'), data() { - const project = document.querySelector('.pipelines'); const store = new PipelinesStore(); return { store, - endpoint: project.dataset.url, }; }, components: { 'vue-pipelines': PipelinesComponent, }, template: ` - <vue-pipelines - :endpoint="endpoint" - :store="store" /> + <vue-pipelines :store="store" /> `, })); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js index f389e5e4950..48f0e9036e8 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js @@ -1,19 +1,15 @@ -/* global Flash */ -/* eslint-disable no-new */ -import '~/flash'; import Vue from 'vue'; import PipelinesService from './services/pipelines_service'; import eventHub from './event_hub'; import PipelinesTableComponent from '../vue_shared/components/pipelines_table'; import TablePaginationComponent from '../vue_shared/components/table_pagination'; +import EmptyState from './components/empty_state'; +import ErrorState from './components/error_state'; +import NavigationTabs from './components/navigation_tabs'; +import NavigationControls from './components/nav_controls'; export default { props: { - endpoint: { - type: String, - required: true, - }, - store: { type: Object, required: true, @@ -23,17 +19,109 @@ export default { components: { 'gl-pagination': TablePaginationComponent, 'pipelines-table-component': PipelinesTableComponent, + 'empty-state': EmptyState, + 'error-state': ErrorState, + 'navigation-tabs': NavigationTabs, + 'navigation-controls': NavigationControls, }, data() { + const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; + return { + endpoint: pipelinesData.endpoint, + cssClass: pipelinesData.cssClass, + helpPagePath: pipelinesData.helpPagePath, + newPipelinePath: pipelinesData.newPipelinePath, + canCreatePipeline: pipelinesData.canCreatePipeline, + allPath: pipelinesData.allPath, + pendingPath: pipelinesData.pendingPath, + runningPath: pipelinesData.runningPath, + finishedPath: pipelinesData.finishedPath, + branchesPath: pipelinesData.branchesPath, + tagsPath: pipelinesData.tagsPath, + hasCi: pipelinesData.hasCi, + ciLintPath: pipelinesData.ciLintPath, state: this.store.state, apiScope: 'all', pagenum: 1, - pageRequest: false, + isLoading: false, + hasError: false, }; }, + computed: { + canCreatePipelineParsed() { + return gl.utils.convertPermissionToBoolean(this.canCreatePipeline); + }, + + scope() { + const scope = gl.utils.getParameterByName('scope'); + return scope === null ? 'all' : scope; + }, + + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + + /** + * The empty state should only be rendered when the request is made to fetch all pipelines + * and none is returned. + * + * @return {Boolean} + */ + shouldRenderEmptyState() { + return !this.isLoading && + !this.hasError && + !this.state.pipelines.length && + (this.scope === 'all' || this.scope === null); + }, + + /** + * When a specific scope does not have pipelines we render a message. + * + * @return {Boolean} + */ + shouldRenderNoPipelinesMessage() { + return !this.isLoading && + !this.hasError && + !this.state.pipelines.length && + this.scope !== 'all' && + this.scope !== null; + }, + + shouldRenderTable() { + return !this.hasError && + !this.isLoading && this.state.pipelines.length; + }, + + /** + * Pagination should only be rendered when there is more than one page. + * + * @return {Boolean} + */ + shouldRenderPagination() { + return !this.isLoading && + this.state.pipelines.length && + this.state.pageInfo.total > this.state.pageInfo.perPage; + }, + + hasCiEnabled() { + return this.hasCi !== undefined; + }, + + paths() { + return { + allPath: this.allPath, + pendingPath: this.pendingPath, + finishedPath: this.finishedPath, + runningPath: this.runningPath, + branchesPath: this.branchesPath, + tagsPath: this.tagsPath, + }; + }, + }, + created() { this.service = new PipelinesService(this.endpoint); @@ -69,7 +157,7 @@ export default { const pageNumber = gl.utils.getParameterByName('page') || this.pagenum; const scope = gl.utils.getParameterByName('scope') || this.apiScope; - this.pageRequest = true; + this.isLoading = true; return this.service.getPipelines(scope, pageNumber) .then(resp => ({ headers: resp.headers, @@ -81,41 +169,72 @@ export default { this.store.storePagination(response.headers); }) .then(() => { - this.pageRequest = false; + this.isLoading = false; }) .catch(() => { - this.pageRequest = false; - new Flash('An error occurred while fetching the pipelines, please reload the page again.'); + this.hasError = true; + this.isLoading = false; }); }, }, - template: ` - <div> - <div class="pipelines realtime-loading" v-if="pageRequest"> - <i class="fa fa-spinner fa-spin" aria-hidden="true"></i> - </div> - <div class="blank-state blank-state-no-icon" - v-if="!pageRequest && state.pipelines.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - No pipelines to show - </h2> + template: ` + <div :class="cssClass"> + + <div + class="top-area" + v-if="!isLoading && !shouldRenderEmptyState"> + <navigation-tabs + :scope="scope" + :count="state.count" + :paths="paths" /> + + <navigation-controls + :new-pipeline-path="newPipelinePath" + :has-ci-enabled="hasCiEnabled" + :help-page-path="helpPagePath" + :ciLintPath="ciLintPath" + :can-create-pipeline="canCreatePipelineParsed " /> </div> - <div class="table-holder" v-if="!pageRequest && state.pipelines.length"> - <pipelines-table-component - :pipelines="state.pipelines" - :service="service"/> + <div class="content-list pipelines"> + + <div + class="realtime-loading" + v-if="isLoading"> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" /> + </div> + + <empty-state + v-if="shouldRenderEmptyState" + :help-page-path="helpPagePath" /> + + <error-state v-if="shouldRenderErrorState" /> + + <div + class="blank-state blank-state-no-icon" + v-if="shouldRenderNoPipelinesMessage"> + <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> + </div> + + <div + class="table-holder" + v-if="shouldRenderTable"> + + <pipelines-table-component + :pipelines="state.pipelines" + :service="service"/> + </div> + + <gl-pagination + v-if="shouldRenderPagination" + :pagenum="pagenum" + :change="change" + :count="state.count.all" + :pageInfo="state.pageInfo"/> </div> - - <gl-pagination - v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage" - :pagenum="pagenum" - :change="change" - :count="state.count.all" - :pageInfo="state.pageInfo" - > - </gl-pagination> </div> `, }; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index cda46223492..50849e95541 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -68,23 +68,19 @@ } @mixin btn-green { - @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, $white-light); + @include btn-color($green-500, $green-600, $green-600, $green-700, $green-700, $green-800, $white-light); } @mixin btn-blue { - @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, $white-light); -} - -@mixin btn-blue-medium { - @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, $white-light); + @include btn-color($blue-500, $blue-600, $blue-600, $blue-700, $blue-700, $blue-800, $white-light); } @mixin btn-orange { - @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, $white-light); + @include btn-color($orange-500, $orange-600, $orange-600, $orange-700, $orange-700, $orange-800, $white-light); } @mixin btn-red { - @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, $white-light); + @include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white-light); } @mixin btn-gray { @@ -145,11 +141,11 @@ &.btn-new, &.btn-create, &.btn-save { - @include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal); + @include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700); } &.btn-remove { - @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal); + @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } } @@ -157,11 +153,8 @@ @include btn-gray; } - &.btn-primary { - @include btn-blue-medium; - } - &.btn-info, + &.btn-primary, &.btn-register { @include btn-blue; } @@ -171,11 +164,11 @@ } &.btn-close { - @include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal); + @include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700); } &.btn-spam { - @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal); + @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } &.btn-danger, @@ -360,7 +353,7 @@ .btn-inverted { &-secondary { - @include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal); + @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700); } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 186bb9ac616..da5b754aec7 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -195,7 +195,7 @@ text-decoration: none; .badge { - background-color: darken($row-hover, 5%); + background-color: darken($dropdown-link-hover-bg, 5%); } } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 2890fcd088b..432024779fd 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -177,34 +177,34 @@ label { } .gl-field-error { - color: $red-normal; + color: $red-500; } .gl-show-field-errors { .gl-field-success-outline { - border: 1px solid $green-normal; + border: 1px solid $green-600; &:focus { - box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-normal; + box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-600; border: 0 none; } } .gl-field-error-outline { - border: 1px solid $red-normal; + border: 1px solid $red-500; &:focus { - box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error; + box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error; border: 0 none; } } .gl-field-success-message { - color: $green-normal; + color: $green-600; } .gl-field-error-message { - color: $red-normal; + color: $red-500; } .gl-field-hint { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 6660a022260..6f356369476 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -26,7 +26,7 @@ header { padding: 0 16px; z-index: 100; margin-bottom: 0; - height: $header-height; + min-height: $header-height; background-color: $gray-light; border: none; border-bottom: 1px solid $border-color; @@ -85,7 +85,7 @@ header { .navbar-toggle { color: $nav-toggle-gray; - margin: 6px 0; + margin: 7px 0; border-radius: 0; position: absolute; right: -10px; @@ -135,12 +135,14 @@ header { } .header-content { + display: flex; + justify-content: space-between; position: relative; - height: $header-height; + min-height: $header-height; padding-left: 30px; - @media (min-width: $screen-sm-min) { - padding-right: 0; + @media (max-width: $screen-sm-max) { + padding-right: 20px; } .dropdown-menu { @@ -165,8 +167,7 @@ header { } .group-name-toggle { - margin: 0 5px; - vertical-align: sub; + margin: 3px 5px; } .group-title { @@ -177,39 +178,32 @@ header { } } + .title-container { + display: flex; + align-items: flex-start; + flex: 1 1 auto; + padding-top: (($header-height - 19) / 2); + overflow: hidden; + } + .title { position: relative; padding-right: 20px; margin: 0; font-size: 18px; - max-width: 385px; + line-height: 22px; display: inline-block; - line-height: $header-height; font-weight: normal; color: $gl-text-color; - overflow: hidden; - text-overflow: ellipsis; vertical-align: top; white-space: nowrap; - &.initializing { - display: none; - } - - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - max-width: 300px; - } - - @media (max-width: $screen-xs-max) { - max-width: 190px; - } - - @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { - max-width: 428px; + &.wrap { + white-space: normal; } - @media (min-width: $screen-lg-min) { - max-width: 685px; + &.initializing { + opacity: 0; } a { @@ -226,10 +220,10 @@ header { border: transparent; background: transparent; position: absolute; + top: 2px; right: 3px; width: 12px; line-height: 19px; - margin-top: (($header-height - 19) / 2); padding: 0; font-size: 10px; text-align: center; @@ -247,7 +241,7 @@ header { } .navbar-collapse { - float: right; + flex: 0 0 auto; border-top: none; @media (min-width: $screen-md-min) { @@ -255,7 +249,7 @@ header { } @media (max-width: $screen-xs-max) { - float: none; + flex: 1 1 auto; } } } @@ -265,7 +259,7 @@ header { } .impersonation i { - color: $red-normal; + color: $red-500; } } diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index db8d231a82a..87667f39ab8 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -1,8 +1,8 @@ .ci-status-icon-success { - color: $gl-success; + color: $green-500; svg { - fill: $gl-success; + fill: $green-500; } } @@ -17,18 +17,18 @@ .ci-status-icon-pending, .ci-status-icon-failed_with_warnings, .ci-status-icon-success_with_warnings { - color: $gl-warning; + color: $orange-500; svg { - fill: $gl-warning; + fill: $orange-500; } } .ci-status-icon-running { - color: $blue-normal; + color: $blue-400; svg { - fill: $blue-normal; + fill: $blue-400; } } diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 46632f15f35..1537b0744cc 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -33,7 +33,7 @@ } &.status-box-open { - background-color: $green-light; + background-color: $green-500; } &.status-box-expired { diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 4d5a2ca52f0..20c7bc93c28 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -76,28 +76,28 @@ body { /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ .alert-warning { transition: background-color 0.15s, border-color 0.15s; - background-color: lighten($gl-warning, 4%); - border-color: lighten($gl-warning, 4%); + background-color: $orange-500; + border-color: $orange-500; } .alert-warning + .alert-warning { - background-color: $gl-warning; - border-color: $gl-warning; + background-color: $orange-600; + border-color: $orange-600; } .alert-warning + .alert-warning + .alert-warning { - background-color: darken($gl-warning, 4%); - border-color: darken($gl-warning, 4%); + background-color: $orange-700; + border-color: $orange-700; } .alert-warning + .alert-warning + .alert-warning + .alert-warning { - background-color: darken($gl-warning, 8%); - border-color: darken($gl-warning, 8%); + background-color: $orange-800; + border-color: $orange-800; } .alert-warning:only-of-type { - background-color: $gl-warning; - border-color: $gl-warning; + background-color: $orange-500; + border-color: $orange-500; } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 7adbb0a4188..15dc0aa6a52 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -122,7 +122,7 @@ ul.content-list { } .member-group-link { - color: $blue-normal; + color: $blue-600; } .description { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 205d23b1329..5ab505034b6 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -416,14 +416,16 @@ .page-with-layout-nav { .right-sidebar { - top: ($header-height * 2) + 2; + top: ($header-height + 1) * 2; } - .build-sidebar { - top: ($header-height * 3) + 3; + &.page-with-sub-nav { + .right-sidebar { + top: ($header-height + 1) * 3; - &.affix { - top: 0; + &.affix { + top: 0; + } } } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 12a86a64645..e54cc2866a7 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -176,6 +176,10 @@ summary { &.panel-without-border { border: 0; } + + &.panel-without-margin { + margin: 0; + } } .panel-succes .panel-heading, diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 0fc89d5976a..c9f345d24be 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -31,6 +31,7 @@ $border-radius-small: 3px !default; // $text-color: $gl-text-color; $link-color: $gl-link-color; +$link-hover-color: $gl-link-hover-color; //== Typography @@ -73,7 +74,7 @@ $pagination-hover-color: $gl-text-color; $pagination-hover-bg: $row-hover; $pagination-hover-border: $border-color; -$pagination-active-color: $blue-dark; +$pagination-active-color: $blue-600; $pagination-active-bg: $white-light; $pagination-active-border: $border-color; @@ -135,8 +136,8 @@ $well-border: #eee; // //## -$code-color: #c7254e; -$code-bg: #f9f2f4; +$code-color: $red-600; +$code-bg: lighten($red-50, 2%); $kbd-color: $white-light; $kbd-bg: #333; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 82c9c76c4c0..97794a47df8 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -26,27 +26,49 @@ $gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; $gray-darkest: #c4c4c4; -$green-light: #3cbd70; -$green-normal: darken($green-light, $darken-normal-factor); -$green-dark: darken($green-light, $darken-dark-factor); - -$blue-light: #2ea8e5; -$blue-normal: darken($blue-light, $darken-normal-factor); -$blue-dark: darken($blue-light, $darken-dark-factor); - -$blue-medium-light: #3498cb; -$blue-medium: darken($blue-medium-light, $darken-normal-factor); -$blue-medium-dark: darken($blue-medium-light, $darken-dark-factor); - -$blue-light-transparent: rgba(44, 159, 216, 0.05); - -$orange-light: #fc8a51; -$orange-normal: darken($orange-light, $darken-normal-factor); -$orange-dark: darken($orange-light, $darken-dark-factor); - -$red-light: #eb4d5c; -$red-normal: darken($red-light, $darken-normal-factor); -$red-dark: darken($red-light, $darken-dark-factor); +$green-50: #e4f5eb; +$green-100: #bae6cc; +$green-200: #8dd5aa; +$green-300: #5fc488; +$green-400: #3cb76f; +$green-500: #1aaa55; +$green-600: #168f48; +$green-700: #12753a; +$green-800: #0e5a2d; +$green-900: #0a4020; + +$blue-50: #e4eff9; +$blue-100: #bcd7f1; +$blue-200: #8fbce8; +$blue-300: #62a1df; +$blue-400: #418cd8; +$blue-500: #1f78d1; +$blue-600: #1b69b6; +$blue-700: #17599c; +$blue-800: #134a81; +$blue-900: #0f3b66; + +$orange-50: #fff2e1; +$orange-100: #fedfb3; +$orange-200: #feca81; +$orange-300: #fdb44f; +$orange-400: #fca429; +$orange-500: #fc9403; +$orange-600: #de7e00; +$orange-700: #c26700; +$orange-800: #a35100; +$orange-900: #853b00; + +$red-50: #fbe7e4; +$red-100: #f4c4bc; +$red-200: #ed9d90; +$red-300: #e67664; +$red-400: #e05842; +$red-500: #db3b21; +$red-600: #c0341d; +$red-700: #a62d19; +$red-800: #8b2615; +$red-900: #711e11; $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); @@ -58,32 +80,11 @@ $border-gray-light: darken($gray-light, $darken-border-factor); $border-gray-normal: darken($gray-normal, $darken-border-factor); $border-gray-dark: darken($white-normal, $darken-border-factor); -$border-green-extra-light: #9adb84; -$border-green-light: darken($green-light, $darken-border-factor); -$border-green-normal: darken($green-normal, $darken-border-factor); -$border-green-dark: darken($green-dark, $darken-border-factor); - -$border-blue-light: darken($blue-light, $darken-border-factor); -$border-blue-normal: darken($blue-normal, $darken-border-factor); -$border-blue-dark: darken($blue-dark, $darken-border-factor); - -$border-orange-light: darken($orange-light, $darken-border-factor); -$border-orange-normal: darken($orange-normal, $darken-border-factor); -$border-orange-dark: darken($orange-dark, $darken-border-factor); - -$border-red-light: darken($red-light, $darken-border-factor); -$border-red-normal: darken($red-normal, $darken-border-factor); -$border-red-dark: darken($red-dark, $darken-border-factor); - -$warning-message-bg: #fbf2d9; -$warning-message-color: #9e8e60; -$warning-message-border: #f0e2bb; - /* * UI elements */ $border-color: #e5e5e5; -$focus-border-color: #3aabf0; +$focus-border-color: $blue-300; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -96,10 +97,11 @@ $gl-font-size: 14px; $gl-text-color: rgba(0, 0, 0, .85); $gl-text-color-secondary: rgba(0, 0, 0, .55); $gl-text-color-disabled: rgba(0, 0, 0, .35); -$gl-text-green: #4a2; -$gl-text-red: #d12f19; -$gl-text-orange: #d90; -$gl-link-color: #3777b0; +$gl-text-green: $green-600; +$gl-text-red: $red-500; +$gl-text-orange: $orange-600; +$gl-link-color: $blue-600; +$gl-link-hover-color: $blue-800; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; @@ -116,9 +118,9 @@ $list-text-disabled-color: $gl-text-color-disabled; $list-border-light: #eee; $list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; -$list-warning-row-bg: #fcf8e3; -$list-warning-row-border: #faebcc; -$list-warning-row-color: #8a6d3b; +$list-warning-row-bg: $orange-50; +$list-warning-row-border: $orange-100; +$list-warning-row-color: $orange-700; /* * Markdown @@ -145,24 +147,24 @@ $gl-sidebar-padding: 22px; /* * Misc */ -$row-hover: #f7faff; -$row-hover-border: #b2d7ff; +$row-hover: lighten($blue-50, 2%); +$row-hover-border: $blue-100; $progress-color: #c0392b; $header-height: 50px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; $gl-avatar-size: 40px; -$error-exclamation-point: #e62958; +$error-exclamation-point: $red-500; $border-radius-default: 2px; $settings-icon-size: 18px; -$provider-btn-not-active-color: #4688f1; -$link-underline-blue: #4a8bee; -$active-item-blue: #4a8bee; +$provider-btn-not-active-color: $blue-500; +$link-underline-blue: $blue-500; +$active-item-blue: $blue-500; $layout-link-gray: #7e7c7c; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; -$issue-status-expired: #cea61b; +$issue-status-expired: $orange-500; $issuable-sidebar-color: $gl-text-color-secondary; $show-aside-bg: #eee; $show-aside-color: #777; @@ -191,10 +193,10 @@ $user-mention-color: #2fa0bb; $time-color: #999; $project-member-show-color: #aaa; $gl-promo-color: #aaa; -$error-bg: #c67; -$warning-message-bg: #ffffe6; -$warning-message-border: #ed9; -$warning-message-color: #b90; +$error-bg: $red-400; +$warning-message-bg: $orange-50; +$warning-message-border: $orange-100; +$warning-message-color: $orange-700; $control-group-descr-color: #666; $table-permission-x-bg: #d9edf7; $username-color: #666; @@ -209,30 +211,30 @@ $tanuki-yellow: #fca326; /* * State colors: */ -$gl-primary: $blue-normal; -$gl-success: $green-normal; +$gl-primary: $blue-500; +$gl-success: $green-500; $gl-success-focus: rgba($gl-success, .4); -$gl-info: $blue-normal; -$gl-warning: $orange-normal; -$gl-danger: $red-normal; +$gl-info: $blue-500; +$gl-warning: $orange-500; +$gl-danger: $red-500; $gl-btn-active-background: rgba(0, 0, 0, 0.16); $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background; /* * Commit Diff Colors */ -$added: #63c363; -$deleted: #f77; -$line-added: #ecfdf0; -$line-added-dark: #c7f0d2; -$line-removed: #fbe9eb; -$line-removed-dark: #fac5cd; -$line-number-old: #f9d7dc; -$line-number-new: #ddfbe6; -$line-number-select: #fbf2da; -$line-target-blue: #f6faff; -$line-select-yellow: #fcf8e7; -$line-select-yellow-dark: #f0e2bd; +$added: $green-300; +$deleted: $red-300; +$line-added: $green-50; +$line-added-dark: $green-100; +$line-removed: $red-50; +$line-removed-dark: $red-100; +$line-number-old: lighten($red-100, 5%); +$line-number-new: lighten($green-100, 5%); +$line-number-select: lighten($orange-100, 5%); +$line-target-blue: $blue-50; +$line-select-yellow: $orange-50; +$line-select-yellow-dark: $orange-100; $dark-diff-match-bg: rgba(255, 255, 255, 0.3); $dark-diff-match-color: rgba(255, 255, 255, 0.1); $file-mode-changed: #777; @@ -272,7 +274,7 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%); /* * Filtered Search */ -$dropdown-hover-color: #3b86ff; +$dropdown-hover-color: $blue-400; /* * Buttons @@ -295,10 +297,10 @@ $award-emoji-menu-shadow: rgba(0,0,0,.175); /* * Search Box */ -$search-input-border-color: rgba(#4688f1, .8); +$search-input-border-color: rgba($blue-400, .8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-width: 220px; -$location-badge-active-bg: #4f91f8; +$location-badge-active-bg: $blue-500; $location-icon-color: #e7e9ed; /* @@ -360,18 +362,18 @@ $builds-trace-bg: #111; /* * Callout */ -$callout-danger-bg: #fdf7f7; -$callout-danger-border: #eed3d7; -$callout-danger-color: #b94a48; -$callout-warning-bg: #faf8f0; -$callout-warning-border: #faebcc; -$callout-warning-color: #8a6d3b; -$callout-info-bg: #f4f8fa; -$callout-info-border: #bce8f1; -$callout-info-color: #34789a; -$callout-success-bg: #dff0d8; -$callout-success-border: #5ca64d; -$callout-success-color: #3c763d; +$callout-danger-bg: $red-50; +$callout-danger-border: $red-100; +$callout-danger-color: $red-700; +$callout-warning-bg: $orange-50; +$callout-warning-border: $orange-100; +$callout-warning-color: $orange-700; +$callout-info-bg: $blue-50; +$callout-info-border: $blue-100; +$callout-info-color: $blue-700; +$callout-success-bg: $green-50; +$callout-success-border: $green-100; +$callout-success-color: $green-700; /* * Commit Page @@ -391,7 +393,7 @@ $common-green: $gl-text-green; /* * Editor */ -$editor-cancel-color: #b94a48; +$editor-cancel-color: $red-600; /* * Events @@ -415,10 +417,10 @@ $logs-p-color: #333; * Forms */ $input-danger-bg: #f2dede; -$input-danger-border: #d66; +$input-danger-border: $red-400; $input-group-addon-bg: #f7f8fa; $gl-field-focus-shadow: rgba(0, 0, 0, 0.075); -$gl-field-focus-shadow-error: rgba(210, 40, 82, 0.6); +$gl-field-focus-shadow-error: rgba($red-500, 0.6); /* * Help @@ -452,14 +454,14 @@ $label-border-radius: 100px; /* * Lint */ -$lint-incorrect-color: red; -$lint-correct-color: #47a447; +$lint-incorrect-color: $red-500; +$lint-correct-color: $green-500; /* * Login */ $login-brand-holder-color: #888; -$login-devise-error-color: #a00; +$login-devise-error-color: $red-700; /* * Nav @@ -473,33 +475,33 @@ $nav-toggle-gray: #666; */ $notify-details: #777; $notify-footer: #777; -$notify-new-file: #090; -$notify-deleted-file: #b00; +$notify-new-file: $green-600; +$notify-deleted-file: $red-700; /* * Projects */ $project-option-descr-color: #54565b; $project-breadcrumb-color: #999; -$project-private-forks-notice-odd: #2aa056; +$project-private-forks-notice-odd: $green-600; $project-network-controls-color: #888; /* * Runners */ -$runner-state-shared-bg: #32b186; -$runner-state-specific-bg: #3498db; -$runner-status-online-color: $green-normal; +$runner-state-shared-bg: $green-400; +$runner-state-specific-bg: $blue-400; +$runner-status-online-color: $green-600; $runner-status-offline-color: $gray-darkest; -$runner-status-paused-color: $red-normal; +$runner-status-paused-color: $red-500; /* Stat Graph */ $stat-graph-common-bg: #f3f3f3; -$stat-graph-area-fill: #1db34f; +$stat-graph-area-fill: $green-500; $stat-graph-axis-fill: #aaa; -$stat-graph-orange-fill: #f17f49; +$stat-graph-orange-fill: $orange-500; $stat-graph-selection-fill: #333; $stat-graph-selection-stroke: #333; @@ -513,7 +515,7 @@ $select2-drop-shadow2: rgba(31, 37, 50, 0.317647); /* * Todo */ -$todo-alert-blue: #428bca; +$todo-alert-blue: $blue-500; $todo-body-pre-color: #777; $todo-body-border: #ddd; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index f9ee33019cd..575d32b1a23 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -487,9 +487,9 @@ right: -3px; top: -3px; width: 17px; - background-color: $blue-light; + background-color: $blue-500; color: $white-light; - border: 1px solid $border-blue-light; + border: 1px solid $blue-600; font-size: 9px; line-height: 15px; border-radius: 50%; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index a24292a7c8c..969fc75c6eb 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -366,9 +366,3 @@ right: 0; margin-top: -17px; } - -@media (min-width: $screen-md-min) { - .sub-nav.build { - width: calc(100% + #{$gutter_width}); - } -} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index da8410eca66..0dad91ba128 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -142,7 +142,9 @@ border: 1px solid $border-gray-dark; border-radius: $border-radius-default; margin-left: 5px; - line-height: 1; + font-size: $gl-font-size; + line-height: $gl-font-size; + outline: none; &:hover { background-color: darken($gray-light, 10%); diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 84d21e48463..cf45f0af2aa 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -9,6 +9,13 @@ } } +.group-root-path { + max-width: 40vw; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: nowrap; +} + .group-row { .stats { float: right; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index ddc0e78c7b6..c1a9bc4be28 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -520,12 +520,12 @@ &.over_estimate { .meter-fill { - background: $red-light; + background: $red-500; } .time-remaining, .compare-value.spent { - color: $red-light; + color: $red-500; } } } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index b27741a928d..b2f45625a2a 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -69,21 +69,17 @@ ul.related-merge-requests > li { height: 20px; border-radius: 3px; line-height: 18px; - border: 1px solid; &.merged { - border-color: darken($blue-normal, 10%); - background: $blue-normal; + background: $blue-500; } &.closed { - border-color: darken($red-normal, 10%); - background: $red-normal; + background: $red-500; } &.open { - border: 1px solid darken($green-normal, 10%); - background: $green-normal; + background: $green-500; } } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 71ed5b1361a..8249e02b64a 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -85,11 +85,11 @@ } .username .validation-success { - color: $green-normal; + color: $green-600; } .username .validation-error { - color: $red-normal; + color: $red-500; } } } diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 5a9f199fb34..35cefd449f1 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -255,7 +255,7 @@ $colors: ( &.saved { .editor { - border-top: solid 2px $border-green-extra-light; + border-top: solid 2px $green-200; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 7c3172421c1..6630904ec92 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -535,7 +535,7 @@ } .fa-info-circle { - color: $orange-normal; + color: $orange-500; padding-right: 5px; } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 27c47d36818..efbd9365fd9 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -63,7 +63,7 @@ } .remaining-days { - color: $orange-light; + color: $orange-600; } .milestone-stats-and-buttons { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e238f0865f6..a2129722633 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -462,17 +462,18 @@ ul.notes { background: $white-light; padding: 1px 5px; font-size: 12px; - color: $gl-link-color; + color: $blue-500; margin-left: -55px; position: absolute; z-index: 10; width: 23px; height: 23px; - border: 1px solid $border-color; + border: 1px solid $blue-500; transition: transform .1s ease-in-out; &:hover { - background: $gl-info; + background: $blue-500; + border-color: $blue-600; color: $white-light; transform: scale(1.15); } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 772f05feb12..a4fe652b52f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -2,6 +2,7 @@ .realtime-loading { font-size: 40px; text-align: center; + margin: 0 auto; } .stage { @@ -13,6 +14,10 @@ white-space: nowrap; } + .empty-state { + margin: 5% auto 0; + } + .table-holder { width: 100%; @@ -668,51 +673,71 @@ // Dropdown button animation in mini pipeline graph &.ci-status-icon-success { - border-color: $gl-success; - color: $gl-success; + border-color: $green-500; + color: $green-500; &:hover, &:focus, &:active { - background-color: rgba($gl-success, 0.1); - border-color: $gl-success; + background-color: $green-50; + border-color: $green-600; + color: $green-600; + + svg { + fill: $green-600; + } } } &.ci-status-icon-failed { - border-color: $gl-danger; - color: $gl-danger; + border-color: $red-500; + color: $red-500; &:hover, &:focus, &:active { - background-color: rgba($gl-danger, 0.1); - border-color: $gl-danger; + background-color: $red-50; + border-color: $red-600; + color: $red-600; + + svg { + fill: $red-600; + } } } &.ci-status-icon-pending, &.ci-status-icon-success_with_warnings { - border-color: $gl-warning; - color: $gl-warning; + border-color: $orange-500; + color: $orange-500; &:hover, &:focus, &:active { - background-color: rgba($gl-warning, 0.1); - border-color: $gl-warning; + background-color: $orange-50; + border-color: $orange-600; + color: $orange-600; + + svg { + fill: $orange-600; + } } } &.ci-status-icon-running { - border-color: $blue-normal; - color: $blue-normal; + border-color: $blue-400; + color: $blue-400; &:hover, &:focus, &:active { - background-color: rgba($blue-normal, 0.1); - border-color: $blue-normal; + background-color: $blue-50; + border-color: $blue-600; + color: $blue-600; + + svg { + fill: $blue-600; + } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 1a983d8c9ef..703c5fc8869 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -74,7 +74,6 @@ display: inline; a { - color: $blue-dark; text-decoration: none; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index efa47be9a73..949d52cffa2 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -582,54 +582,55 @@ pre.light-well { /* * Projects list rendered on dashboard and user page */ - .projects-list { @include basic-list; + display: flex; + flex-direction: column; .project-row { - border-color: $white-normal; - - .project-full-name { - @include str-truncated; + display: flex; + align-items: center; + } - @media (max-width: $screen-xs-max) { - max-width: 50%; - } - } + h3 { + font-size: $gl-font-size; + } - .controls { - line-height: $list-text-height; + a { + color: $gl-text-color; + } - .badge { - @media (max-width: $screen-xs-max) { - display: none; - } - } + .avatar-container, + .controls { + flex: 0 0 auto; + } - a:hover { - text-decoration: none; - } + .avatar-container { + align-self: flex-start; + } - > span { - margin-left: 10px; - } + .project-details { + min-width: 0; - svg { - position: relative; - top: 2px; - } + p, + .commit-row-message { + @include str-truncated(100%); + margin-bottom: 0; } + } - .description p { - @media (max-width: $screen-xs-max) { - max-width: 50%; - } - } + .controls { + margin-left: auto; } - .bottom { - padding-top: $gl-padding; - padding-bottom: 0; + .ci-status-link { + display: inline-block; + line-height: 17px; + vertical-align: middle; + + &:hover { + text-decoration: none; + } } } diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss index bed6470dbd3..23a9c2ada80 100644 --- a/app/assets/stylesheets/pages/sherlock.scss +++ b/app/assets/stylesheets/pages/sherlock.scss @@ -28,6 +28,6 @@ table .sherlock-code { } .sherlock-line-samples-table .slow { - color: $red-light; + color: $red-500; font-weight: bold; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 6f31d4ed789..4a284247143 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -21,42 +21,41 @@ &.ci-failed, &.ci-failed_with_warnings { - color: $gl-danger; - border-color: $gl-danger; + color: $red-500; + border-color: $red-500; &:not(span):hover { - background-color: rgba($gl-danger, .07); + background-color: $red-50; + color: $red-600; + border-color: $red-600; + + svg { + fill: $red-600; + } } svg { - fill: $gl-danger; + fill: $red-500; } } &.ci-success, &.ci-success_with_warnings { - color: $gl-success; - border-color: $gl-success; + color: $green-600; + border-color: $green-500; &:not(span):hover { - background-color: rgba($gl-success, .07); - } - - svg { - fill: $gl-success; - } - } - - &.ci-info { - color: $gl-info; - border-color: $gl-info; + background-color: $green-50; + color: $green-700; + border-color: $green-600; - &:not(span):hover { - background-color: rgba($gl-info, .07); + svg { + fill: $green-600; + } } svg { - fill: $gl-info; + fill: $green-500; } } @@ -75,28 +74,41 @@ } &.ci-pending { - color: $gl-warning; - border-color: $gl-warning; + color: $orange-600; + border-color: $orange-500; &:not(span):hover { - background-color: rgba($gl-warning, .07); + background-color: $orange-50; + color: $orange-700; + border-color: $orange-600; + + svg { + fill: $orange-600; + } } svg { - fill: $gl-warning; + fill: $orange-500; } } + &.ci-info, &.ci-running { - color: $blue-normal; - border-color: $blue-normal; + color: $blue-500; + border-color: $blue-500; &:not(span):hover { - background-color: rgba($blue-normal, .07); + background-color: $blue-50; + color: $blue-600; + border-color: $blue-600; + + svg { + fill: $blue-600; + } } svg { - fill: $blue-normal; + fill: $blue-500; } } diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 5f0aede4f5e..b071d7f18cd 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -47,6 +47,7 @@ .todo-avatar, .todo-actions { + @include transition(opacity); -webkit-flex: 0 0 auto; flex: 0 0 auto; } @@ -67,21 +68,34 @@ flex: 0 1 100%; min-width: 0; } -} -.todos-list > .todo.todo-pending.done-reversible { - background-color: $gray-light; + &.todo-pending.done-reversible { + background-color: $white-light; - &:hover { - border-color: $border-color; - } + &:hover { + border-color: $white-dark; + background-color: $gray-light; - .title { - font-weight: normal; + .todo-avatar, + .todo-item { + opacity: .6; + } + } + + .todo-avatar, + .todo-item { + opacity: .2; + } + + .btn { + background-color: $gray-light; + } } } .todo-item { + @include transition(opacity); + .todo-title { display: flex; diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 24504685e48..563bcc65bd6 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -95,18 +95,14 @@ class Admin::UsersController < Admin::ApplicationController def create opts = { - force_random_password: true, - password_expires_at: nil + reset_password: true, + skip_confirmation: true } - @user = User.new(user_params.merge(opts)) - @user.created_by_id = current_user.id - @user.generate_password - @user.generate_reset_token - @user.skip_confirmation! + @user = Users::CreateService.new(current_user, user_params.merge(opts)).execute respond_to do |format| - if @user.save + if @user.persisted? format.html { redirect_to [:admin, @user], notice: 'User was successfully created.' } format.json { render json: @user, status: :created, location: @user } else diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 886934a3f67..3f3c90a49ab 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,7 +1,7 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play] - before_action :authorize_update_build!, except: [:index, :show, :status, :raw] + before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace] layout 'project' def index @@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController end def status - render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) + render json: BuildSerializer + .new(project: @project, user: @current_user) + .represent_status(@build) end def erase diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 0d6d9f492c1..d984e6d3918 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -260,4 +260,13 @@ class Projects::IssuesController < Projects::ApplicationController :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [] ) end + + def authenticate_user! + return if current_user + + notice = "Please sign in to create the new issue." + + store_location_for :user, request.fullpath + redirect_to new_user_session_path, notice: notice + end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 677a8a1a73a..1ee96799792 100644..100755 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, - :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues + :ci_status, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] @@ -402,7 +402,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController if params[:ref].present? @ref = params[:ref] - @commit = @repository.commit(@ref) + @commit = @repository.commit("refs/heads/#{@ref}") end render layout: false @@ -413,7 +413,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController if params[:ref].present? @ref = params[:ref] - @commit = @target_project.commit(@ref) + @commit = @target_project.commit("refs/heads/#{@ref}") end render layout: false @@ -473,6 +473,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: response end + def pipeline_status + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .represent_status(@merge_request.head_pipeline) + end + def ci_environments_status environments = begin diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index be52b0fa7cf..5922e686cd0 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -13,11 +13,14 @@ class Projects::MilestonesController < Projects::ApplicationController def index @milestones = case params[:state] - when 'all' then @project.milestones.reorder(due_date: :desc, title: :asc) - when 'closed' then @project.milestones.closed.reorder(due_date: :desc, title: :asc) - else @project.milestones.active.reorder(due_date: :asc, title: :asc) + when 'all' then @project.milestones + when 'closed' then @project.milestones.closed + else @project.milestones.active end + @sort = params[:sort] || 'due_date_asc' + @milestones = @milestones.sort(@sort) + @milestones = @milestones.includes(:project) respond_to do |format| format.html do diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 718d9e86bea..43a1abaa662 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController end end + def status + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .represent_status(@pipeline) + end + def stage @stage = pipeline.stage(params[:stage]) return not_found unless @stage diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index b44f38d4a0c..a49a1f50a81 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,5 +1,4 @@ class RegistrationsController < Devise::RegistrationsController - before_action :signup_enabled? include Recaptcha::Verify def new @@ -21,6 +20,8 @@ class RegistrationsController < Devise::RegistrationsController flash.delete :recaptcha_error render action: 'new' end + rescue Gitlab::Access::AccessDeniedError + redirect_to(new_user_session_path) end def destroy @@ -50,12 +51,6 @@ class RegistrationsController < Devise::RegistrationsController private - def signup_enabled? - unless current_application_settings.signup_enabled? - redirect_to(new_user_session_path) - end - end - def sign_up_params params.require(:user).permit(:username, :email, :email_confirmation, :name, :password) end @@ -65,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController end def resource - @resource ||= User.new(sign_up_params) + @resource ||= Users::CreateService.new(current_user, sign_up_params).build end def devise_mapping diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index fa0e2a5e3d8..e52083f86e4 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder if project? if project - label_ids << project.group.labels if project.group.present? - label_ids << project.labels + if project.group.present? + labels_table = Label.arel_table + + label_ids << Label.where( + labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or( + labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id)) + ) + ) + else + label_ids << project.labels + end end else label_ids << Label.where(group_id: projects.group_ids) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 5053b937c02..bd3f51fc658 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -89,10 +89,12 @@ module MilestonesHelper content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" } content.slice!("about ") content << " remaining" + content.html_safe elsif milestone.start_date && milestone.start_date.past? days = milestone.elapsed_days content = content_tag(:strong, days) content << " #{'day'.pluralize(days)} elapsed" + content.html_safe end end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 2e3a15bc1b9..7f656b8caae 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -6,7 +6,13 @@ module NamespacesHelper def namespaces_options(selected = :current_user, display_path: false, extra_group: nil) groups = current_user.owned_groups + current_user.masters_groups - groups << extra_group if extra_group && !Group.exists?(name: extra_group.name) + unless extra_group.nil? || extra_group.is_a?(Group) + extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group' + end + + if extra_group && extra_group.is_a?(Group) && (!Group.exists?(name: extra_group.name) || Ability.allowed?(current_user, :read_group, extra_group)) + groups |= [extra_group] + end users = [current_user.namespace] diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index a8f167cbff2..991fd949b94 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -31,7 +31,11 @@ module NavHelper end def layout_nav_class - "page-with-layout-nav" if defined?(nav) && nav + class_name = '' + class_name << " page-with-layout-nav" if defined?(nav) && nav + class_name << " page-with-sub-nav" if content_for?(:sub_nav) + + class_name end def nav_control_class diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 959ee310867..5c89cbea3fc 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -2,6 +2,7 @@ module SortingHelper def sort_options_hash { sort_value_name => sort_title_name, + sort_value_name_desc => sort_title_name_desc, sort_value_recently_updated => sort_title_recently_updated, sort_value_oldest_updated => sort_title_oldest_updated, sort_value_recently_created => sort_title_recently_created, @@ -50,6 +51,17 @@ module SortingHelper } end + def milestone_sort_options_hash + { + sort_value_name => sort_title_name_asc, + sort_value_name_desc => sort_title_name_desc, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_due_date_later => sort_title_due_date_later, + sort_value_start_date_soon => sort_title_start_date_soon, + sort_value_start_date_later => sort_title_start_date_later, + } + end + def sort_title_priority 'Priority' end @@ -90,6 +102,14 @@ module SortingHelper 'Due later' end + def sort_title_start_date_soon + 'Start soon' + end + + def sort_title_start_date_later + 'Start later' + end + def sort_title_name 'Name' end @@ -202,6 +222,14 @@ module SortingHelper 'due_date_desc' end + def sort_value_start_date_soon + 'start_date_asc' + end + + def sort_value_start_date_later + 'start_date_desc' + end + def sort_value_name 'name_asc' end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 00000000000..9c623c9ba7c --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,7 @@ +module UsersHelper + def user_link(user) + link_to(user.name, user_path(user), + title: user.email, + class: 'has-tooltip commit-committer-link') + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4759829a15c..5ff83944d8c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -154,8 +154,10 @@ class MergeRequest < ActiveRecord::Base # # Returns an ActiveRecord::Relation. def self.in_projects(relation) - source = where(source_project_id: relation).select(:id) - target = where(target_project_id: relation).select(:id) + # unscoping unnecessary conditions that'll be applied + # when executing `where("merge_requests.id IN (#{union.to_sql})")` + source = unscoped.where(source_project_id: relation).select(:id) + target = unscoped.where(target_project_id: relation).select(:id) union = Gitlab::SQL::Union.new([source, target]) where("merge_requests.id IN (#{union.to_sql})") diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c0deb59ec4c..e85d5709624 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -107,6 +107,21 @@ class Milestone < ActiveRecord::Base end end + def self.sort(method) + case method.to_s + when 'due_date_asc' + reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) + when 'due_date_desc' + reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC')) + when 'start_date_asc' + reorder(Gitlab::Database.nulls_last_order('start_date', 'ASC')) + when 'start_date_desc' + reorder(Gitlab::Database.nulls_last_order('start_date', 'DESC')) + else + order_by(method) + end + end + ## # Returns the String necessary to reference this Milestone in Markdown # diff --git a/app/models/project.rb b/app/models/project.rb index 04641dd58a0..f1bba56d32c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -314,20 +314,15 @@ class Project < ActiveRecord::Base ntable = Namespace.arel_table pattern = "%#{query}%" - projects = select(:id).where( + # unscoping unnecessary conditions that'll be applied + # when executing `where("projects.id IN (#{union.to_sql})")` + projects = unscoped.select(:id).where( ptable[:path].matches(pattern). or(ptable[:name].matches(pattern)). or(ptable[:description].matches(pattern)) ) - # We explicitly remove any eager loading clauses as they're: - # - # 1. Not needed by this query - # 2. Combined with .joins(:namespace) lead to all columns from the - # projects & namespaces tables being selected, leading to a SQL error - # due to the columns of all UNION'd queries no longer being the same. - namespaces = select(:id). - except(:includes). + namespaces = unscoped.select(:id). joins(:namespace). where(ntable[:name].matches(pattern)) diff --git a/app/models/user.rb b/app/models/user.rb index 5d19d873f43..612066654dc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -115,7 +115,9 @@ class User < ActiveRecord::Base validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true validates :bio, length: { maximum: 255 }, allow_blank: true - validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } + validates :projects_limit, + presence: true, + numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE } validates :username, namespace: true, presence: true, @@ -126,10 +128,9 @@ class User < ActiveRecord::Base validate :unique_email, if: ->(user) { user.email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? } + validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } - before_validation :generate_password, on: :create - before_validation :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } before_validation :sanitize_attrs before_validation :set_notification_email, if: ->(user) { user.email_changed? } before_validation :set_public_email, if: ->(user) { user.public_email_changed? } @@ -139,8 +140,6 @@ class User < ActiveRecord::Base before_save :ensure_external_user_rights after_save :ensure_namespace_correct after_initialize :set_projects_limit - before_create :check_confirmation_email - after_create :post_create_hook after_destroy :post_destroy_hook # User's Layout preference @@ -384,10 +383,8 @@ class User < ActiveRecord::Base "#{self.class.reference_prefix}#{username}" end - def generate_password - if force_random_password - self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min) - end + def skip_confirmation=(bool) + skip_confirmation! if bool end def generate_reset_token @@ -399,10 +396,6 @@ class User < ActiveRecord::Base @reset_token end - def check_confirmation_email - skip_confirmation! unless current_application_settings.send_user_confirmation_email - end - def recently_sent_password_reset? reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago end @@ -797,12 +790,6 @@ class User < ActiveRecord::Base end end - def post_create_hook - log_info("User \"#{name}\" (#{email}) was created") - notification_service.new_user(self, @reset_token) if created_by_id - system_hook_service.execute_hooks_for(self, :create) - end - def post_destroy_hook log_info("User \"#{name}\" (#{email}) was removed") system_hook_service.execute_hooks_for(self, :destroy) diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb index 5bcbe285052..fadd6c5c597 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -18,10 +18,17 @@ class BuildEntity < Grape::Entity expose :created_at expose :updated_at + expose :detailed_status, as: :status, with: StatusEntity private + alias_method :build, :object + def path_to(route, build) send("#{route}_path", build.project.namespace, build.project, build) end + + def detailed_status + build.detailed_status(request.user) + end end diff --git a/app/serializers/build_serializer.rb b/app/serializers/build_serializer.rb new file mode 100644 index 00000000000..79b67001199 --- /dev/null +++ b/app/serializers/build_serializer.rb @@ -0,0 +1,8 @@ +class BuildSerializer < BaseSerializer + entity BuildEntity + + def represent_status(resource) + data = represent(resource, { only: [:status] }) + data.fetch(:status, {}) + end +end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 61f0f11d7d2..3f16dd66d54 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity end expose :details do - expose :status do |pipeline, options| - StatusEntity.represent( - pipeline.detailed_status(request.user), - options) - end - + expose :detailed_status, as: :status, with: StatusEntity expose :duration expose :finished_at expose :stages, using: StageEntity @@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity pipeline.cancelable? && can?(request.user, :update_pipeline, pipeline) end + + def detailed_status + pipeline.detailed_status(request.user) + end end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index ab2d3d5a3ec..7829df9fada 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer super(resource, opts) end end + + def represent_status(resource) + return {} unless resource.present? + + data = represent(resource, { only: [{ details: [:status] }] }) + data.dig(:details, :status) || {} + end end diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index 47066bebfb1..dfd9d1584a1 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -1,7 +1,7 @@ class StatusEntity < Grape::Entity include RequestAwareEntity - expose :icon, :text, :label, :group + expose :icon, :favicon, :text, :label, :group expose :has_details?, as: :has_details expose :details_path diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index 574561adc4c..f72ddbf690c 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -7,14 +7,14 @@ module Ci raise Gitlab::Access::AccessDeniedError end - pipeline.builds.failed_or_canceled.find_each do |build| + pipeline.builds.latest.failed_or_canceled.find_each do |build| next unless build.retryable? Ci::RetryBuildService.new(project, current_user) .reprocess(build) end - pipeline.builds.skipped.find_each do |skipped| + pipeline.builds.latest.skipped.find_each do |skipped| retry_optimistic_lock(skipped) { |build| build.process } end diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb new file mode 100644 index 00000000000..f4f0b80f30a --- /dev/null +++ b/app/services/users/create_service.rb @@ -0,0 +1,110 @@ +module Users + # Service for creating a new user. + class CreateService < BaseService + def initialize(current_user, params = {}) + @current_user = current_user + @params = params.dup + end + + def build + raise Gitlab::Access::AccessDeniedError unless can_create_user? + + user = User.new(build_user_params) + + if current_user&.is_admin? + if params[:reset_password] + @reset_token = user.generate_reset_token + params[:force_random_password] = true + end + + if params[:force_random_password] + random_password = Devise.friendly_token.first(Devise.password_length.min) + user.password = user.password_confirmation = random_password + end + end + + identity_attrs = params.slice(:extern_uid, :provider) + + if identity_attrs.any? + user.identities.build(identity_attrs) + end + + user + end + + def execute + user = build + + if user.save + log_info("User \"#{user.name}\" (#{user.email}) was created") + notification_service.new_user(user, @reset_token) if @reset_token + system_hook_service.execute_hooks_for(user, :create) + end + + user + end + + private + + def can_create_user? + (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.is_admin? + end + + # Allowed params for creating a user (admins only) + def admin_create_params + [ + :access_level, + :admin, + :avatar, + :bio, + :can_create_group, + :color_scheme_id, + :email, + :external, + :force_random_password, + :hide_no_password, + :hide_no_ssh_key, + :key_id, + :linkedin, + :name, + :password, + :password_expires_at, + :projects_limit, + :remember_me, + :skip_confirmation, + :skype, + :theme_id, + :twitter, + :username, + :website_url + ] + end + + # Allowed params for user signup + def signup_params + [ + :email, + :email_confirmation, + :name, + :password, + :username + ] + end + + def build_user_params + if current_user&.is_admin? + user_params = params.slice(*admin_create_params) + user_params[:created_by_id] = current_user.id + + if params[:reset_password] + user_params.merge!(force_random_password: true, password_expires_at: nil) + end + else + user_params = params.slice(*signup_params) + user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email + end + + user_params + end + end +end diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 7855239dfe5..794aaec89bd 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -2,7 +2,7 @@ %legend Access .form-group = f.label :projects_limit, class: 'control-label' - .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control' + .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control' .form-group = f.label :can_create_group, class: 'control-label' diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index e31fa5fbe95..52d6ebd8a14 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -68,12 +68,11 @@ = link_to todos_filter_path(sort: sort_value_oldest_created) do = sort_title_oldest_created - .js-todos-all - if @todos.any? .js-todos-list-container .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } } - .panel.panel-default.panel-small.panel-without-border + .panel.panel-default.panel-without-border.panel-without-margin %ul.content-list.todos-list = render @todos = paginate @todos, theme: "gitlab" diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 56f463572bb..f630f1effdc 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -17,24 +17,3 @@ = link_to filter_projects_path(visibility_level: level) do = visibility_level_icon(level) = visibility_level_label(level) - -- if @tags.present? - .dropdown - %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } - = icon('tags') - %span.light Tags: - - if params[:tag].present? - = params[:tag] - - else - Any - = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to filter_projects_path(tag: nil) do - Any - - - @tags.each do |tag| - %li{ class: active_when(tag.name == params[:tag]) || 'light' } - = link_to filter_projects_path(tag: tag.name) do - = icon('tag') - = tag.name diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index a35a918d501..b7df11681d3 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -3,8 +3,9 @@ .layout-nav .container-fluid = render "layouts/nav/#{nav}" - .content-wrapper{ class: "#{layout_nav_class}" } + - if content_for?(:sub_nav) = yield :sub_nav + .content-wrapper{ class: layout_nav_class } .alert-wrapper = render "layouts/broadcast" = render "layouts/flash" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 7ddee0e5244..7bf4bc70f7c 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -15,6 +15,13 @@ %span.sr-only Toggle navigation = icon('ellipsis-v') + .header-logo + = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do + = brand_header_logo + + .title-container + %h1.title{ class: ('initializing' if @has_group_title) }= title + .navbar-collapse.collapse %ul.nav.navbar-nav %li.hidden-sm.hidden-xs @@ -63,12 +70,6 @@ %div = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' - .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do - = brand_header_logo - - %h1.title{ class: ('initializing' if @has_group_title) }= title - = yield :header_content = render 'shared/outdated_browser' diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 4924c73cf8e..e14885f264b 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -5,7 +5,7 @@ %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title= title .modal-body - = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do + = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal', data: { method: method } do .dropzone .dropzone-previews.blob-upload-dropzone-previews %p.dz-message.light @@ -24,8 +24,5 @@ .inline.prepend-left-10 = commit_in_fork_help - -:javascript - gl.utils.disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file'); - new BlobFileDropzone($('.js-upload-blob-form'), '#{method}'); - new NewCommitForm($('.js-upload-blob-form')) +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('blob') diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 3bcddcb37f1..afe0b5dba45 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -2,7 +2,7 @@ - page_title "Edit", @blob.path, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_bundle_tag('blob_edit') + = page_specific_javascript_bundle_tag('blob') = render "projects/commits/head" %div{ class: container_class } diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index e0ce8cc9601..4c449e040ee 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,7 +1,7 @@ - page_title "New File", @path.presence, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_bundle_tag('blob_edit') + = page_specific_javascript_bundle_tag('blob') %h3.page-title New File diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 78720d88e4e..b597c7f7a12 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -1,6 +1,6 @@ - builds = @build.pipeline.builds.to_a -%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } } +%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "153", "spy" => "affix" } } .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default Job %strong ##{@build.id} @@ -137,3 +137,6 @@ = build.id - if build.retried? %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } + +:javascript + new Sidebar(); diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 307010edb58..d5fe771613c 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "#{@build.name} (##{@build.id})", "Jobs" -= render "projects/pipelines/head", build_subnav: true += render "projects/pipelines/head" %div{ class: container_class } .build-page diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index da5a676274f..09e3a775d1c 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -1,6 +1,7 @@ - disable_initialization = local_assigns.fetch(:disable_initialization, false) #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization, endpoint: endpoint, + "help-page-path" => help_page_path('ci/quick_start/README'), } } - content_for :page_specific_javascripts do diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index ef0dd0eda3c..c8363087d6a 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -16,7 +16,7 @@ .col-sm-6 .nav-controls - = link_to @environment.external_url, class: 'btn btn-default' do + = link_to @environment.external_url, class: 'btn btn-default', target: '_blank', rel: 'noopener noreferrer nofollow' do = icon('external-link') = render 'projects/deployments/actions', deployment: @environment.last_deployment diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index f0a23bec5e7..e632fc681cf 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -1,7 +1,8 @@ - case @status - when :success + - remove_source_branch = params[:should_remove_source_branch] == '1' || @merge_request.remove_source_branch? :plain - merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'}); + merge_request_widget.mergeInProgress(#{remove_source_branch}); - when :merge_when_pipeline_succeeds :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}"); diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 918f5d161bb..b6340a00b29 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -7,6 +7,7 @@ = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) .nav-controls + = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do New Milestone diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index d129da943f8..34a1214a350 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -23,7 +23,7 @@ - if current_user.can_select_namespace? .input-group-addon = root_url - = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} + = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1} - else .input-group-addon.static-namespace diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index a5acb7ac4a5..b02fef638ff 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -1,7 +1,7 @@ = content_for :sub_nav do .scrolling-tabs-container.sub-nav-scroll = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) } + .nav-links.sub-nav.scrolling-tabs %ul{ class: (container_class) } - if project_nav_tab? :pipelines = nav_link(path: 'pipelines#index', controller: :pipelines) do diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 0605af4fcd3..4be9a1371ec 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,10 +1,12 @@ .page-content-header .header-main-content = render 'ci/status/badge', status: @pipeline.detailed_status(current_user) - %strong Pipeline ##{@commit.pipelines.last.id} - triggered #{time_ago_with_tooltip(@commit.authored_date)} by - = author_avatar(@commit, size: 24) - = commit_author_link(@commit) + %strong Pipeline ##{@pipeline.id} + triggered #{time_ago_with_tooltip(@pipeline.created_at)} + - if @pipeline.user + by + = user_avatar(user: @pipeline.user, size: 24) + = user_link(@pipeline.user) .header-action-buttons - if can?(current_user, :update_pipeline, @pipeline.project) - if @pipeline.retryable? diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 5d59ce06612..3d73284699f 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -2,53 +2,19 @@ - page_title "Pipelines" = render "projects/pipelines/head" -%div{ class: container_class } - .top-area - %ul.nav-links - %li.js-pipelines-tab-all{ class: active_when(@scope.nil?) }> - = link_to project_pipelines_path(@project) do - All - %span.badge.js-totalbuilds-count - = number_with_delimiter(@pipelines_count) - - %li.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }> - = link_to project_pipelines_path(@project, scope: :pending) do - Pending - %span.badge - = number_with_delimiter(@pending_count) - - %li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }> - = link_to project_pipelines_path(@project, scope: :running) do - Running - %span.badge.js-running-count - = number_with_delimiter(@running_count) - - %li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }> - = link_to project_pipelines_path(@project, scope: :finished) do - Finished - %span.badge - = number_with_delimiter(@finished_count) - - %li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }> - = link_to project_pipelines_path(@project, scope: :branches) do - Branches - - %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }> - = link_to project_pipelines_path(@project, scope: :tags) do - Tags - - .nav-controls - - if can? current_user, :create_pipeline, @project - = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do - Run pipeline - - - unless @repository.gitlab_ci_yml - = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' - - = link_to ci_lint_path, class: 'btn btn-default' do - %span CI Lint - .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } - .vue-pipelines-index +#pipelines-list-vue{ data: { endpoint: namespace_project_pipelines_path(@project.namespace, @project, format: :json), + "css-class" => container_class, + "help-page-path" => help_page_path('ci/quick_start/README'), + "new-pipeline-path" => new_namespace_project_pipeline_path(@project.namespace, @project), + "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, + "all-path" => project_pipelines_path(@project), + "pending-path" => project_pipelines_path(@project, scope: :pending), + "running-path" => project_pipelines_path(@project, scope: :running), + "finished-path" => project_pipelines_path(@project, scope: :finished), + "branches-path" => project_pipelines_path(@project, scope: :branches), + "tags-path" => project_pipelines_path(@project, scope: :tags), + "has-ci" => @repository.gitlab_ci_yml, + "ci-lint-path" => ci_lint_path } } = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('vue_pipelines') diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 8c582f747b3..713b758727e 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -1,4 +1,4 @@ -%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } +%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "spy" => "affix" } } .block.wiki-sidebar-header.append-bottom-default %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } = icon('angle-double-right') @@ -19,3 +19,6 @@ More Pages = render 'projects/wikis/new' + +:javascript + new Sidebar(); diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index c2d9ac87b20..7974eb67f0f 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -1,4 +1,6 @@ - parent = Group.find_by(id: params[:parent_id] || @group.parent_id) +- group_path = root_url +- group_path << parent.full_path + '/' if parent - if @group.persisted? .form-group = f.label :name, class: 'control-label' do @@ -11,7 +13,7 @@ Group path .col-sm-10 .input-group.gl-field-error-anchor - .input-group-addon + .group-root-path.input-group-addon.has-tooltip{ title: group_path, :'data-placement' => 'bottom' } %span>= root_url - if parent %strong= parent.full_path + '/' diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 57a0eaa919e..db2ac1e1d12 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -4,10 +4,10 @@ Open %span.badge= counts[:opened] %li{ class: milestone_class_for_state(params[:state], 'closed') }> - = link_to milestones_filter_path(state: 'closed') do + = link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc') do Closed %span.badge= counts[:closed] %li{ class: milestone_class_for_state(params[:state], 'all') }> - = link_to milestones_filter_path(state: 'all') do + = link_to milestones_filter_path(state: 'all', sort: 'due_date_desc') do All %span.badge= counts[:all] diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml new file mode 100644 index 00000000000..9b2f2fdcc93 --- /dev/null +++ b/app/views/shared/_milestones_sort_dropdown.html.haml @@ -0,0 +1,22 @@ +.dropdown.inline.prepend-left-10 + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } } + %span.light + - if @sort.present? + = milestone_sort_options_hash[@sort] + - else + = sort_title_due_date_soon + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort + %li + = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do + = sort_title_due_date_soon + = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do + = sort_title_due_date_later + = link_to page_filter_path(sort: sort_value_start_date_soon, label: true) do + = sort_title_start_date_soon + = link_to page_filter_path(sort: sort_value_start_date_later, label: true) do + = sort_title_start_date_later + = link_to page_filter_path(sort: sort_value_name, label: true) do + = sort_title_name_asc + = link_to page_filter_path(sort: sort_value_name_desc, label: true) do + = sort_title_name_desc diff --git a/app/views/shared/empty_states/icons/_pipelines_empty.svg b/app/views/shared/empty_states/icons/_pipelines_empty.svg new file mode 100644 index 00000000000..8119d5bebe0 --- /dev/null +++ b/app/views/shared/empty_states/icons/_pipelines_empty.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd" transform="translate(0-3)"><g transform="translate(0 105)"><g fill="#e5e5e5"><rect width="78" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="m152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2"/></g><g transform="translate(0 4)"><path fill="#98d7b2" fill-rule="nonzero" d="m19 38c-10.493 0-19-8.507-19-19 0-10.493 8.507-19 19-19 10.493 0 19 8.507 19 19 0 10.493-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path fill="#31af64" d="m17.07 21.02l-2.829-2.829c-.786-.786-2.047-.781-2.828 0-.786.786-.781 2.047 0 2.828l4.243 4.243c.392.392.902.587 1.412.588.512.002 1.021-.193 1.41-.582l7.79-7.79c.777-.777.775-2.042-.006-2.823-.786-.786-2.045-.784-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a" transform="translate(102)"><path fill-rule="nonzero" d="m24 47.5c-12.979 0-23.5-10.521-23.5-23.5 0-12.979 10.521-23.5 23.5-23.5 12.979 0 23.5 10.521 23.5 23.5 0 12.979-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5 0-10.217-8.283-18.5-18.5-18.5-10.217 0-18.5 8.283-18.5 18.5 0 10.217 8.283 18.5 18.5 18.5"/><path d="m28.24 24l2.833-2.833c1.167-1.167 1.167-3.067-.004-4.239-1.169-1.169-3.069-1.173-4.239-.004l-2.833 2.833-2.833-2.833c-1.167-1.167-3.067-1.167-4.239.004-1.169 1.169-1.173 3.069-.004 4.239l2.833 2.833-2.833 2.833c-1.167 1.167-1.167 3.067.004 4.239 1.169 1.169 3.069 1.173 4.239.004l2.833-2.833 2.833 2.833c1.167 1.167 3.067 1.167 4.239-.004 1.169-1.169 1.173-3.069.004-4.239l-2.833-2.833"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="m236 37c-7.732 0-14-6.268-14-14 0-7.732 6.268-14 14-14 7.732 0 14 6.268 14 14 0 7.732-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10 0-5.523-4.477-10-10-10-5.523 0-10 4.477-10 10 0 5.523 4.477 10 10 10"/></g><g transform="translate(69 3)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 11.99v60.02c0 4.413 3.583 7.99 8 7.99h89.991c4.419 0 8-3.579 8-7.99v-60.02c0-4.413-3.583-7.99-8-7.99h-89.991c-4.419 0-8 3.579-8 7.99m-4 0c0-6.622 5.378-11.99 12-11.99h89.991c6.629 0 12 5.367 12 11.99v60.02c0 6.622-5.378 11.99-12 11.99h-89.991c-6.629 0-12-5.367-12-11.99v-60.02m52.874 80.3l-13.253-15.292h34.76l-13.253 15.292c-2.237 2.582-6.01 2.585-8.253 0m3.02-2.62c.644.743 1.564.743 2.207 0l7.516-8.673h-17.24l7.516 8.673"/><rect width="18" height="6" x="15" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="39" y="39" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="33" y="55" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="39" y="23" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="57" y="55" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="15" y="55" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="81" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="15" y="39" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="57" y="23" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="69" y="23" rx="3"/><rect width="6" height="6" x="75" y="39" rx="3"/></g><rect width="6" height="6" x="63" y="39" fill="#e52c5a" rx="3"/></g><g transform="matrix(.70711-.70711.70711.70711 84.34 52.5)"><path fill="#6b4fbb" fill-rule="nonzero" d="m28.02 67.48c-15.927-2.825-28.02-16.738-28.02-33.476 0-18.778 15.222-34 34-34 18.778 0 34 15.222 34 34 0 16.738-12.1 30.652-28.02 33.476.015.173.023.347.023.524v21.999c0 3.314-2.693 6-6 6-3.314 0-6-2.682-6-6v-21.999c0-.177.008-.351.023-.524m5.977-7.476c14.359 0 26-11.641 26-26 0-14.359-11.641-26-26-26-14.359 0-26 11.641-26 26 0 14.359 11.641 26 26 26"/><path fill="#fff" fill-opacity=".3" stroke="#6b4fbb" stroke-width="8" d="m31 71c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30" transform="matrix(.86603.5-.5.86603 26.663-17.507)"/></g></g></svg>
\ No newline at end of file diff --git a/app/views/shared/empty_states/icons/_pipelines_failed.svg b/app/views/shared/empty_states/icons/_pipelines_failed.svg new file mode 100644 index 00000000000..7dbabf7e4ef --- /dev/null +++ b/app/views/shared/empty_states/icons/_pipelines_failed.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 249" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m260.03 114h23.972v-.013c19.972-.53 36-16.887 36-36.987 0-20.435-16.565-37-37-37-.993 0-1.977.039-2.95.116-4.95-14.605-18.773-25.12-35.05-25.12-5.464 0-10.652 1.185-15.32 3.311-6.649-9.841-17.909-16.311-30.68-16.311-20.435 0-37 16.565-37 37 0 .701.019 1.397.058 2.088-16.11 3.999-28.06 18.561-28.06 35.912 0 20.435 16.565 37 37 37 .324 0 .646-.004.968-.012"/><ellipse id="2" cx="41" cy="41" rx="41" ry="41"/><mask id="1" width="186" height="112" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="82" height="82" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="matrix(.86603.5-.5.86603 228.11 137.43)"><path stroke="#b5a7dd" stroke-width="4" d="m.445.161c15.89 10.636 34.998 16.839 55.55 16.839"/><g transform="translate(56 4)"><path fill="#fb722e" d="m16 8c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2m0 10c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2"/><path fill="#fde5d8" fill-rule="nonzero" d="m4 22h6c3.315 0 6-2.685 6-5.997v-6.01c0-3.315-2.684-5.997-6-5.997h-6v18m-4-18.992c0-1.661 1.343-3.01 2.994-3.01h7.01c5.523 0 10 4.47 10 9.997v6.01c0 5.521-4.476 9.997-10 9.997h-7.01c-1.654 0-2.994-1.343-2.994-3.01v-19.984"/></g></g><g fill-rule="nonzero" transform="translate(257)"><path fill="#e5e5e5" d="m3.597 18.747c5.611-9.09 15.519-14.747 26.403-14.747 17.12 0 31 13.879 31 31 0 7.02-2.34 13.685-6.58 19.1l3.149 2.466c4.786-6.111 7.431-13.639 7.431-21.565 0-19.33-15.67-35-35-35-12.286 0-23.476 6.384-29.808 16.647l3.404 2.1"/><g transform="matrix(.96593.25882-.25882.96593 15.98 9.578)"><path fill="#b5a7dd" d="m12.426 11.592l-2.142 1.768-3.664-2.116c-.186-.107-.43-.042-.543.154l-1.229 2.129c-.116.2-.052.438.138.547l3.658 2.112-.455 2.735c-.109.657-.165 1.327-.165 2.01 0 .678.055 1.348.165 2.01l.455 2.735-3.658 2.112c-.186.107-.251.351-.138.547l1.229 2.129c.116.2.353.264.543.154l3.664-2.116 2.142 1.768c1.036.855 2.205 1.533 3.462 2l2.6.972v4.225c0 .215.179.393.405.393h2.458c.231 0 .405-.174.405-.393v-4.225l2.6-.972c1.257-.47 2.426-1.147 3.462-2l2.142-1.768 3.664 2.116c.186.107.43.042.543-.154l1.229-2.129c.116-.2.052-.438-.138-.547l-3.658-2.112.455-2.735c.109-.657.165-1.327.165-2.01 0-.678-.055-1.348-.165-2.01l-.455-2.735 3.658-2.112c.186-.107.251-.351.138-.547l-1.229-2.129c-.116-.2-.353-.264-.543-.154l-3.664 2.116-2.142-1.768c-1.036-.855-2.205-1.533-3.462-2l-2.6-.972v-4.225c0-.215-.179-.393-.405-.393h-2.458c-.231 0-.405.174-.405.393v4.225l-2.6.972c-1.257.47-2.426 1.147-3.462 2m2.062-5.749v-1.45c0-2.426 1.963-4.393 4.405-4.393h2.458c2.433 0 4.405 1.967 4.405 4.393v1.45c1.689.631 3.243 1.538 4.608 2.665l1.259-.727c2.101-1.213 4.786-.497 6.01 1.618l1.229 2.129c1.216 2.107.499 4.798-1.602 6.01l-1.257.726c.144.866.219 1.755.219 2.662 0 .907-.075 1.796-.219 2.662l1.257.726c2.101 1.213 2.823 3.896 1.602 6.01l-1.229 2.129c-1.216 2.107-3.906 2.832-6.01 1.618l-1.259-.727c-1.365 1.127-2.92 2.034-4.608 2.665v1.45c0 2.426-1.963 4.393-4.405 4.393h-2.458c-2.433 0-4.405-1.967-4.405-4.393v-1.45c-1.689-.631-3.243-1.538-4.608-2.665l-1.259.727c-2.101 1.213-4.786.497-6.01-1.618l-1.229-2.129c-1.216-2.107-.499-4.798 1.602-6.01l1.257-.726c-.144-.866-.219-1.755-.219-2.662 0-.907.075-1.796.219-2.662l-1.257-.726c-2.101-1.213-2.823-3.896-1.602-6.01l1.229-2.129c1.216-2.107 3.906-2.832 6.01-1.618l1.259.727c1.365-1.127 2.92-2.034 4.608-2.665"/><path fill="#6b4fbb" d="m20.12 23.366c1.347 0 2.439-1.092 2.439-2.439 0-1.347-1.092-2.439-2.439-2.439-1.347 0-2.439 1.092-2.439 2.439 0 1.347 1.092 2.439 2.439 2.439m0 4c-3.556 0-6.439-2.883-6.439-6.439 0-3.556 2.883-6.439 6.439-6.439 3.556 0 6.439 2.883 6.439 6.439 0 3.556-2.883 6.439-6.439 6.439"/></g></g><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#1)" stroke-linejoin="round" xlink:href="#0"/><g transform="translate(175 58)"><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="m41 78c20.435 0 37-16.565 37-37 0-20.435-16.565-37-37-37-20.435 0-37 16.565-37 37 0 20.435 16.565 37 37 37m0 4c-22.644 0-41-18.356-41-41 0-22.644 18.356-41 41-41 22.644 0 41 18.356 41 41 0 22.644-18.356 41-41 41"/><g transform="matrix(.96593.25882-.25882.96593 23.581 9.415)"><path fill="#b5a7dd" d="m14.821 13.655l-2.142 1.768-3.933-2.271c-.72-.416-1.634-.171-2.046.543l-1.507 2.61c-.409.708-.161 1.631.553 2.043l3.926 2.267-.455 2.735c-.145.869-.218 1.754-.218 2.65 0 .896.073 1.782.218 2.65l.455 2.735-3.926 2.267c-.72.416-.965 1.329-.553 2.043l1.507 2.61c.409.708 1.332.955 2.046.543l3.933-2.271 2.142 1.768c1.369 1.131 2.916 2.027 4.579 2.648l2.6.972v4.534c0 .831.669 1.5 1.493 1.5h3.01c.817 0 1.493-.676 1.493-1.5v-4.534l2.6-.972c1.663-.621 3.21-1.518 4.579-2.648l2.142-1.768 3.933 2.271c.72.416 1.634.171 2.046-.543l1.507-2.61c.409-.708.161-1.631-.553-2.043l-3.926-2.267.455-2.735c.145-.869.218-1.754.218-2.65 0-.896-.073-1.782-.218-2.65l-.455-2.735 3.926-2.267c.72-.416.965-1.329.553-2.043l-1.507-2.61c-.409-.708-1.332-.955-2.046-.543l-3.933 2.271-2.142-1.768c-1.369-1.131-2.916-2.027-4.579-2.648l-2.6-.972v-4.534c0-.831-.669-1.5-1.493-1.5h-3.01c-.817 0-1.493.676-1.493 1.5v4.534l-2.6.972c-1.663.621-3.21 1.518-4.579 2.648m3.179-6.395v-1.759c0-3.038 2.471-5.5 5.493-5.5h3.01c3.034 0 5.493 2.46 5.493 5.5v1.759c2.098.784 4.03 1.91 5.725 3.311l1.528-.882c2.631-1.519 5.999-.61 7.51 2.01l1.507 2.61c1.517 2.627.616 5.987-2.02 7.507l-1.525.881c.179 1.076.272 2.18.272 3.307 0 1.127-.093 2.231-.272 3.307l1.525.881c2.631 1.519 3.528 4.89 2.02 7.507l-1.507 2.61c-1.517 2.627-4.877 3.527-7.51 2.01l-1.528-.882c-1.696 1.401-3.627 2.527-5.725 3.311v1.759c0 3.038-2.471 5.5-5.493 5.5h-3.01c-3.034 0-5.493-2.46-5.493-5.5v-1.759c-2.098-.784-4.03-1.91-5.725-3.311l-1.528.882c-2.631 1.519-5.999.61-7.51-2.01l-1.507-2.61c-1.517-2.627-.616-5.987 2.02-7.507l1.525-.881c-.179-1.076-.272-2.18-.272-3.307 0-1.127.093-2.231.272-3.307l-1.525-.881c-2.631-1.519-3.528-4.89-2.02-7.507l1.507-2.61c1.517-2.627 4.877-3.527 7.51-2.01l1.528.882c1.696-1.401 3.627-2.527 5.725-3.311"/><path fill="#6b4fbb" d="m25 30c2.209 0 4-1.791 4-4 0-2.209-1.791-4-4-4-2.209 0-4 1.791-4 4 0 2.209 1.791 4 4 4m0 4c-4.418 0-8-3.582-8-8 0-4.418 3.582-8 8-8 4.418 0 8 3.582 8 8 0 4.418-3.582 8-8 8"/></g></g></g><g transform="translate(140 161)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 8.541v30.01c0 2.202 1.793 3.995 4 3.995h20c2.209 0 4-1.789 4-3.995v-30.01c0-2.202-1.793-3.995-4-3.995h-20c-2.209 0-4 1.789-4 3.995m-4 0c0-4.416 3.583-7.995 8-7.995h20c4.416 0 8 3.584 8 7.995v30.01c0 4.416-3.583 7.995-8 7.995h-20c-4.416 0-8-3.584-8-7.995v-30.01"/><g fill="#fb722e"><rect width="4" height="11" x="10" y="18.545" rx="2"/><rect width="4" height="11" x="21" y="18.545" rx="2"/></g></g><path fill="#e5e5e5" fill-rule="nonzero" d="m445.16 245.34c-16.874-11.778-110.62-20.336-222.14-20.336-111.61 0-205.4 8.571-222.18 20.364-.904.635-1.121 1.883-.486 2.786.635.904 1.883 1.121 2.786.486 15.756-11.07 109.46-19.636 219.88-19.636 110.34 0 203.99 8.55 219.85 19.617.906.632 2.153.41 2.785-.495.632-.906.41-2.153-.495-2.785"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index a95020a9be8..09f946f1d88 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -17,7 +17,7 @@ .stats %span = icon('bookmark') - = number_with_delimiter(group.projects.count) + = number_with_delimiter(group.projects.non_archived.count) %span = icon('users') diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 25a4aec0a38..884bd3ca9ca 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -3,7 +3,7 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('issuable') -%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } +%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index c57282c5742..c0699b13719 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -10,7 +10,7 @@ .js-projects-list-holder - if projects.any? - %ul.projects-list.content-list + %ul.projects-list - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) ? 'hide' : nil = render "shared/projects/project", project: project, skip_namespace: skip_namespace, diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index df21857e1ad..059aeebaf34 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -10,44 +10,44 @@ %li.project-row{ class: css_class } = cache(cache_key) do + - if avatar + .avatar-container.s40 + - if use_creator_avatar + = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' + - else + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + .project-details + %h3.prepend-top-0.append-bottom-0 + = link_to project_path(project), class: dom_class(project) do + %span.project-full-name + %span.namespace-name + - if project.namespace && !skip_namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name + + - if show_last_commit_as_description + .description.prepend-top-5 + = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit), + class: "commit-row-message" + - elsif project.description.present? + .description.prepend-top-5 + = markdown_field(project, :description) + .controls - if project.archived - %span.label.label-warning archived + %span.prepend-left-10.label.label-warning archived - if project.pipeline_status.has_status? - %span + %span.prepend-left-10 = render_project_pipeline_status(project.pipeline_status) - if forks - %span + %span.prepend-left-10 = icon('code-fork') = number_with_delimiter(project.forks_count) - if stars - %span + %span.prepend-left-10 = icon('star') = number_with_delimiter(project.star_count) - %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } + %span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } = visibility_level_icon(project.visibility_level, fw: true) - - .title - = link_to project_path(project), class: dom_class(project) do - - if avatar - .dash-project-avatar - .avatar-container.s40 - - if use_creator_avatar - = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' - - else - = project_icon(project, alt: '', class: 'avatar project-avatar s40') - %span.project-full-name - %span.namespace-name - - if project.namespace && !skip_namespace - = project.namespace.human_name - \/ - %span.project-name.filter-title - = project.name - - - if show_last_commit_as_description - .description - = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit), - class: "commit-row-message" - - elsif project.description.present? - .description - = markdown_field(project, :description) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index dc9a3b0d0df..601187455b3 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -13,7 +13,7 @@ .cover-block.user-cover-block .cover-controls - if @user == current_user - = link_to profile_path, class: 'btn btn-gray' do + = link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do = icon('pencil') - elsif current_user - if @user.abuse_report @@ -24,7 +24,7 @@ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('exclamation-circle') - = link_to user_path(@user, rss_url_options), class: 'btn btn-gray' do + = link_to user_path(@user, rss_url_options), class: 'btn btn-gray has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do = icon('rss') - if current_user && current_user.admin? = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area', @@ -44,7 +44,7 @@ %span.middle-dot-divider @#{@user.username} %span.middle-dot-divider - Member since #{@user.created_at.to_s(:medium)} + Member since #{@user.created_at.to_date.to_s(:long)} .cover-desc - unless @user.public_email.blank? @@ -97,7 +97,8 @@ Snippets %div{ class: container_class } - .user-callout{ 'callout-svg' => custom_icon('icon_customization') } + - if @user == current_user + .user-callout{ 'callout-svg' => custom_icon('icon_customization') } .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs diff --git a/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml b/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml new file mode 100644 index 00000000000..953009213df --- /dev/null +++ b/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Expose CI/CD status API endpoints with Gitlab::Ci::Status facility on pipeline, + job and merge request for favicon +merge_request: 9561 +author: dosuken123 diff --git a/changelogs/unreleased/22850-404-when-requesting-build-trace.yml b/changelogs/unreleased/22850-404-when-requesting-build-trace.yml new file mode 100644 index 00000000000..6b442130d9b --- /dev/null +++ b/changelogs/unreleased/22850-404-when-requesting-build-trace.yml @@ -0,0 +1,4 @@ +--- +title: Resolve "404 when requesting build trace" +merge_request: 9759 +author: dosuken123 diff --git a/changelogs/unreleased/23862-fix-group-project-count.yml b/changelogs/unreleased/23862-fix-group-project-count.yml new file mode 100644 index 00000000000..7b2e9f9bfa6 --- /dev/null +++ b/changelogs/unreleased/23862-fix-group-project-count.yml @@ -0,0 +1,4 @@ +--- +title: Adding non_archived scope for counting projects +merge_request: 8305 +author: Naveen Kumar diff --git a/changelogs/unreleased/25188-polyfill-es-symbol.yml b/changelogs/unreleased/25188-polyfill-es-symbol.yml new file mode 100644 index 00000000000..d0cf36b9ec6 --- /dev/null +++ b/changelogs/unreleased/25188-polyfill-es-symbol.yml @@ -0,0 +1,4 @@ +--- +title: Add ECMAScript polyfills for Symbol and Array.find +merge_request: 10120 +author: diff --git a/changelogs/unreleased/27574-pipelines-empty-state.yml b/changelogs/unreleased/27574-pipelines-empty-state.yml new file mode 100644 index 00000000000..c26ea97205f --- /dev/null +++ b/changelogs/unreleased/27574-pipelines-empty-state.yml @@ -0,0 +1,4 @@ +--- +title: Adds empty and error state to pipelines +merge_request: +author: diff --git a/changelogs/unreleased/27878-new-service-for-creating-user.yml b/changelogs/unreleased/27878-new-service-for-creating-user.yml new file mode 100644 index 00000000000..c07f0cef8db --- /dev/null +++ b/changelogs/unreleased/27878-new-service-for-creating-user.yml @@ -0,0 +1,4 @@ +--- +title: Implement user create service +merge_request: 9220 +author: George Andrinopoulos diff --git a/changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml b/changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml new file mode 100644 index 00000000000..40fd8dacc82 --- /dev/null +++ b/changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml @@ -0,0 +1,4 @@ +--- +title: Allow admin to view all namespaces +merge_request: +author: George Andrinopoulos diff --git a/changelogs/unreleased/28614-harmonious-color-palette.yml b/changelogs/unreleased/28614-harmonious-color-palette.yml new file mode 100644 index 00000000000..b436e7129a4 --- /dev/null +++ b/changelogs/unreleased/28614-harmonious-color-palette.yml @@ -0,0 +1,4 @@ +--- +title: Update color palette to a more harmonious and consistent one. +merge_request: 10154 +author: diff --git a/changelogs/unreleased/28634-todos-margin.yml b/changelogs/unreleased/28634-todos-margin.yml new file mode 100644 index 00000000000..f4221ce4350 --- /dev/null +++ b/changelogs/unreleased/28634-todos-margin.yml @@ -0,0 +1,4 @@ +--- +title: Remove extra margin at bottom of todos page +merge_request: +author: diff --git a/changelogs/unreleased/29116-maxint-error.yml b/changelogs/unreleased/29116-maxint-error.yml new file mode 100644 index 00000000000..06e976617d5 --- /dev/null +++ b/changelogs/unreleased/29116-maxint-error.yml @@ -0,0 +1,4 @@ +--- +title: Fix projects_limit RangeError on user create +merge_request: +author: Alexander Randa diff --git a/changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml b/changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml new file mode 100644 index 00000000000..d1da96096f8 --- /dev/null +++ b/changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml @@ -0,0 +1,4 @@ +--- +title: Remove forced scroll into view when switching to Changes MR tab +merge_request: +author: diff --git a/changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml b/changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml new file mode 100644 index 00000000000..754d471c7d7 --- /dev/null +++ b/changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml @@ -0,0 +1,4 @@ +--- +title: Add tooltip and accessibility for profile cover buttons +merge_request: 10182 +author: diff --git a/changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml b/changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml new file mode 100644 index 00000000000..651c299ac66 --- /dev/null +++ b/changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml @@ -0,0 +1,4 @@ +--- +title: Fix escaped html appearing in milestone page +merge_request: 10224 +author: diff --git a/changelogs/unreleased/4195-add-sorting-to-project-milestones.yml b/changelogs/unreleased/4195-add-sorting-to-project-milestones.yml new file mode 100644 index 00000000000..d4104dfa772 --- /dev/null +++ b/changelogs/unreleased/4195-add-sorting-to-project-milestones.yml @@ -0,0 +1,4 @@ +--- +title: Add dropdown sort to project milestones +merge_request: +author: George Andrinopoulos diff --git a/changelogs/unreleased/add-issue-modal-loading-indicator.yml b/changelogs/unreleased/add-issue-modal-loading-indicator.yml new file mode 100644 index 00000000000..5398399c018 --- /dev/null +++ b/changelogs/unreleased/add-issue-modal-loading-indicator.yml @@ -0,0 +1,4 @@ +--- +title: Shows loading icon in issue boards modal when changing filters +merge_request: +author: diff --git a/changelogs/unreleased/fix-ci-api-regression-for-after-script.yml b/changelogs/unreleased/fix-ci-api-regression-for-after-script.yml new file mode 100644 index 00000000000..cdd7d1e6945 --- /dev/null +++ b/changelogs/unreleased/fix-ci-api-regression-for-after-script.yml @@ -0,0 +1,4 @@ +--- +title: Fix after_script processing for Runners APIv4 +merge_request: 10185 +author: diff --git a/changelogs/unreleased/fix-gb-pipeline-retry-only-latest-jobs.yml b/changelogs/unreleased/fix-gb-pipeline-retry-only-latest-jobs.yml new file mode 100644 index 00000000000..c14679be70f --- /dev/null +++ b/changelogs/unreleased/fix-gb-pipeline-retry-only-latest-jobs.yml @@ -0,0 +1,5 @@ +--- +title: Fix bug that caused jobs that already had been retried to be retried again + when retrying a pipeline +merge_request: 10249 +author: diff --git a/changelogs/unreleased/fix-issue-23237.yml b/changelogs/unreleased/fix-issue-23237.yml new file mode 100644 index 00000000000..ed0ffc0684d --- /dev/null +++ b/changelogs/unreleased/fix-issue-23237.yml @@ -0,0 +1,4 @@ +--- +title: "Fixes an issue in the new merge request form, where a tag would be selected instead of a branch when they have the same names" +merge_request: 9535 +author: Weiqing Chu diff --git a/changelogs/unreleased/labels-finder-optimize-project.yml b/changelogs/unreleased/labels-finder-optimize-project.yml new file mode 100644 index 00000000000..0d2e82e1efe --- /dev/null +++ b/changelogs/unreleased/labels-finder-optimize-project.yml @@ -0,0 +1,4 @@ +--- +title: Optimize labels finder query when searching for a project with a group +merge_request: +author: mhasbini diff --git a/changelogs/unreleased/projects-list-line-breaks.yml b/changelogs/unreleased/projects-list-line-breaks.yml new file mode 100644 index 00000000000..179d7081293 --- /dev/null +++ b/changelogs/unreleased/projects-list-line-breaks.yml @@ -0,0 +1,4 @@ +--- +title: Fixed projects list lines breaking +merge_request: +author: diff --git a/changelogs/unreleased/sh-remove-tags-from-explore.yml b/changelogs/unreleased/sh-remove-tags-from-explore.yml new file mode 100644 index 00000000000..b76ec89a006 --- /dev/null +++ b/changelogs/unreleased/sh-remove-tags-from-explore.yml @@ -0,0 +1,4 @@ +--- +title: Remove Tags filter from Projects Explore dropdown +merge_request: +author: diff --git a/changelogs/unreleased/slow-search-changelog.yml b/changelogs/unreleased/slow-search-changelog.yml new file mode 100644 index 00000000000..d50cf1f94cd --- /dev/null +++ b/changelogs/unreleased/slow-search-changelog.yml @@ -0,0 +1,4 @@ +--- +title: Simplify search queries for projects and merge requests +merge_request: 10053 +author: mhasbini diff --git a/changelogs/unreleased/tc-pipeline-show-trigger-date.yml b/changelogs/unreleased/tc-pipeline-show-trigger-date.yml new file mode 100644 index 00000000000..4de784d98f3 --- /dev/null +++ b/changelogs/unreleased/tc-pipeline-show-trigger-date.yml @@ -0,0 +1,4 @@ +--- +title: Show correct user & creation time in heading of the pipeline page +merge_request: 9936 +author: diff --git a/changelogs/unreleased/user-callout-showing-on-all-profiles.yml b/changelogs/unreleased/user-callout-showing-on-all-profiles.yml new file mode 100644 index 00000000000..b8eb5a149b7 --- /dev/null +++ b/changelogs/unreleased/user-callout-showing-on-all-profiles.yml @@ -0,0 +1,4 @@ +--- +title: User callout only shows on current users profile +merge_request: +author: diff --git a/changelogs/unreleased/user-profile-join-date.yml b/changelogs/unreleased/user-profile-join-date.yml new file mode 100644 index 00000000000..f9d78b0dc3e --- /dev/null +++ b/changelogs/unreleased/user-profile-join-date.yml @@ -0,0 +1,4 @@ +--- +title: Removed the hours & minutes from the users start date on their profile +merge_request: +author: diff --git a/config/routes/project.rb b/config/routes/project.rb index 44b8ae7aedd..823e0614aeb 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -102,6 +102,7 @@ constraints(ProjectUrlConstrainer.new) do get :merge_widget_refresh post :cancel_merge_when_pipeline_succeeds get :ci_status + get :pipeline_status get :ci_environments_status post :toggle_subscription post :remove_wip @@ -152,6 +153,7 @@ constraints(ProjectUrlConstrainer.new) do post :cancel post :retry get :builds + get :status end end diff --git a/config/webpack.config.js b/config/webpack.config.js index 3cf94b9b435..0859c8416c8 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -21,7 +21,7 @@ var config = { common_vue: ['vue', './vue_shared/common_vue.js'], common_d3: ['d3'], main: './main.js', - blob_edit: './blob_edit/blob_edit_bundle.js', + blob: './blob_edit/blob_bundle.js', boards: './boards/boards_bundle.js', simulate_drag: './test_utils/simulate_drag.js', cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb index 1e2abea5254..69dd15b8b4e 100644 --- a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb +++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb @@ -17,7 +17,7 @@ class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration end remove_column :ci_pipelines, :push_data, :text - remove_column :ci_builds, :job_id, :integer + remove_column :ci_builds, :job_id, :integer if column_exists?(:ci_builds, :job_id) remove_column :ci_builds, :deploy, :boolean end diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md index 0a08591c3ce..cf3aca106e9 100644 --- a/doc/administration/high_availability/database.md +++ b/doc/administration/high_availability/database.md @@ -13,6 +13,8 @@ Database Service (RDS) that runs PostgreSQL. If you use a cloud-managed service, or provide your own PostgreSQL: +1. Setup PostgreSQL according to the + [database requirements document](doc/install/requirements.md#database). 1. Set up a `gitlab` username with a password of your choice. The `gitlab` user needs privileges to create the `gitlabhq_production` database. 1. Configure the GitLab application servers with the appropriate details. diff --git a/doc/api/README.md b/doc/api/README.md index 58d090b8f5e..e627b6f2ee8 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -74,6 +74,12 @@ returned with status code `401`: } ``` +### Session Cookie + +When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is +set. The API will use this cookie for authentication if it is present, but using +the API to generate a new session cookie is currently not supported. + ### Private Tokens You need to pass a `private_token` parameter via query string or header. If passed as a @@ -113,65 +119,25 @@ moment – `read_user` and `api` – the groundwork has been laid to add more sc At any time you can revoke any personal access token by just clicking **Revoke**. -### Session Cookie +### Impersonation tokens -When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is -set. The API will use this cookie for authentication if it is present, but using -the API to generate a new session cookie is currently not supported. +> [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions. -## Basic Usage +Impersonation tokens are a type of [Personal Access Token](#personal-access-tokens) +that can only be created by an admin for a specific user. -API requests should be prefixed with `api` and the API version. The API version -is defined in [`lib/api.rb`][lib-api-url]. +They are a better alternative to using the user's password/private token +or using the [Sudo](#sudo) feature which also requires the admin's password +or private token, since the password/token can change over time. Impersonation +tokens are a great fit if you want to build applications or tools which +authenticate with the API as a specific user. -Example of a valid API request: +For more information about the usage please refer to the +[users API](users.md#retrieve-user-impersonation-tokens) docs. -```shell -GET https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK -``` - -Example of a valid API request using cURL and authentication via header: - -```shell -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" -``` - -The API uses JSON to serialize data. You don't need to specify `.json` at the -end of an API URL. - -## Status codes - -The API is designed to return different status codes according to context and -action. This way, if a request results in an error, the caller is able to get -insight into what went wrong. - -The following table gives an overview of how the API functions generally behave. - -| Request type | Description | -| ------------ | ----------- | -| `GET` | Access one or more resources and return the result as JSON. | -| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. | -| `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. | -| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind this, is that the user is not really interested if the resource existed before or not. | - -The following table shows the possible return codes for API requests. - -| Return values | Description | -| ------------- | ----------- | -| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | -| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. | -| `201 Created` | The `POST` request was successful and the resource is returned as JSON. | -| `304 Not Modified` | Indicates that the resource has not been modified since the last request. | -| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. | -| `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. | -| `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. | -| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. | -| `405 Method Not Allowed` | The request is not supported. | -| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. | -| `422 Unprocessable` | The entity could not be processed. | -| `500 Server Error` | While handling the request something went wrong server-side. | +### Sudo -## Sudo +> Needs admin permissions. All API requests support performing an API call as if you were another user, provided your private token is from an administrator account. You need to pass @@ -202,7 +168,7 @@ returned with status code `404`: Example of a valid API call and a request using cURL with sudo request, providing a username: -```shell +``` GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username ``` @@ -213,7 +179,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "h Example of a valid API call and a request using cURL with sudo request, providing an ID: -```shell +``` GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 ``` @@ -221,13 +187,57 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects" ``` -## Impersonation Tokens +## Basic Usage + +API requests should be prefixed with `api` and the API version. The API version +is defined in [`lib/api.rb`][lib-api-url]. + +Example of a valid API request: + +``` +GET /projects?private_token=9koXpg98eAheJpvBs5tK +``` + +Example of a valid API request using cURL and authentication via header: + +```shell +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" +``` + +The API uses JSON to serialize data. You don't need to specify `.json` at the +end of an API URL. + +## Status codes + +The API is designed to return different status codes according to context and +action. This way, if a request results in an error, the caller is able to get +insight into what went wrong. + +The following table gives an overview of how the API functions generally behave. + +| Request type | Description | +| ------------ | ----------- | +| `GET` | Access one or more resources and return the result as JSON. | +| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. | +| `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. | +| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind this, is that the user is not really interested if the resource existed before or not. | -Impersonation Tokens are a type of Personal Access Token that can only be created by an admin for a specific user. These can be used by automated tools -to authenticate with the API as a specific user, as a better alternative to using the user's password or private token directly, which may change over time, -and to using the [Sudo](#sudo) feature, which requires the tool to know an admin's password or private token, which can change over time as well and are extremely powerful. +The following table shows the possible return codes for API requests. -For more information about the usage please refer to the [Users](users.md) page +| Return values | Description | +| ------------- | ----------- | +| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | +| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. | +| `201 Created` | The `POST` request was successful and the resource is returned as JSON. | +| `304 Not Modified` | Indicates that the resource has not been modified since the last request. | +| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. | +| `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. | +| `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. | +| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. | +| `405 Method Not Allowed` | The request is not supported. | +| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. | +| `422 Unprocessable` | The entity could not be processed. | +| `500 Server Error` | While handling the request something went wrong server-side. | ## Pagination @@ -307,14 +317,14 @@ For example, an issue might have `id: 46` and `iid: 5`. That means that if you want to get an issue via the API you should use the `id`: -```bash +``` GET /projects/42/issues/:id ``` On the other hand, if you want to create a link to a web page you should use the `iid`: -```bash +``` GET /projects/42/issues/:iid ``` @@ -398,3 +408,4 @@ programming languages. Visit the [GitLab website] for a complete list. [lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb [ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749 [ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951 +[ce-9099]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9099 diff --git a/doc/api/users.md b/doc/api/users.md index 14b5c6c713e..2ada4d09c84 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -828,10 +828,12 @@ Example response: ] ``` -## Retrieve user impersonation tokens +## Get all impersonation tokens of a user -It retrieves every impersonation token of the user. Note that only administrators can do this. -This function takes pagination parameters `page` and `per_page` to restrict the list of impersonation tokens. +> Requires admin permissions. + +It retrieves every impersonation token of the user. Use the pagination +parameters `page` and `per_page` to restrict the list of impersonation tokens. ``` GET /users/:user_id/impersonation_tokens @@ -842,27 +844,50 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `user_id` | integer | yes | The ID of the user | -| `state` | string | no | filter tokens based on state (all, active, inactive) | +| `state` | string | no | filter tokens based on state (`all`, `active`, `inactive`) | + +``` +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/impersonation_tokens +``` Example response: + ```json [ - { - "id": 1, - "name": "mytoken", - "revoked": false, - "expires_at": "2017-01-04", - "scopes": ['api'], - "active": true, - "impersonation": true, - "token": "9koXpg98eAheJpvBs5tK" - } + { + "active" : true, + "token" : "EsMo-vhKfXGwX9RKrwiy", + "scopes" : [ + "api" + ], + "revoked" : false, + "name" : "mytoken", + "id" : 2, + "created_at" : "2017-03-17T17:18:09.283Z", + "impersonation" : true, + "expires_at" : "2017-04-04" + }, + { + "active" : false, + "scopes" : [ + "read_user" + ], + "revoked" : true, + "token" : "ZcZRpLeEuQRprkRjYydY", + "name" : "mytoken2", + "created_at" : "2017-03-17T17:19:28.697Z", + "id" : 3, + "impersonation" : true, + "expires_at" : "2017-04-14" + } ] ``` -## Show a user's impersonation token +## Get an impersonation token of a user -It shows a user's impersonation token. Note that only administrators can do this. +> Requires admin permissions. + +It shows a user's impersonation token. ``` GET /users/:user_id/impersonation_tokens/:impersonation_token_id @@ -875,7 +900,31 @@ Parameters: | `user_id` | integer | yes | The ID of the user | | `impersonation_token_id` | integer | yes | The ID of the impersonation token | -## Create a impersonation token +``` +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/impersonation_tokens/2 +``` + +Example response: + +```json +{ + "active" : true, + "token" : "EsMo-vhKfXGwX9RKrwiy", + "scopes" : [ + "api" + ], + "revoked" : false, + "name" : "mytoken", + "id" : 2, + "created_at" : "2017-03-17T17:18:09.283Z", + "impersonation" : true, + "expires_at" : "2017-04-04" +} +``` + +## Create an impersonation token + +> Requires admin permissions. It creates a new impersonation token. Note that only administrators can do this. You are only able to create impersonation tokens to impersonate the user and perform @@ -891,32 +940,46 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `user_id` | integer | yes | The ID of the user | -| `name` | string | yes | The name of the impersonation token | -| `expires_at` | date | no | The expiration date of the impersonation token | -| `scopes` | array | no | The array of scopes of the impersonation token (api, read_user) | +| `name` | string | yes | The name of the impersonation token | +| `expires_at` | date | no | The expiration date of the impersonation token in ISO format (`YYYY-MM-DD`)| +| `scopes` | array | yes | The array of scopes of the impersonation token (`api`, `read_user`) | + +``` +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=mytoken" --data "expires_at=2017-04-04" --data "scopes[]=api" https://gitlab.example.com/api/v4/users/42/impersonation_tokens +``` Example response: + ```json { - "id": 1, - "name": "mytoken", - "revoked": false, - "expires_at": "2017-01-04", - "scopes": ['api'], - "active": true, - "impersonation": true, - "token": "9koXpg98eAheJpvBs5tK" + "id" : 2, + "revoked" : false, + "scopes" : [ + "api" + ], + "token" : "EsMo-vhKfXGwX9RKrwiy", + "active" : true, + "impersonation" : true, + "name" : "mytoken", + "created_at" : "2017-03-17T17:18:09.283Z", + "expires_at" : "2017-04-04" } ``` ## Revoke an impersonation token -It revokes an impersonation token. Note that only administrators can revoke impersonation tokens. +> Requires admin permissions. + +It revokes an impersonation token. ``` DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id ``` +``` +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/impersonation_tokens/1 +``` + Parameters: | Attribute | Type | Required | Description | diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 7f4426ee85d..8e002fe0022 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -80,3 +80,4 @@ Below are the changes made between V3 and V4. - `GET /projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath` - Moved `GET /projects/:id/repository/commits/:sha/blob?file_path=:file_path` and `GET /projects/:id/repository/blobs/:sha?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path/raw?ref=:sha` - `GET /projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency +- `confirm` parameter for `POST /users` has been deprecated in favor of `skip_confirmation` parameter diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index ccaee33dc92..e380282f910 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -4,6 +4,7 @@ - [Introduced][ci-229] in GitLab CE 7.14. - GitLab 8.12 has a completely redesigned job permissions system. Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers). +- GitLab 9.0 introduced a trigger ownership to solve permission problems. Triggers can be used to force a rebuild of a specific `ref` (branch or tag) with an API call. @@ -21,13 +22,30 @@ overview of the time the triggers were last used. ![Triggers page overview](img/triggers_page.png) +## Take ownership + +Each created trigger when run will impersonate their associated user including +their access to projects and their project permissions. + +You can take ownership of existing triggers by clicking *Take ownership*. +From now on the trigger will be run as you. + +## Legacy triggers + +Old triggers, created before 9.0 will be marked as Legacy. Triggers with +the legacy label do not have an associated user and only have access +to the current project. + +Legacy trigger are considered deprecated and will be removed +with one of the future versions of GitLab. + ## Revoke a trigger You can revoke a trigger any time by going at your project's **Settings > Triggers** and hitting the **Revoke** button. The action is irreversible. -## Trigger a job +## Trigger a pipeline > **Note**: Valid refs are only the branches and tags. If you pass a commit SHA as a ref, @@ -63,7 +81,7 @@ below. See the [Examples](#examples) section for more details on how to actually trigger a rebuild. -## Trigger a job from webhook +## Trigger a pipeline from webhook > Introduced in GitLab 8.14. @@ -117,7 +135,7 @@ curl --request POST \ "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline?token=TOKEN&ref=master" ``` -### Triggering a job within `.gitlab-ci.yml` +### Triggering a pipeline within `.gitlab-ci.yml` You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that you have two projects, A and B, and you want to trigger a rebuild on the `master` diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png Binary files differindex 8ebf68d0384..eafd8519a23 100644 --- a/doc/ci/triggers/img/triggers_page.png +++ b/doc/ci/triggers/img/triggers_page.png diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index b559d132590..55610a7b014 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -87,12 +87,12 @@ your Runners in the most possible secure way, by avoiding the following: By using an insecure GitLab Runner configuration, you allow the rogue developers to steal the tokens of other jobs. -## job triggers +## Pipeline triggers -[job triggers][triggers] do not support the new permission model. -They continue to use the old authentication mechanism where the CI job -can access only its own sources. We plan to remove that limitation in one of -the upcoming releases. +Since 9.0 [pipelnie triggers][triggers] do support the new permission model. +The new triggers do impersonate their associated user including their access +to projects and their project permissions. To migrate trigger to use new permisison +model use **Take ownership**. ## Before GitLab 8.12 diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index 1762d5bdf95..e55dc2913c3 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -45,7 +45,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps end step 'I have new comment with emoji added' do - expect(page).to have_selector ".emoji[title=':smile:']" + expect(page).to have_selector 'gl-emoji[data-name="smile"]' end step 'I have award added' do diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 11fa85ed2fe..071aa2e3eff 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -196,7 +196,7 @@ module SharedDiffNote step 'The diff comment preview tab should display rendered Markdown' do page.within(diff_file_selector) do find('.js-md-preview-button').click - expect(find('.js-md-preview')).to have_css('img.emoji', visible: true) + expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true) end end @@ -210,7 +210,7 @@ module SharedDiffNote step 'I should see a diff comment with an emoji image' do page.within("#{diff_file_selector} .note") do - expect(page).to have_xpath("//img[@alt=':smile:']") + expect(page).to have_xpath("//gl-emoji[@data-name='smile']") end end diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb index a036d9b884f..875d27d9383 100644 --- a/features/steps/shared/markdown.rb +++ b/features/steps/shared/markdown.rb @@ -40,7 +40,7 @@ module SharedMarkdown step 'The Markdown preview tab should display rendered Markdown' do page.within('.gfm-form') do find('.js-md-preview-button').click - expect(find('.js-md-preview')).to have_css('img.emoji', visible: true) + expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true) end end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 1870f6bc0c3..fd925e0d447 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -95,7 +95,7 @@ module SharedNote step 'The comment preview tab should be display rendered Markdown' do page.within(".js-main-target-form") do find('.js-md-preview-button').click - expect(find('.js-md-preview')).to have_css('img.emoji', visible: true) + expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true) end end diff --git a/lib/api/users.rb b/lib/api/users.rb index 2d4d5a25221..a4201fe6fed 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -27,7 +27,7 @@ module API optional :location, type: String, desc: 'The location of the user' optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' - optional :confirm, type: Boolean, desc: 'Flag indicating the account needs to be confirmed' + optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' all_or_none_of :extern_uid, :provider end @@ -97,29 +97,10 @@ module API post do authenticated_as_admin! - # Filter out params which are used later - user_params = declared_params(include_missing: false) - identity_attrs = user_params.slice(:provider, :extern_uid) - confirm = user_params.delete(:confirm) - user = User.new(user_params.except(:extern_uid, :provider, :reset_password)) - - if user_params.delete(:reset_password) - user.attributes = { - force_random_password: true, - password_expires_at: nil, - created_by_id: current_user.id - } - user.generate_password - user.generate_reset_token - end - - user.skip_confirmation! unless confirm - - if identity_attrs.any? - user.identities.build(identity_attrs) - end + params = declared_params(include_missing: false) + user = ::Users::CreateService.new(current_user, params).execute - if user.save + if user.persisted? present user, with: Entities::UserPublic else conflict!('Email has already been taken') if User. diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb index 14f54731730..5e18cecc431 100644 --- a/lib/api/v3/users.rb +++ b/lib/api/v3/users.rb @@ -9,6 +9,59 @@ module API end resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do + helpers do + params :optional_attributes do + optional :skype, type: String, desc: 'The Skype username' + optional :linkedin, type: String, desc: 'The LinkedIn username' + optional :twitter, type: String, desc: 'The Twitter username' + optional :website_url, type: String, desc: 'The website of the user' + optional :organization, type: String, desc: 'The organization of the user' + optional :projects_limit, type: Integer, desc: 'The number of projects a user can create' + optional :extern_uid, type: String, desc: 'The external authentication provider UID' + optional :provider, type: String, desc: 'The external provider' + optional :bio, type: String, desc: 'The biography of the user' + optional :location, type: String, desc: 'The location of the user' + optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' + optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' + optional :confirm, type: Boolean, default: true, desc: 'Flag indicating the account needs to be confirmed' + optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' + all_or_none_of :extern_uid, :provider + end + end + + desc 'Create a user. Available only for admins.' do + success ::API::Entities::UserPublic + end + params do + requires :email, type: String, desc: 'The email of the user' + optional :password, type: String, desc: 'The password of the new user' + optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token' + at_least_one_of :password, :reset_password + requires :name, type: String, desc: 'The name of the user' + requires :username, type: String, desc: 'The username of the user' + use :optional_attributes + end + post do + authenticated_as_admin! + + params = declared_params(include_missing: false) + user = ::Users::CreateService.new(current_user, params.merge!(skip_confirmation: !params[:confirm])).execute + + if user.persisted? + present user, with: ::API::Entities::UserPublic + else + conflict!('Email has already been taken') if User. + where(email: user.email). + count > 0 + + conflict!('Username has already been taken') if User. + where(username: user.username). + count > 0 + + render_validation_error!(user) + end + end + desc 'Get the SSH keys of a specified user. Available only for admins.' do success ::API::Entities::SSHKey end diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb index 1877429ac46..ee034d9cc56 100644 --- a/lib/gitlab/ci/build/step.rb +++ b/lib/gitlab/ci/build/step.rb @@ -7,13 +7,12 @@ module Gitlab WHEN_ALWAYS = 'always'.freeze attr_reader :name - attr_writer :script - attr_accessor :timeout, :when, :allow_failure + attr_accessor :script, :timeout, :when, :allow_failure class << self def from_commands(job) self.new(:script).tap do |step| - step.script = job.commands + step.script = job.commands.split("\n") step.timeout = job.timeout step.when = WHEN_ON_SUCCESS end @@ -36,10 +35,6 @@ module Gitlab @name = name @allow_failure = false end - - def script - @script.split("\n") - end end end end diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb index dd6d99e9075..97c121ce7b9 100644 --- a/lib/gitlab/ci/status/canceled.rb +++ b/lib/gitlab/ci/status/canceled.rb @@ -13,6 +13,10 @@ module Gitlab def icon 'icon_status_canceled' end + + def favicon + 'favicon_status_canceled' + end end end end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 3dd2b9e01f6..d4fd83b93f8 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -18,6 +18,10 @@ module Gitlab raise NotImplementedError end + def favicon + raise NotImplementedError + end + def label raise NotImplementedError end diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb index 6596d7e01ca..0721bf6ec7c 100644 --- a/lib/gitlab/ci/status/created.rb +++ b/lib/gitlab/ci/status/created.rb @@ -13,6 +13,10 @@ module Gitlab def icon 'icon_status_created' end + + def favicon + 'favicon_status_created' + end end end end diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb index c5b5e3203ad..cb75e9383a8 100644 --- a/lib/gitlab/ci/status/failed.rb +++ b/lib/gitlab/ci/status/failed.rb @@ -13,6 +13,10 @@ module Gitlab def icon 'icon_status_failed' end + + def favicon + 'favicon_status_failed' + end end end end diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb index 5f28521901d..f8f6c2903ba 100644 --- a/lib/gitlab/ci/status/manual.rb +++ b/lib/gitlab/ci/status/manual.rb @@ -13,6 +13,10 @@ module Gitlab def icon 'icon_status_manual' end + + def favicon + 'favicon_status_manual' + end end end end diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb index d30f35a59a2..f40cc1314dc 100644 --- a/lib/gitlab/ci/status/pending.rb +++ b/lib/gitlab/ci/status/pending.rb @@ -13,6 +13,10 @@ module Gitlab def icon 'icon_status_pending' end + + def favicon + 'favicon_status_pending' + end end end end diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb index 2aba3c373c7..1237cd47dc8 100644 --- a/lib/gitlab/ci/status/running.rb +++ b/lib/gitlab/ci/status/running.rb @@ -13,6 +13,10 @@ module Gitlab def icon 'icon_status_running' end + + def favicon + 'favicon_status_running' + end end end end diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb index 16282aefd03..28005d91503 100644 --- a/lib/gitlab/ci/status/skipped.rb +++ b/lib/gitlab/ci/status/skipped.rb @@ -13,6 +13,10 @@ module Gitlab def icon 'icon_status_skipped' end + + def favicon + 'favicon_status_skipped' + end end end end diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb index c09c5f006e3..88f7758a270 100644 --- a/lib/gitlab/ci/status/success.rb +++ b/lib/gitlab/ci/status/success.rb @@ -13,6 +13,10 @@ module Gitlab def icon 'icon_status_success' end + + def favicon + 'favicon_status_success' + end end end end diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index e0fdf3f3d64..0829c1c318e 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -5,35 +5,44 @@ module Gitlab CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze CHECK_DIR = Rails.root.join('ee_compat_check') - MAX_FETCH_DEPTH = 500 IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze - - attr_reader :repo_dir, :patches_dir, :ce_repo, :ce_branch + PLEASE_READ_THIS_BANNER = %Q{ + ============================================================ + ===================== PLEASE READ THIS ===================== + ============================================================ + }.freeze + THANKS_FOR_READING_BANNER = %Q{ + ============================================================ + ==================== THANKS FOR READING ==================== + ============================================================\n + }.freeze + + attr_reader :ee_repo_dir, :patches_dir, :ce_repo, :ce_branch, :ee_branch_found + attr_reader :failed_files def initialize(branch:, ce_repo: CE_REPO) - @repo_dir = CHECK_DIR.join('repo') + @ee_repo_dir = CHECK_DIR.join('ee-repo') @patches_dir = CHECK_DIR.join('patches') @ce_branch = branch @ce_repo = ce_repo end def check - ensure_ee_repo ensure_patches_dir - generate_patch(ce_branch, ce_patch_full_path) - Dir.chdir(repo_dir) do - step("In the #{repo_dir} directory") + ensure_ee_repo + Dir.chdir(ee_repo_dir) do + step("In the #{ee_repo_dir} directory") status = catch(:halt_check) do ce_branch_compat_check! - delete_ee_branch_locally! + delete_ee_branches_locally! ee_branch_presence_check! ee_branch_compat_check! end - delete_ee_branch_locally! + delete_ee_branches_locally! if status.nil? true @@ -46,11 +55,13 @@ module Gitlab private def ensure_ee_repo - if Dir.exist?(repo_dir) - step("#{repo_dir} already exists") + if Dir.exist?(ee_repo_dir) + step("#{ee_repo_dir} already exists") else - cmd = %W[git clone --branch master --single-branch --depth 200 #{EE_REPO} #{repo_dir}] - step("Cloning #{EE_REPO} into #{repo_dir}", cmd) + step( + "Cloning #{EE_REPO} into #{ee_repo_dir}", + %W[git clone --branch master --single-branch --depth=200 #{EE_REPO} #{ee_repo_dir}] + ) end end @@ -61,23 +72,18 @@ module Gitlab def generate_patch(branch, patch_path) FileUtils.rm(patch_path, force: true) - depth = 0 - loop do - depth += 50 - cmd = %W[git fetch --depth #{depth} origin --prune +refs/heads/master:refs/remotes/origin/master] - Gitlab::Popen.popen(cmd) - _, status = Gitlab::Popen.popen(%w[git merge-base FETCH_HEAD HEAD]) + find_merge_base_with_master(branch: branch) - raise "#{branch} is too far behind master, please rebase it!" if depth >= MAX_FETCH_DEPTH - break if status.zero? - end + step( + "Generating the patch against origin/master in #{patch_path}", + %w[git format-patch origin/master --stdout] + ) do |output, status| + throw(:halt_check, :ko) unless status.zero? - step("Generating the patch against master in #{patch_path}") - output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout]) - throw(:halt_check, :ko) unless status.zero? + File.write(patch_path, output) - File.write(patch_path, output) - throw(:halt_check, :ko) unless File.exist?(patch_path) + throw(:halt_check, :ko) unless File.exist?(patch_path) + end end def ce_branch_compat_check! @@ -88,9 +94,17 @@ module Gitlab end def ee_branch_presence_check! - status = step("Fetching origin/#{ee_branch}", %W[git fetch origin #{ee_branch}]) + _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch origin #{ee_branch_prefix}]) - unless status.zero? + if status.zero? + @ee_branch_found = ee_branch_prefix + else + _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch origin #{ee_branch_suffix}]) + end + + if status.zero? + @ee_branch_found = ee_branch_suffix + else puts puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg @@ -99,9 +113,9 @@ module Gitlab end def ee_branch_compat_check! - step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD]) + step("Checking out origin/#{ee_branch_found}", %W[git checkout -b #{ee_branch_found} FETCH_HEAD]) - generate_patch(ee_branch, ee_patch_full_path) + generate_patch(ee_branch_found, ee_patch_full_path) unless check_patch(ee_patch_full_path).zero? puts @@ -116,36 +130,72 @@ module Gitlab def check_patch(patch_path) step("Checking out master", %w[git checkout master]) - step("Reseting to latest master", %w[git reset --hard origin/master]) - - step("Checking if #{patch_path} applies cleanly to EE/master") - output, status = Gitlab::Popen.popen(%W[git apply --check --3way #{patch_path}]) - - unless status.zero? - failed_files = output.lines.reduce([]) do |memo, line| - if line.start_with?('error: patch failed:') - file = line.sub(/\Aerror: patch failed: /, '') - memo << file unless file =~ IGNORED_FILES_REGEX + step("Resetting to latest master", %w[git reset --hard origin/master]) + step( + "Checking if #{patch_path} applies cleanly to EE/master", + %W[git apply --check --3way #{patch_path}] + ) do |output, status| + unless status.zero? + @failed_files = output.lines.reduce([]) do |memo, line| + if line.start_with?('error: patch failed:') + file = line.sub(/\Aerror: patch failed: /, '') + memo << file unless file =~ IGNORED_FILES_REGEX + end + memo end - memo + + status = 0 if failed_files.empty? end - if failed_files.empty? - status = 0 - else - puts "\nConflicting files:" - failed_files.each do |file| - puts " - #{file}" - end + status + end + end + + def delete_ee_branches_locally! + command(%w[git checkout master]) + command(%W[git branch --delete --force #{ee_branch_prefix}]) + command(%W[git branch --delete --force #{ee_branch_suffix}]) + end + + def merge_base_found? + step( + "Finding merge base with master", + %w[git merge-base origin/master HEAD] + ) do |output, status| + if status.zero? + puts "Merge base was found: #{output}" + true end end + end + + def find_merge_base_with_master(branch:) + return if merge_base_found? + + # Start with (Math.exp(3).to_i = 20) until (Math.exp(6).to_i = 403) + # In total we go (20 + 54 + 148 + 403 = 625) commits deeper + depth = 20 + success = + (3..6).any? do |factor| + depth += Math.exp(factor).to_i + # Repository is initially cloned with a depth of 20 so we need to fetch + # deeper in the case the branch has more than 20 commits on top of master + fetch(branch: branch, depth: depth) + fetch(branch: 'master', depth: depth) + + merge_base_found? + end - status + raise "\n#{branch} is too far behind master, please rebase it!\n" unless success end - def delete_ee_branch_locally! - command(%w[git checkout master]) - step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}]) + def fetch(branch:, depth:) + step( + "Fetching deeper...", + %W[git fetch --depth=#{depth} --prune origin +refs/heads/#{branch}:refs/remotes/origin/#{branch}] + ) do |output, status| + raise "Fetch failed: #{output}" unless status.zero? + end end def ce_patch_name @@ -156,8 +206,12 @@ module Gitlab @ce_patch_full_path ||= patches_dir.join(ce_patch_name) end - def ee_branch - @ee_branch ||= "#{ce_branch}-ee" + def ee_branch_suffix + @ee_branch_suffix ||= "#{ce_branch}-ee" + end + + def ee_branch_prefix + @ee_branch_prefix ||= "ee-#{ce_branch}" end def ee_patch_name @@ -178,98 +232,125 @@ module Gitlab if cmd start = Time.now puts "\n$ #{cmd.join(' ')}" - status = command(cmd) - puts "\nFinished in #{Time.now - start} seconds" - status + + output, status = command(cmd) + puts "\n==> Finished in #{Time.now - start} seconds" + + if block_given? + yield(output, status) + else + [output, status] + end end end def command(cmd) - output, status = Gitlab::Popen.popen(cmd) - puts output - - status + Gitlab::Popen.popen(cmd) end def applies_cleanly_msg(branch) - <<-MSG.strip_heredoc - ================================================================= + %Q{ + #{PLEASE_READ_THIS_BANNER} 🎉 Congratulations!! 🎉 - The #{branch} branch applies cleanly to EE/master! + The `#{branch}` branch applies cleanly to EE/master! - Much ❤️!! - =================================================================\n - MSG + Much ❤️! For more information, see + https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests + #{THANKS_FOR_READING_BANNER} + } end def ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg - <<-MSG.strip_heredoc - ================================================================= + %Q{ + #{PLEASE_READ_THIS_BANNER} 💥 Oh no! 💥 - The #{ce_branch} branch does not apply cleanly to the current - EE/master, and no #{ee_branch} branch was found in the EE repository. + The `#{ce_branch}` branch does not apply cleanly to the current + EE/master, and no `#{ee_branch_prefix}` or `#{ee_branch_suffix}` branch + was found in the EE repository. - Please create a #{ee_branch} branch that includes changes from - #{ce_branch} but also specific changes than can be applied cleanly - to EE/master. + #{conflicting_files_msg} + + We advise you to create a `#{ee_branch_prefix}` or `#{ee_branch_suffix}` + branch that includes changes from `#{ce_branch}` but also specific changes + than can be applied cleanly to EE/master. In some cases, the conflicts + are trivial and you can ignore the warning from this job. As always, + use your best judgment! There are different ways to create such branch: - 1. Create a new branch based on the CE branch and rebase it on top of EE/master + 1. Create a new branch from master and cherry-pick your CE commits # In the EE repo - $ git fetch #{ce_repo} #{ce_branch} - $ git checkout -b #{ee_branch} FETCH_HEAD - - # You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit - # before rebasing to limit the conflicts-resolving steps during the rebase $ git fetch origin - $ git rebase origin/master + $ git checkout -b #{ee_branch_prefix} origin/master + $ git fetch #{ce_repo} #{ce_branch} + $ git cherry-pick SHA # Repeat for all the commits you want to pick - At this point you will likely have conflicts. - Solve them, and continue/finish the rebase. + You can squash the `#{ce_branch}` commits into a single "Port of #{ce_branch} to EE" commit. - You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE". + 2. Apply your branch's patch to EE - 2. Create a new branch from master and cherry-pick your CE commits + # In the CE repo + $ git fetch origin master + $ git format-patch origin/master --stdout > #{ce_branch}.patch # In the EE repo - $ git fetch origin - $ git checkout -b #{ee_branch} origin/master - $ git fetch #{ce_repo} #{ce_branch} - $ git cherry-pick SHA # Repeat for all the commits you want to pick + $ git fetch origin master + $ git checkout -b #{ee_branch_prefix} origin/master + $ git apply --3way path/to/#{ce_branch}.patch + + At this point you might have conflicts such as: - You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit. + error: patch failed: lib/gitlab/ee_compat_check.rb:5 + Falling back to three-way merge... + Applied patch to 'lib/gitlab/ee_compat_check.rb' with conflicts. + U lib/gitlab/ee_compat_check.rb - Don't forget to push your branch to #{EE_REPO}: + Resolve them, stage the changes and commit them. + + ⚠️ Don't forget to push your branch to gitlab-ee: # In the EE repo - $ git push origin #{ee_branch} + $ git push origin #{ee_branch_prefix} + + ⚠️ Also, don't forget to create a new merge request on gitlab-ce and + cross-link it with the CE merge request. - You can then retry this failed build, and hopefully it should pass. + Once this is done, you can retry this failed build, and it should pass. - Stay 💪 ! - =================================================================\n - MSG + Stay 💪 ! For more information, see + https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests + #{THANKS_FOR_READING_BANNER} + } end def ee_branch_doesnt_apply_cleanly_msg - <<-MSG.strip_heredoc - ================================================================= + %Q{ + #{PLEASE_READ_THIS_BANNER} 💥 Oh no! 💥 - The #{ce_branch} does not apply cleanly to the current - EE/master, and even though a #{ee_branch} branch exists in the EE - repository, it does not apply cleanly either to EE/master! + The `#{ce_branch}` does not apply cleanly to the current EE/master, and + even though a `#{ee_branch_found}` branch + exists in the EE repository, it does not apply cleanly either to + EE/master! + + #{conflicting_files_msg} - Please update the #{ee_branch}, push it again to #{EE_REPO}, and + Please update the `#{ee_branch_found}`, push it again to gitlab-ee, and retry this build. - Stay 💪 ! - =================================================================\n - MSG + Stay 💪 ! For more information, see + https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests + #{THANKS_FOR_READING_BANNER} + } + end + + def conflicting_files_msg + failed_files.reduce("The conflicts detected were as follows:\n") do |memo, file| + memo << "\n - #{file}" + end end end end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index fcf51b7fc5b..f98481c6d3a 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -147,10 +147,8 @@ module Gitlab end def build_new_user - user = ::User.new(user_attributes) - user.skip_confirmation! - user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider) - user + user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true) + Users::CreateService.new(nil, user_params).build end def user_attributes diff --git a/package.json b/package.json index 91d8752bebb..7b6c4556e2c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "stats-webpack-plugin": "^0.4.3", "timeago.js": "^2.0.5", "underscore": "^1.8.3", + "visibilityjs": "^1.2.4", "vue": "^2.2.4", "vue-resource": "^0.9.3", "webpack": "^2.2.1", diff --git a/scripts/merge-reports b/scripts/merge-reports index f7b574001ac..aad76bcc327 100755 --- a/scripts/merge-reports +++ b/scripts/merge-reports @@ -1,7 +1,6 @@ #!/usr/bin/env ruby require 'json' -require 'yaml' main_report_file = ARGV.shift unless main_report_file diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 885b7eabba0..f170743aea3 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -38,7 +38,7 @@ fi # Only install knapsack after bundle install! Otherwise oddly some native # gems could not be found under some circumstance. No idea why, hours wasted. -retry gem install knapsack +retry gem install knapsack fog-aws mime-types if [ "$SETUP_DB" != "false" ]; then bundle exec rake db:drop db:create db:schema:load db:migrate diff --git a/scripts/sync-reports b/scripts/sync-reports new file mode 100755 index 00000000000..5ed65e78005 --- /dev/null +++ b/scripts/sync-reports @@ -0,0 +1,95 @@ +#!/usr/bin/env ruby + +require 'rubygems' +require 'fog/aws' + +class SyncReports + ACTIONS = %w[get put].freeze + + attr_reader :options + + def initialize(options) + @options = options + + perform_sync! + end + + private + + def perform_sync! + case options[:action] + when 'get' + get_reports! + when 'put' + put_reports! + end + end + + def get_reports! + options[:report_paths].each { |report_path| get_report!(report_path) } + end + + def put_reports! + options[:report_paths].each { |report_path| put_report!(report_path) } + end + + def get_report!(report_path) + file = bucket.files.get(report_path) + + if file.respond_to?(:body) + File.write(report_path, file.body) + puts "#{report_path} was retrieved from S3." + else + puts "#{report_path} does not seem to exist on S3." + end + end + + def put_report!(report_path) + bucket.files.create( + key: report_path, + body: File.open(report_path), + public: true + ) + puts "#{report_path} was uploaded to S3." + end + + def bucket + @bucket ||= storage.directories.get(options[:bucket]) + end + + def storage + @storage ||= + Fog::Storage.new( + provider: 'AWS', + aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'], + aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] + ) + end +end + +def usage!(error: 'action') + print "\n[ERROR]: " + case error + when 'action' + puts "Please specify an action as first argument: #{SyncReports::ACTIONS.join(', ')}\n\n" + when 'bucket' + puts "Please specify a bucket as second argument!\n\n" + when 'files' + puts "Please specify one or more file paths as third argument!\n\n" + end + puts "Usage: #{__FILE__} [get|put] bucket report_path ...\n\n" + puts "Note: the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment "\ + "variables need to be set\n\n" + exit 1 +end + +if $0 == __FILE__ + action = ARGV.shift + usage!(error: 'action') unless SyncReports::ACTIONS.include?(action) + + bucket = ARGV.shift + usage!(error: 'bucket') unless bucket + usage!(error: 'files') unless ARGV.any? + + SyncReports.new(action: action, bucket: bucket, report_paths: ARGV) +end diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/builds_controller_spec.rb new file mode 100644 index 00000000000..683667129e5 --- /dev/null +++ b/spec/controllers/projects/builds_controller_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Projects::BuildsController do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + before do + sign_in(user) + end + + describe 'GET status.json' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:status) { build.detailed_status(double('user')) } + + before do + get :status, namespace_id: project.namespace, + project_id: project, + id: build.id, + format: :json + end + + it 'return a detailed build status in json' do + expect(response).to have_http_status(:ok) + expect(json_response['text']).to eq status.text + expect(json_response['label']).to eq status.label + expect(json_response['icon']).to eq status.icon + expect(json_response['favicon']).to eq status.favicon + end + end +end diff --git a/spec/controllers/projects/builds_controller_specs.rb b/spec/controllers/projects/builds_controller_specs.rb new file mode 100644 index 00000000000..d501f7b3155 --- /dev/null +++ b/spec/controllers/projects/builds_controller_specs.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Projects::BuildsController do + include ApiHelpers + + let(:project) { create(:empty_project, :public) } + + describe 'GET trace.json' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:user) { create(:user) } + + context 'when user is logged in as developer' do + before do + project.add_developer(user) + sign_in(user) + get_trace + end + + it 'traces build log' do + expect(response).to have_http_status(:ok) + expect(json_response['id']).to eq build.id + expect(json_response['status']).to eq build.status + end + end + + context 'when user is logged in as non member' do + before do + sign_in(user) + get_trace + end + + it 'traces build log' do + expect(response).to have_http_status(:ok) + expect(json_response['id']).to eq build.id + expect(json_response['status']).to eq build.status + end + end + + def get_trace + get :trace, namespace_id: project.namespace, + project_id: project, + id: build.id, + format: :json + end + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index c467ab9fb8a..734966d50b2 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -90,6 +90,7 @@ describe Projects::IssuesController do it 'redirects to signin if not logged in' do get :new, namespace_id: project.namespace, project_id: project + expect(flash[:notice]).to eq 'Please sign in to create the new issue.' expect(response).to redirect_to(new_user_session_path) end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c310d830e81..72f41f7209a 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1178,4 +1178,42 @@ describe Projects::MergeRequestsController do end end end + + describe 'GET pipeline_status.json' do + context 'when head_pipeline exists' do + let!(:pipeline) do + create(:ci_pipeline, project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + end + + let(:status) { pipeline.detailed_status(double('user')) } + + before { get_pipeline_status } + + it 'return a detailed head_pipeline status in json' do + expect(response).to have_http_status(:ok) + expect(json_response['text']).to eq status.text + expect(json_response['label']).to eq status.label + expect(json_response['icon']).to eq status.icon + expect(json_response['favicon']).to eq status.favicon + end + end + + context 'when head_pipeline does not exist' do + before { get_pipeline_status } + + it 'return empty' do + expect(response).to have_http_status(:ok) + expect(json_response).to be_empty + end + end + + def get_pipeline_status + get :pipeline_status, namespace_id: project.namespace, + project_id: project, + id: merge_request.iid, + format: :json + end + end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 04bb5cbbd59..d8f9bfd0d37 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -69,4 +69,24 @@ describe Projects::PipelinesController do format: :json end end + + describe 'GET status.json' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:status) { pipeline.detailed_status(double('user')) } + + before do + get :status, namespace_id: project.namespace, + project_id: project, + id: pipeline.id, + format: :json + end + + it 'return a detailed pipeline status in json' do + expect(response).to have_http_status(:ok) + expect(json_response['text']).to eq status.text + expect(json_response['label']).to eq status.label + expect(json_response['icon']).to eq status.icon + expect(json_response['favicon']).to eq status.favicon + end + end end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 8cc216445eb..902911071c4 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -30,6 +30,15 @@ describe RegistrationsController do expect(subject.current_user).to be_nil end end + + context 'when signup_enabled? is false' do + it 'redirects to sign_in' do + allow_any_instance_of(ApplicationSetting).to receive(:signup_enabled?).and_return(false) + + expect { post(:create, user_params) }.not_to change(User, :count) + expect(response).to redirect_to(new_user_session_path) + end + end end context 'when reCAPTCHA is enabled' do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 6b0d084614b..f78086211f7 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -172,7 +172,7 @@ FactoryGirl.define do { image: 'ruby:2.1', services: ['postgres'], - after_script: "ls\ndate", + after_script: %w(ls date), artifacts: { name: 'artifacts_file', untracked: false, diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb index bc957ec72e1..d6c63f66a9b 100644 --- a/spec/features/admin/admin_broadcast_messages_spec.rb +++ b/spec/features/admin/admin_broadcast_messages_spec.rb @@ -45,7 +45,7 @@ feature 'Admin Broadcast Messages', feature: true do page.within('.broadcast-message-preview') do expect(page).to have_selector('strong', text: 'Markdown') - expect(page).to have_selector('img.emoji') + expect(page).to have_selector('gl-emoji[data-name="tada"]') end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 0e305c52358..881f1fca4d1 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -11,12 +11,16 @@ describe 'Commits' do stub_ci_pipeline_to_return_yaml_file end + let(:creator) { create(:user) } + let!(:pipeline) do create(:ci_pipeline, project: project, + user: creator, ref: project.default_branch, sha: project.commit.sha, - status: :success) + status: :success, + created_at: 5.months.ago) end context 'commit status is Generic Commit Status' do @@ -80,7 +84,8 @@ describe 'Commits' do it 'shows pipeline`s data' do expect(page).to have_content pipeline.sha[0..7] expect(page).to have_content pipeline.git_commit_message - expect(page).to have_content pipeline.git_author_name + expect(page).to have_content pipeline.user.name + expect(page).to have_content pipeline.created_at.strftime('%b %d, %Y') end end @@ -150,7 +155,7 @@ describe 'Commits' do it do expect(page).to have_content pipeline.sha[0..7] expect(page).to have_content pipeline.git_commit_message - expect(page).to have_content pipeline.git_author_name + expect(page).to have_content pipeline.user.name expect(page).to have_link('Download artifacts') expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Retry') @@ -169,7 +174,7 @@ describe 'Commits' do it do expect(page).to have_content pipeline.sha[0..7] expect(page).to have_content pipeline.git_commit_message - expect(page).to have_content pipeline.git_author_name + expect(page).to have_content pipeline.user.name expect(page).not_to have_link('Download artifacts') expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Retry') diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index 773ae4b38bc..9daaaa8e555 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -7,6 +7,7 @@ describe 'Explore Groups page', js: true, feature: true do let!(:group) { create(:group) } let!(:public_group) { create(:group, :public) } let!(:private_group) { create(:group, :private) } + let!(:empty_project) { create(:empty_project, group: public_group) } before do group.add_owner(user) @@ -43,4 +44,23 @@ describe 'Explore Groups page', js: true, feature: true do expect(page).not_to have_content(private_group.full_name) expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 end + + it 'shows non-archived projects count' do + # Initially project is not archived + expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") + + # Archive project + empty_project.archive! + visit explore_groups_path + + # Check project count + expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("0") + + # Unarchive project + empty_project.unarchive! + visit explore_groups_path + + # Check project count + expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") + end end diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb index 8528718a2f7..8a1d415c4f1 100644 --- a/spec/features/groups/group_name_toggle_spec.rb +++ b/spec/features/groups/group_name_toggle_spec.rb @@ -6,39 +6,46 @@ feature 'Group name toggle', feature: true, js: true do let(:nested_group_2) { create(:group, parent: nested_group_1) } let(:nested_group_3) { create(:group, parent: nested_group_2) } + SMALL_SCREEN = 300 + before do login_as :user end - it 'is not present for less than 3 groups' do - visit group_path(group) - expect(page).not_to have_css('.group-name-toggle') + it 'is not present if enough horizontal space' do + visit group_path(nested_group_3) - visit group_path(nested_group_1) + container_width = page.evaluate_script("$('.title-container')[0].offsetWidth") + title_width = page.evaluate_script("$('.title')[0].offsetWidth") + + expect(container_width).to be > title_width expect(page).not_to have_css('.group-name-toggle') end - it 'is present for nested group of 3 or more in the namespace' do - visit group_path(nested_group_2) - expect(page).to have_css('.group-name-toggle') - + it 'is present if the title is longer than the container' do visit group_path(nested_group_3) - expect(page).to have_css('.group-name-toggle') + title_width = page.evaluate_script("$('.title')[0].offsetWidth") + + page_height = page.current_window.size[1] + page.current_window.resize_to(SMALL_SCREEN, page_height) + + find('.group-name-toggle') + container_width = page.evaluate_script("$('.title-container')[0].offsetWidth") + + expect(title_width).to be > container_width end - context 'for group with at least 3 groups' do - before do - visit group_path(nested_group_2) - end + it 'should show the full group namespace when toggled' do + page_height = page.current_window.size[1] + page.current_window.resize_to(SMALL_SCREEN, page_height) + visit group_path(nested_group_3) - it 'should show the full group namespace when toggled' do - expect(page).not_to have_content(group.name) - expect(page).to have_css('.group-path.hidable', visible: false) + expect(page).not_to have_content(group.name) + expect(page).to have_css('.group-path.hidable', visible: false) - click_button '...' + click_button '...' - expect(page).to have_content(group.name) - expect(page).to have_css('.group-path.hidable', visible: true) - end + expect(page).to have_content(group.name) + expect(page).to have_css('.group-path.hidable', visible: true) end end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 8cc0996acab..f1ad4a55246 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -12,6 +12,33 @@ feature 'Create New Merge Request', feature: true, js: true do login_as user end + it 'selects the source branch sha when a tag with the same name exists' do + visit namespace_project_merge_requests_path(project.namespace, project) + + click_link 'New Merge Request' + expect(page).to have_content('Source branch') + expect(page).to have_content('Target branch') + + first('.js-source-branch').click + first('.dropdown-source-branch .dropdown-content a', text: 'v1.1.0').click + + expect(page).to have_content "b83d6e3" + end + + it 'selects the target branch sha when a tag with the same name exists' do + visit namespace_project_merge_requests_path(project.namespace, project) + + click_link 'New Merge Request' + + expect(page).to have_content('Source branch') + expect(page).to have_content('Target branch') + + first('.js-target-branch').click + first('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0').click + + expect(page).to have_content "b83d6e3" + end + it 'generates a diff for an orphaned branch' do visit namespace_project_merge_requests_path(project.namespace, project) diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb index a2cf9b18bf2..3acd3f6a8b3 100644 --- a/spec/features/merge_requests/toggler_behavior_spec.rb +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -18,7 +18,7 @@ feature 'toggler_behavior', js: true, feature: true do it 'should be scrolled down to fragment' do page_height = page.current_window.size[1] page_scroll_y = page.evaluate_script("window.scrollY") - fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top") + fragment_position_top = page.evaluate_script("Math.round($('#{fragment_id}').offset().top)") expect(find('.js-toggle-content').visible?).to eq true expect(find(fragment_id).visible?).to eq true expect(fragment_position_top).to be >= page_scroll_y diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb index 030043d14aa..b2a3b111c9e 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -53,6 +53,7 @@ describe "Compare", js: true do dropdown = find(".js-compare-#{dropdown_type}-dropdown") dropdown.find(".compare-dropdown-toggle").click dropdown.fill_in("Filter by Git revision", with: selection) - find_link(selection, visible: true).click + wait_for_ajax + dropdown.find_all("a[data-ref=\"#{selection}\"]", visible: true).last.click end end diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index b64c15e0adc..de25d45f447 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -61,7 +61,7 @@ feature 'Projects > Members > User requests access', feature: true do click_link('Settings') end - page.within('.page-with-layout-nav .sub-nav') do + page.within('.sub-nav') do click_link('Members') end end diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb new file mode 100644 index 00000000000..da3eaed707a --- /dev/null +++ b/spec/features/projects/milestones/milestones_sorting_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +feature 'Milestones sorting', :feature, :js do + include SortingHelper + let(:user) { create(:user) } + let(:project) { create(:empty_project, name: 'test', namespace: user.namespace) } + + before do + # Milestones + create(:milestone, + due_date: 10.days.from_now, + created_at: 2.hours.ago, + title: "aaa", project: project) + create(:milestone, + due_date: 11.days.from_now, + created_at: 1.hour.ago, + title: "bbb", project: project) + login_as(user) + end + + scenario 'visit project milestones and sort by due_date_asc' do + visit namespace_project_milestones_path(project.namespace, project) + + expect(page).to have_button('Due soon') + + # assert default sorting + within '.milestones' do + expect(page.all('ul.content-list > li').first.text).to include('aaa') + expect(page.all('ul.content-list > li').last.text).to include('bbb') + end + + click_button 'Due soon' + + sort_options = find('ul.dropdown-menu-sort li').all('a').collect(&:text) + + expect(sort_options[0]).to eq('Due soon') + expect(sort_options[1]).to eq('Due later') + expect(sort_options[2]).to eq('Start soon') + expect(sort_options[3]).to eq('Start later') + expect(sort_options[4]).to eq('Name, ascending') + expect(sort_options[5]).to eq('Name, descending') + + click_link 'Due later' + + expect(page).to have_button('Due later') + + within '.milestones' do + expect(page.all('ul.content-list > li').first.text).to include('bbb') + expect(page.all('ul.content-list > li').last.text).to include('aaa') + end + end +end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 9f06e52ab55..5a53e48f5f8 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -45,7 +45,7 @@ describe 'Pipeline', :feature, :js do include_context 'pipeline builds' let(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) } before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 162056671e0..2272b19bc8f 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -442,7 +442,7 @@ describe 'Pipelines', :feature, :js do context 'when project is public' do let(:project) { create(:project, :public) } - it { expect(page).to have_content 'No pipelines to show' } + it { expect(page).to have_content 'Build with confidence' } it { expect(page).to have_http_status(:success) } end diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb index 336c4092c98..659cd7c7af7 100644 --- a/spec/features/user_callout_spec.rb +++ b/spec/features/user_callout_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe 'User Callouts', js: true do let(:user) { create(:user) } + let(:another_user) { create(:user) } let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') } before do @@ -32,6 +33,11 @@ describe 'User Callouts', js: true do within('.user-callout') do find('.close-user-callout').click end - expect(page).not_to have_selector('#user-callout') + expect(page).not_to have_selector('.user-callout') + end + + it 'does not show callout on another users profile' do + visit user_path(another_user) + expect(page).not_to have_selector('.user-callout') end end diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb new file mode 100644 index 00000000000..581726c1d0e --- /dev/null +++ b/spec/helpers/avatars_helper_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe AvatarsHelper do + let(:user) { create(:user) } + + describe '#user_avatar' do + subject { helper.user_avatar(user: user) } + + it "links to the user's profile" do + is_expected.to include("href=\"#{user_path(user)}\"") + end + + it "has the user's name as title" do + is_expected.to include("title=\"#{user.name}\"") + end + + it "contains the user's avatar image" do + is_expected.to include(CGI.escapeHTML(user.avatar_url(16))) + end + end +end diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb new file mode 100644 index 00000000000..e5143a0263d --- /dev/null +++ b/spec/helpers/namespaces_helper_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe NamespacesHelper, type: :helper do + let!(:admin) { create(:admin) } + let!(:admin_group) { create(:group, :private) } + let!(:user) { create(:user) } + let!(:user_group) { create(:group, :private) } + + before do + admin_group.add_owner(admin) + user_group.add_owner(user) + end + + describe '#namespaces_options' do + it 'returns groups without being a member for admin' do + allow(helper).to receive(:current_user).and_return(admin) + + options = helper.namespaces_options(user_group.id, display_path: true, extra_group: user_group.id) + + expect(options).to include(admin_group.name) + expect(options).to include(user_group.name) + end + + it 'returns only allowed namespaces for user' do + allow(helper).to receive(:current_user).and_return(user) + + options = helper.namespaces_options + + expect(options).not_to include(admin_group.name) + expect(options).to include(user_group.name) + end + end +end diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb new file mode 100644 index 00000000000..03f78de8e91 --- /dev/null +++ b/spec/helpers/users_helper_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe UsersHelper do + let(:user) { create(:user) } + + describe '#user_link' do + subject { helper.user_link(user) } + + it "links to the user's profile" do + is_expected.to include("href=\"#{user_path(user)}\"") + end + + it "has the user's email as title" do + is_expected.to include("title=\"#{user.email}\"") + end + end +end diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index e21f4ca2bc0..8153e46c438 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -50,9 +50,9 @@ describe('Store', () => { it('finds list by ID', () => { gl.issueBoards.BoardsStore.addList(listObj); - const list = gl.issueBoards.BoardsStore.findList('id', 1); + const list = gl.issueBoards.BoardsStore.findList('id', listObj.id); - expect(list.id).toBe(1); + expect(list.id).toBe(listObj.id); }); it('finds list by type', () => { @@ -64,7 +64,7 @@ describe('Store', () => { it('gets issue when new list added', (done) => { gl.issueBoards.BoardsStore.addList(listObj); - const list = gl.issueBoards.BoardsStore.findList('id', 1); + const list = gl.issueBoards.BoardsStore.findList('id', listObj.id); expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); @@ -89,9 +89,9 @@ describe('Store', () => { expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); setTimeout(() => { - const list = gl.issueBoards.BoardsStore.findList('id', 1); + const list = gl.issueBoards.BoardsStore.findList('id', listObj.id); expect(list).toBeDefined(); - expect(list.id).toBe(1); + expect(list.id).toBe(listObj.id); expect(list.position).toBe(0); done(); }, 0); @@ -126,7 +126,7 @@ describe('Store', () => { expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); - gl.issueBoards.BoardsStore.removeList(1, 'label'); + gl.issueBoards.BoardsStore.removeList(listObj.id, 'label'); expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0); }); @@ -137,7 +137,7 @@ describe('Store', () => { expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); - gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']); + gl.issueBoards.BoardsStore.moveList(listOne, [listObjDuplicate.id, listObj.id]); expect(listOne.position).toBe(1); }); diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index 66fc01fa1e5..a9d4c6ef76f 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -43,7 +43,7 @@ describe('List model', () => { list = new List({ title: 'test', label: { - id: 1, + id: _.random(10000), title: 'test', color: 'red' } @@ -51,7 +51,7 @@ describe('List model', () => { list.save(); setTimeout(() => { - expect(list.id).toBe(1); + expect(list.id).toBe(listObj.id); expect(list.type).toBe('label'); expect(list.position).toBe(0); done(); @@ -60,7 +60,7 @@ describe('List model', () => { it('destroys the list', (done) => { gl.issueBoards.BoardsStore.addList(listObj); - list = gl.issueBoards.BoardsStore.findList('id', 1); + list = gl.issueBoards.BoardsStore.findList('id', listObj.id); expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); list.destroy(); @@ -92,7 +92,7 @@ describe('List model', () => { const listDup = new List(listObjDuplicate); const issue = new ListIssue({ title: 'Testing', - iid: 1, + iid: _.random(10000), confidential: false, labels: [list.label, listDup.label] }); @@ -102,7 +102,7 @@ describe('List model', () => { spyOn(gl.boardService, 'moveIssue').and.callThrough(); - listDup.updateIssueLabel(list, issue); + listDup.updateIssueLabel(issue, list); expect(gl.boardService.moveIssue) .toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined); diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 7a399b307ad..a4fa694eebe 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,12 +1,12 @@ /* eslint-disable comma-dangle, no-unused-vars, quote-props */ const listObj = { - id: 1, + id: _.random(10000), position: 0, title: 'Test', list_type: 'label', label: { - id: 1, + id: _.random(10000), title: 'Testing', color: 'red', description: 'testing;' @@ -14,12 +14,12 @@ const listObj = { }; const listObjDuplicate = { - id: 2, + id: listObj.id, position: 1, title: 'Test', list_type: 'label', label: { - id: 2, + id: listObj.label.id, title: 'Testing', color: 'red', description: 'testing;' diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 75efcc06585..bc2e092db65 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -33,7 +33,8 @@ describe('Pipelines table in Commits and Merge requests', () => { }); setTimeout(() => { - expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + expect(component.$el.querySelector('.empty-state')).toBeDefined(); + expect(component.$el.querySelector('.realtime-loading')).toBe(null); done(); }, 1); }); @@ -63,6 +64,7 @@ describe('Pipelines table in Commits and Merge requests', () => { setTimeout(() => { expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); + expect(component.$el.querySelector('.realtime-loading')).toBe(null); done(); }, 0); }); @@ -92,7 +94,8 @@ describe('Pipelines table in Commits and Merge requests', () => { }); setTimeout(() => { - expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); + expect(component.$el.querySelector('.realtime-loading')).toBe(null); done(); }, 0); }); diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js new file mode 100644 index 00000000000..50000c5a5f5 --- /dev/null +++ b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import limitWarningComp from '~/cycle_analytics/components/limit_warning_component'; + +describe('Limit warning component', () => { + let component; + let LimitWarningComponent; + + beforeEach(() => { + LimitWarningComponent = Vue.extend(limitWarningComp); + }); + + it('should not render if count is not exactly than 50', () => { + component = new LimitWarningComponent({ + propsData: { + count: 5, + }, + }).$mount(); + + expect(component.$el.textContent.trim()).toBe(''); + + component = new LimitWarningComponent({ + propsData: { + count: 55, + }, + }).$mount(); + + expect(component.$el.textContent.trim()).toBe(''); + }); + + it('should render if count is exactly 50', () => { + component = new LimitWarningComponent({ + propsData: { + count: 50, + }, + }).$mount(); + + expect(component.$el.textContent.trim()).toBe('Showing 50 events'); + }); +}); diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml new file mode 100644 index 00000000000..418a38a0e2e --- /dev/null +++ b/spec/javascripts/fixtures/pipelines.html.haml @@ -0,0 +1,14 @@ +%div + #pipelines-list-vue{ data: { endpoint: 'foo', + "css-class" => 'foo', + "help-page-path" => 'foo', + "new-pipeline-path" => 'foo', + "can-create-pipeline" => 'true', + "all-path" => 'foo', + "pending-path" => 'foo', + "running-path" => 'foo', + "finished-path" => 'foo', + "branches-path" => 'foo', + "tags-path" => 'foo', + "has-ci" => 'foo', + "ci-lint-path" => 'foo' } } diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml index fbe4a434f76..ad1682704bb 100644 --- a/spec/javascripts/fixtures/pipelines_table.html.haml +++ b/spec/javascripts/fixtures/pipelines_table.html.haml @@ -1,2 +1 @@ -#commit-pipeline-table-view{ data: { endpoint: "endpoint" } } -.pipeline-svgs{ data: { "commit_icon_svg": "svg"} } +#commit-pipeline-table-view{ data: { endpoint: "endpoint", "help-page-path": "foo" } } diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index 05bc6bfd74b..c794a632417 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -124,4 +124,40 @@ describe('Poll', () => { Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); }); + + describe('stop', () => { + it('stops polling when method is called', (done) => { + const pollInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } })); + }; + + Vue.http.interceptors.push(pollInterceptor); + + const service = new ServiceMock('endpoint'); + spyOn(service, 'fetch').and.callThrough(); + + const Polling = new Poll({ + resource: service, + method: 'fetch', + data: { page: 1 }, + successCallback: () => { + Polling.stop(); + }, + errorCallback: callbacks.error, + }); + + spyOn(Polling, 'stop').and.callThrough(); + + Polling.makeRequest(); + + setTimeout(() => { + expect(service.fetch.calls.count()).toEqual(1); + expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); + expect(Polling.stop).toHaveBeenCalled(); + done(); + }, 100); + + Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); + }); + }); }); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 15465588223..d658f680f97 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -35,7 +35,8 @@ testsContext.keys().forEach(function (path) { if (process.env.BABEL_ENV === 'coverage') { // exempt these files from the coverage report const troubleMakers = [ - './blob_edit/blob_edit_bundle.js', + './blob_edit/blob_bundle.js', + './boards/boards_bundle.js', './cycle_analytics/components/stage_plan_component.js', './cycle_analytics/components/stage_staging_component.js', './cycle_analytics/components/stage_test_component.js', diff --git a/spec/javascripts/vue_pipelines_index/empty_state_spec.js b/spec/javascripts/vue_pipelines_index/empty_state_spec.js new file mode 100644 index 00000000000..733337168dc --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/empty_state_spec.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import emptyStateComp from '~/vue_pipelines_index/components/empty_state'; + +describe('Pipelines Empty State', () => { + let component; + let EmptyStateComponent; + + beforeEach(() => { + EmptyStateComponent = Vue.extend(emptyStateComp); + + component = new EmptyStateComponent({ + propsData: { + helpPagePath: 'foo', + }, + }).$mount(); + }); + + it('should render empty state SVG', () => { + expect(component.$el.querySelector('.svg-content svg')).toBeDefined(); + }); + + it('should render emtpy state information', () => { + expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); + + expect( + component.$el.querySelector('p').textContent, + ).toContain('Continous Integration can help catch bugs by running your tests automatically'); + + expect( + component.$el.querySelector('p').textContent, + ).toContain('Continuous Deployment can help you deliver code to your product environment'); + }); + + it('should render a link with provided help path', () => { + expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo'); + expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/error_state_spec.js b/spec/javascripts/vue_pipelines_index/error_state_spec.js new file mode 100644 index 00000000000..524e018b1fa --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/error_state_spec.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import errorStateComp from '~/vue_pipelines_index/components/error_state'; + +describe('Pipelines Error State', () => { + let component; + let ErrorStateComponent; + + beforeEach(() => { + ErrorStateComponent = Vue.extend(errorStateComp); + + component = new ErrorStateComponent().$mount(); + }); + + it('should render error state SVG', () => { + expect(component.$el.querySelector('.svg-content svg')).toBeDefined(); + }); + + it('should render emtpy state information', () => { + expect( + component.$el.querySelector('h4').textContent, + ).toContain('The API failed to fetch the pipelines'); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/mock_data.js b/spec/javascripts/vue_pipelines_index/mock_data.js new file mode 100644 index 00000000000..2365a662b9f --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/mock_data.js @@ -0,0 +1,107 @@ +export default { + pipelines: [{ + id: 115, + user: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + path: '/root/review-app/pipelines/115', + details: { + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/115', + }, + duration: null, + finished_at: '2017-03-17T19:00:15.996Z', + stages: [{ + name: 'build', + title: 'build: failed', + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/115#build', + }, + path: '/root/review-app/pipelines/115#build', + dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=build', + }, + { + name: 'review', + title: 'review: skipped', + status: { + icon: 'icon_status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + has_details: true, + details_path: '/root/review-app/pipelines/115#review', + }, + path: '/root/review-app/pipelines/115#review', + dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=review', + }], + artifacts: [], + manual_actions: [{ + name: 'stop_review', + path: '/root/review-app/builds/3766/play', + }], + }, + flags: { + latest: true, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: false, + }, + ref: { + name: 'thisisabranch', + path: '/root/review-app/tree/thisisabranch', + tag: false, + branch: true, + }, + commit: { + id: '9e87f87625b26c42c59a2ee0398f81d20cdfe600', + short_id: '9e87f876', + title: 'Update README.md', + created_at: '2017-03-15T22:58:28.000+00:00', + parent_ids: ['3744f9226e699faec2662a8b267e5d3fd0bfff0e'], + message: 'Update README.md', + author_name: 'Root', + author_email: 'admin@example.com', + authored_date: '2017-03-15T22:58:28.000+00:00', + committer_name: 'Root', + committer_email: 'admin@example.com', + committed_date: '2017-03-15T22:58:28.000+00:00', + author: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + commit_url: 'http://localhost:3000/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600', + commit_path: '/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600', + }, + retry_path: '/root/review-app/pipelines/115/retry', + created_at: '2017-03-15T22:58:33.436Z', + updated_at: '2017-03-17T19:00:15.997Z', + }], + count: { + all: 52, + running: 0, + pending: 0, + finished: 52, + }, +}; diff --git a/spec/javascripts/vue_pipelines_index/nav_controls_spec.js b/spec/javascripts/vue_pipelines_index/nav_controls_spec.js new file mode 100644 index 00000000000..659c4854a56 --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/nav_controls_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import navControlsComp from '~/vue_pipelines_index/components/nav_controls'; + +describe('Pipelines Nav Controls', () => { + let NavControlsComponent; + + beforeEach(() => { + NavControlsComponent = Vue.extend(navControlsComp); + }); + + it('should render link to create a new pipeline', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: true, + helpPagePath: 'foo', + ciLintPath: 'foo', + canCreatePipeline: true, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline'); + expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath); + }); + + it('should not render link to create pipeline if no permission is provided', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: true, + helpPagePath: 'foo', + ciLintPath: 'foo', + canCreatePipeline: false, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelector('.btn-create')).toEqual(null); + }); + + it('should render link for CI lint', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: true, + helpPagePath: 'foo', + ciLintPath: 'foo', + canCreatePipeline: true, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelector('.btn-default').textContent).toContain('CI Lint'); + expect(component.$el.querySelector('.btn-default').getAttribute('href')).toEqual(mockData.ciLintPath); + }); + + it('should render link to help page when CI is not enabled', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: false, + helpPagePath: 'foo', + ciLintPath: 'foo', + canCreatePipeline: true, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); + expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath); + }); + + it('should not render link to help page when CI is enabled', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: true, + helpPagePath: 'foo', + ciLintPath: 'foo', + canCreatePipeline: true, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelector('.btn-info')).toEqual(null); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipelines_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_spec.js new file mode 100644 index 00000000000..725f6cb2d7a --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipelines_spec.js @@ -0,0 +1,114 @@ +import Vue from 'vue'; +import pipelinesComp from '~/vue_pipelines_index/pipelines'; +import Store from '~/vue_pipelines_index/stores/pipelines_store'; +import pipelinesData from './mock_data'; + +describe('Pipelines', () => { + preloadFixtures('static/pipelines.html.raw'); + + let PipelinesComponent; + + beforeEach(() => { + loadFixtures('static/pipelines.html.raw'); + + PipelinesComponent = Vue.extend(pipelinesComp); + }); + + describe('successfull request', () => { + describe('with pipelines', () => { + const pipelinesInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(pipelinesData), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesInterceptor, + ); + }); + + it('should render table', (done) => { + const component = new PipelinesComponent({ + propsData: { + store: new Store(), + }, + }).$mount(); + + setTimeout(() => { + expect(component.$el.querySelector('.table-holder')).toBeDefined(); + expect(component.$el.querySelector('.realtime-loading')).toBe(null); + done(); + }); + }); + }); + + describe('without pipelines', () => { + const emptyInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(emptyInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, emptyInterceptor, + ); + }); + + it('should render empty state', (done) => { + const component = new PipelinesComponent({ + propsData: { + store: new Store(), + }, + }).$mount(); + + setTimeout(() => { + expect(component.$el.querySelector('.empty-state')).toBeDefined(); + expect(component.$el.querySelector('.realtime-loading')).toBe(null); + done(); + }); + }); + }); + }); + + describe('unsuccessfull request', () => { + const errorInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 500, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(errorInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, errorInterceptor, + ); + }); + + it('should render error state', (done) => { + const component = new PipelinesComponent({ + propsData: { + store: new Store(), + }, + }).$mount(); + + setTimeout(() => { + expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); + expect(component.$el.querySelector('.realtime-loading')).toBe(null); + done(); + }); + }); + }); +}); diff --git a/spec/lib/gitlab/ci/build/step_spec.rb b/spec/lib/gitlab/ci/build/step_spec.rb index 2a314a744ca..49457b129e3 100644 --- a/spec/lib/gitlab/ci/build/step_spec.rb +++ b/spec/lib/gitlab/ci/build/step_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::Ci::Build::Step do end context 'when after_script is not empty' do - let(:job) { create(:ci_build, options: { after_script: "ls -la\ndate" }) } + let(:job) { create(:ci_build, options: { after_script: ['ls -la', 'date'] }) } it 'fabricates an object' do expect(subject.name).to eq(:after_script) diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 8b3bd08cf13..e648a3ac3a2 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -27,6 +27,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'passed' expect(status.icon).to eq 'icon_status_success' + expect(status.favicon).to eq 'favicon_status_success' expect(status.label).to eq 'passed' expect(status).to have_details expect(status).to have_action @@ -53,6 +54,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'failed' expect(status.icon).to eq 'icon_status_failed' + expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed' expect(status).to have_details expect(status).to have_action @@ -79,6 +81,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'failed' expect(status.icon).to eq 'icon_status_warning' + expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed (allowed to fail)' expect(status).to have_details expect(status).to have_action @@ -107,6 +110,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'canceled' expect(status.icon).to eq 'icon_status_canceled' + expect(status.favicon).to eq 'favicon_status_canceled' expect(status.label).to eq 'canceled' expect(status).to have_details expect(status).to have_action @@ -132,6 +136,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'running' expect(status.icon).to eq 'icon_status_running' + expect(status.favicon).to eq 'favicon_status_running' expect(status.label).to eq 'running' expect(status).to have_details expect(status).to have_action @@ -157,6 +162,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'pending' expect(status.icon).to eq 'icon_status_pending' + expect(status.favicon).to eq 'favicon_status_pending' expect(status.label).to eq 'pending' expect(status).to have_details expect(status).to have_action @@ -181,6 +187,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'skipped' expect(status.icon).to eq 'icon_status_skipped' + expect(status.favicon).to eq 'favicon_status_skipped' expect(status.label).to eq 'skipped' expect(status).to have_details expect(status).not_to have_action @@ -208,6 +215,7 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.text).to eq 'manual' expect(status.group).to eq 'manual' expect(status.icon).to eq 'icon_status_manual' + expect(status.favicon).to eq 'favicon_status_manual' expect(status.label).to eq 'manual play action' expect(status).to have_details expect(status).to have_action @@ -235,6 +243,7 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.text).to eq 'manual' expect(status.group).to eq 'manual' expect(status.icon).to eq 'icon_status_manual' + expect(status.favicon).to eq 'favicon_status_manual' expect(status.label).to eq 'manual stop action' expect(status).to have_details expect(status).to have_action diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 768f8926f1d..530639a5897 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Canceled do it { expect(subject.icon).to eq 'icon_status_canceled' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_canceled' } + end + describe '#group' do it { expect(subject.group).to eq 'canceled' } end diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index e96c13aede3..aef982e17f1 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Created do it { expect(subject.icon).to eq 'icon_status_created' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_created' } + end + describe '#group' do it { expect(subject.group).to eq 'created' } end diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index e5da0a91159..9a25743885c 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Failed do it { expect(subject.icon).to eq 'icon_status_failed' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_failed' } + end + describe '#group' do it { expect(subject.group).to eq 'failed' } end diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb index 3fd3727b92d..6fdc3801d71 100644 --- a/spec/lib/gitlab/ci/status/manual_spec.rb +++ b/spec/lib/gitlab/ci/status/manual_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Manual do it { expect(subject.icon).to eq 'icon_status_manual' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_manual' } + end + describe '#group' do it { expect(subject.group).to eq 'manual' } end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index 8d09cf2a05a..ffc53f0506b 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Pending do it { expect(subject.icon).to eq 'icon_status_pending' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_pending' } + end + describe '#group' do it { expect(subject.group).to eq 'pending' } end diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index 10d3bf749c1..0babf1fb54e 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Running do it { expect(subject.icon).to eq 'icon_status_running' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_running' } + end + describe '#group' do it { expect(subject.group).to eq 'running' } end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index 10db93d3802..670747c9f0b 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Skipped do it { expect(subject.icon).to eq 'icon_status_skipped' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_skipped' } + end + describe '#group' do it { expect(subject.group).to eq 'skipped' } end diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index 230f24b94a4..ff65b074808 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Success do it { expect(subject.icon).to eq 'icon_status_success' } end + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_success' } + end + describe '#group' do it { expect(subject.group).to eq 'success' } end diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb index 17d6be470ac..d6d365f6492 100644 --- a/spec/lib/gitlab/git/blob_snippet_spec.rb +++ b/spec/lib/gitlab/git/blob_snippet_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe Gitlab::Git::BlobSnippet, seed_helper: true do - describe :data do + describe '#data' do context 'empty lines' do let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) } diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 8049e2c120d..b883526151e 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -5,7 +5,7 @@ require "spec_helper" describe Gitlab::Git::Blob, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } - describe :initialize do + describe 'initialize' do let(:blob) { Gitlab::Git::Blob.new(name: 'test') } it 'handles nil data' do @@ -15,7 +15,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe :find do + describe '.find' do context 'file in subdir' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") } @@ -101,7 +101,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe :raw do + describe '.raw' do let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) } it { expect(raw_blob.data[0..10]).to eq("require \'fi") } @@ -222,7 +222,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe :lfs_pointers do + describe 'lfs_pointers' do context 'file a valid lfs pointer' do let(:blob) do Gitlab::Git::Blob.find( diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index e1be6784c20..5cf4631fbfc 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -65,7 +65,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end context 'Class methods' do - describe :find do + describe '.find' do it "should return first head commit if without params" do expect(Gitlab::Git::Commit.last(repository).id).to eq( repository.raw.head.target.oid @@ -103,7 +103,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :last_for_path do + describe '.last_for_path' do context 'no path' do subject { Gitlab::Git::Commit.last_for_path(repository, 'master') } @@ -132,7 +132,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe "where" do + describe '.where' do context 'path is empty string' do subject do commits = Gitlab::Git::Commit.where( @@ -230,7 +230,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :between do + describe '.between' do subject do commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) commits.map { |c| c.id } @@ -243,7 +243,7 @@ describe Gitlab::Git::Commit, seed_helper: true do it { is_expected.not_to include(SeedRepo::FirstCommit::ID) } end - describe :find_all do + describe '.find_all' do context 'max_count' do subject do commits = Gitlab::Git::Commit.find_all( @@ -304,7 +304,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :init_from_rugged do + describe '#init_from_rugged' do let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) } subject { gitlab_commit } @@ -314,7 +314,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :init_from_hash do + describe '#init_from_hash' do let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) } subject { commit } @@ -329,7 +329,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :stats do + describe '#stats' do subject { commit.stats } describe '#additions' do @@ -343,25 +343,25 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :to_diff do + describe '#to_diff' do subject { commit.to_diff } it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" } it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} end - describe :has_zero_stats? do + describe '#has_zero_stats?' do it { expect(commit.has_zero_stats?).to eq(false) } end - describe :to_patch do + describe '#to_patch' do subject { commit.to_patch } it { is_expected.to include "From #{SeedRepo::Commit::ID}" } it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} end - describe :to_hash do + describe '#to_hash' do let(:hash) { commit.to_hash } subject { hash } @@ -373,7 +373,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe :diffs do + describe '#diffs' do subject { commit.diffs } it { is_expected.to be_kind_of Gitlab::Git::DiffCollection } @@ -381,7 +381,7 @@ describe Gitlab::Git::Commit, seed_helper: true do it { expect(subject.first).to be_kind_of Gitlab::Git::Diff } end - describe :ref_names do + describe '#ref_names' do let(:commit) { Gitlab::Git::Commit.find(repository, 'master') } subject { commit.ref_names(repository) } diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb index f66b68e4218..e28debe1494 100644 --- a/spec/lib/gitlab/git/compare_spec.rb +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Git::Compare, seed_helper: true do let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) } let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) } - describe :commits do + describe '#commits' do subject do compare.commits.map(&:id) end @@ -42,7 +42,7 @@ describe Gitlab::Git::Compare, seed_helper: true do end end - describe :diffs do + describe '#diffs' do subject do compare.diffs.map(&:new_path) end @@ -67,7 +67,7 @@ describe Gitlab::Git::Compare, seed_helper: true do end end - describe :same do + describe '#same' do subject do compare.same end @@ -81,7 +81,7 @@ describe Gitlab::Git::Compare, seed_helper: true do end end - describe :commits_straight do + describe '#commits', 'straight compare' do subject do compare_straight.commits.map(&:id) end @@ -94,7 +94,7 @@ describe Gitlab::Git::Compare, seed_helper: true do it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) } end - describe :diffs_straight do + describe '#diffs', 'straight compare' do subject do compare_straight.diffs.map(&:new_path) end diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 47bdd7310d5..122c93dcd69 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do it { is_expected.to be_kind_of ::Array } end - describe :decorate! do + describe '#decorate!' do let(:file_count) { 3 } it 'modifies the array in place' do @@ -302,7 +302,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end end - describe :each do + describe '#each' do context 'when diff are too large' do let(:collection) do Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }]) diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 688e2a75373..83d2ff8f9b3 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(tree.select(&:file?).size).to eq(10) } it { expect(tree.select(&:submodule?).size).to eq(2) } - describe :dir do + describe '#dir?' do let(:dir) { tree.select(&:dir?).first } it { expect(dir).to be_kind_of Gitlab::Git::Tree } @@ -41,7 +41,7 @@ describe Gitlab::Git::Tree, seed_helper: true do end end - describe :file do + describe '#file?' do let(:file) { tree.select(&:file?).first } it { expect(file).to be_kind_of Gitlab::Git::Tree } @@ -50,21 +50,21 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(file.name).to eq('.gitignore') } end - describe :readme do + describe '#readme?' do let(:file) { tree.select(&:readme?).first } it { expect(file).to be_kind_of Gitlab::Git::Tree } it { expect(file.name).to eq('README.md') } end - describe :contributing do + describe '#contributing?' do let(:file) { tree.select(&:contributing?).first } it { expect(file).to be_kind_of Gitlab::Git::Tree } it { expect(file.name).to eq('CONTRIBUTING.md') } end - describe :submodule do + describe '#submodule?' do let(:submodule) { tree.select(&:submodule?).first } it { expect(submodule).to be_kind_of Gitlab::Git::Tree } diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb index 8d43b570e98..bcca4d4c746 100644 --- a/spec/lib/gitlab/git/util_spec.rb +++ b/spec/lib/gitlab/git/util_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Git::Util do - describe :count_lines do + describe '#count_lines' do [ ["", 0], ["foo", 1], diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 2f3bd4393b7..346cf0d117c 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -57,7 +57,7 @@ describe Gitlab::LDAP::User, lib: true do end end - describe :find_or_create do + describe 'find or create' do it "finds the user if already existing" do create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index e8caad00c44..8acec805584 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -6,6 +6,9 @@ describe SystemHook, models: true do let(:user) { create(:user) } let(:project) { create(:empty_project, namespace: user.namespace) } let(:group) { create(:group) } + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jg@example.com', password: 'mydummypass' } + end before do WebMock.stub_request(:post, system_hook.url) @@ -29,7 +32,7 @@ describe SystemHook, models: true do end it "user_create hook" do - create(:user) + Users::CreateService.new(nil, params).execute expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_create/, diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 3cee2b7714f..f3f48f951a8 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -109,7 +109,7 @@ describe Milestone, models: true do it { expect(milestone.percent_complete(user)).to eq(75) } end - describe :items_count do + describe '#is_empty?' do before do milestone.issues << create(:issue, project: project) milestone.issues << create(:closed_issue, project: project) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 67d48557184..09aa6e9337f 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -163,7 +163,7 @@ describe Namespace, models: true do end end - describe :rm_dir do + describe '#rm_dir', 'callback' do let!(:project) { create(:empty_project, namespace: namespace) } let!(:path) { File.join(Gitlab.config.repositories.storages.default['path'], namespace.full_path) } diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index e6a4583a8fb..c6c45d78990 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -5,7 +5,7 @@ describe PagesDomain, models: true do it { is_expected.to belong_to(:project) } end - describe :validate_domain do + describe 'validate domain' do subject { build(:pages_domain, domain: domain) } context 'is unique' do @@ -75,7 +75,7 @@ describe PagesDomain, models: true do end end - describe :url do + describe '#url' do subject { domain.url } context 'without the certificate' do @@ -91,7 +91,7 @@ describe PagesDomain, models: true do end end - describe :has_matching_key? do + describe '#has_matching_key?' do subject { domain.has_matching_key? } context 'for matching key' do @@ -107,7 +107,7 @@ describe PagesDomain, models: true do end end - describe :has_intermediates? do + describe '#has_intermediates?' do subject { domain.has_intermediates? } context 'for self signed' do @@ -133,7 +133,7 @@ describe PagesDomain, models: true do end end - describe :expired? do + describe '#expired?' do subject { domain.expired? } context 'for valid' do @@ -149,7 +149,7 @@ describe PagesDomain, models: true do end end - describe :subject do + describe '#subject' do let(:domain) { build(:pages_domain, :with_certificate) } subject { domain.subject } @@ -157,7 +157,7 @@ describe PagesDomain, models: true do it { is_expected.to eq('/CN=test-certificate') } end - describe :certificate_text do + describe '#certificate_text' do let(:domain) { build(:pages_domain, :with_certificate) } subject { domain.certificate_text } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 274e4f00a0a..585b87b828d 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1083,7 +1083,7 @@ describe Repository, models: true do end end - describe :skip_merged_commit do + describe 'skip_merges option' do subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map{ |k| k.id } } it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 90378179e32..a9e37be1157 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -81,6 +81,7 @@ describe User, models: true do it { is_expected.to validate_numericality_of(:projects_limit) } it { is_expected.to allow_value(0).for(:projects_limit) } it { is_expected.not_to allow_value(-1).for(:projects_limit) } + it { is_expected.not_to allow_value(Gitlab::Database::MAX_INT_VALUE + 1).for(:projects_limit) } it { is_expected.to validate_length_of(:bio).is_at_most(255) } @@ -360,22 +361,10 @@ describe User, models: true do end describe '#generate_password' do - it "executes callback when force_random_password specified" do - user = build(:user, force_random_password: true) - expect(user).to receive(:generate_password) - user.save - end - it "does not generate password by default" do user = create(:user, password: 'abcdefghe') expect(user.password).to eq('abcdefghe') end - - it "generates password when forcing random password" do - allow(Devise).to receive(:friendly_token).and_return('123456789') - user = create(:user, password: 'abcdefg', force_random_password: true) - expect(user.password).to eq('12345678') - end end describe 'authentication token' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index c481b7e72b1..a3de4702ad0 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -902,7 +902,7 @@ describe API::Projects, :api do end end - describe :fork_admin do + describe 'fork management' do let(:project_fork_target) { create(:empty_project) } let(:project_fork_source) { create(:empty_project, :public) } diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index d8bb562587d..b1aa793ec00 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -949,7 +949,7 @@ describe API::V3::Projects, api: true do end end - describe :fork_admin do + describe 'fork management' do let(:project_fork_target) { create(:empty_project) } let(:project_fork_source) { create(:empty_project, :public) } diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index 17bbb0b53c1..b38cbe74b85 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -263,4 +263,18 @@ describe API::V3::Users, api: true do expect(json_response['message']).to eq('404 User Not Found') end end + + describe 'POST /users' do + it 'creates confirmed user when confirm parameter is false' do + optional_attributes = { confirm: false } + attributes = attributes_for(:user).merge(optional_attributes) + + post v3_api('/users', admin), attributes + + user_id = json_response['id'] + new_user = User.find(user_id) + + expect(new_user).to be_confirmed + end + end end diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb index 60c9642ee2c..7dcdf54fd93 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/build_entity_spec.rb @@ -1,10 +1,16 @@ require 'spec_helper' describe BuildEntity do + let(:user) { create(:user) } let(:build) { create(:ci_build) } + let(:request) { double('request') } + + before do + allow(request).to receive(:user).and_return(user) + end let(:entity) do - described_class.new(build, request: double) + described_class.new(build, request: request) end subject { entity.as_json } @@ -22,6 +28,11 @@ describe BuildEntity do expect(subject).to include(:created_at, :updated_at) end + it 'contains details' do + expect(subject).to include :status + expect(subject[:status]).to include :icon, :favicon, :text, :label + end + context 'when build is a regular job' do it 'does not contain path to play action' do expect(subject).not_to include(:play_path) diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb new file mode 100644 index 00000000000..3cc791bca50 --- /dev/null +++ b/spec/serializers/build_serializer_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe BuildSerializer do + let(:user) { create(:user) } + + let(:serializer) do + described_class.new(user: user) + end + + subject { serializer.represent(resource) } + + describe '#represent' do + context 'when a single object is being serialized' do + let(:resource) { create(:ci_build) } + + it 'serializers the pipeline object' do + expect(subject[:id]).to eq resource.id + end + end + + context 'when multiple objects are being serialized' do + let(:resource) { create_list(:ci_build, 2) } + + it 'serializers the array of pipelines' do + expect(subject).not_to be_empty + end + end + end + + describe '#represent_status' do + context 'when represents only status' do + let(:resource) { create(:ci_build) } + let(:status) { resource.detailed_status(double('user')) } + + subject { serializer.represent_status(resource) } + + it 'serializes only status' do + expect(subject[:text]).to eq(status.text) + expect(subject[:label]).to eq(status.label) + expect(subject[:icon]).to eq(status.icon) + expect(subject[:favicon]).to eq(status.favicon) + end + end + end +end diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index ea87771e2a2..95eca5463eb 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -1,8 +1,15 @@ require 'spec_helper' describe DeploymentEntity do + let(:user) { create(:user) } + let(:request) { double('request') } + + before do + allow(request).to receive(:user).and_return(user) + end + let(:entity) do - described_class.new(deployment, request: double) + described_class.new(deployment, request: request) end let(:deployment) { create(:deployment) } diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index ccb72973f9c..93d5a21419d 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -30,7 +30,7 @@ describe PipelineEntity do .to include :duration, :finished_at expect(subject[:details]) .to include :stages, :artifacts, :manual_actions - expect(subject[:details][:status]).to include :icon, :text, :label + expect(subject[:details][:status]).to include :icon, :favicon, :text, :label end it 'contains flags' do diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 2aaef03cb93..8642b803844 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -94,4 +94,20 @@ describe PipelineSerializer do end end end + + describe '#represent_status' do + context 'when represents only status' do + let(:resource) { create(:ci_pipeline) } + let(:status) { resource.detailed_status(double('user')) } + + subject { serializer.represent_status(resource) } + + it 'serializes only status' do + expect(subject[:text]).to eq(status.text) + expect(subject[:label]).to eq(status.label) + expect(subject[:icon]).to eq(status.icon) + expect(subject[:favicon]).to eq(status.favicon) + end + end + end end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index 89428b4216e..c94902dbab8 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -16,7 +16,7 @@ describe StatusEntity do subject { entity.as_json } it 'contains status details' do - expect(subject).to include :text, :icon, :label, :group + expect(subject).to include :text, :icon, :favicon, :label, :group expect(subject).to include :has_details, :details_path end end diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index 5445b65f4e8..f1b2d3a4798 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -9,6 +9,19 @@ describe Ci::RetryPipelineService, '#execute', :services do context 'when user has ability to modify pipeline' do let(:user) { create(:admin) } + context 'when there are already retried jobs present' do + before do + create_build('rspec', :canceled, 0) + create_build('rspec', :failed, 0) + end + + it 'does not retry jobs that has already been retried' do + expect(statuses.first).to be_retried + expect { service.execute(pipeline) } + .to change { CommitStatus.count }.by(1) + end + end + context 'when there are failed builds in the last stage' do before do create_build('rspec 1', :success, 0) diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb index 92b84308f73..fe6a19e97ea 100644 --- a/spec/services/milestones/close_service_spec.rb +++ b/spec/services/milestones/close_service_spec.rb @@ -17,7 +17,7 @@ describe Milestones::CloseService, services: true do it { expect(milestone).to be_valid } it { expect(milestone).to be_closed } - describe :event do + describe 'event' do let(:event) { Event.recent.first } it { expect(event.milestone).to be_truthy } diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 8e614211116..e3be1989c93 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::ForkService, services: true do - describe :fork_by_user do + describe 'fork by user' do before do @from_namespace = create(:namespace) @from_user = create(:user, namespace: @from_namespace ) @@ -100,7 +100,7 @@ describe Projects::ForkService, services: true do end end - describe :fork_to_namespace do + describe 'fork to namespace' do before do @group_owner = create(:user) @developer = create(:user) diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb new file mode 100644 index 00000000000..5f79203701a --- /dev/null +++ b/spec/services/users/create_service_spec.rb @@ -0,0 +1,182 @@ +require 'spec_helper' + +describe Users::CreateService, services: true do + describe '#build' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' } + end + + context 'with an admin user' do + let(:admin_user) { create(:admin) } + let(:service) { described_class.new(admin_user, params) } + + it 'returns a valid user' do + expect(service.build).to be_valid + end + end + + context 'with non admin user' do + let(:user) { create(:user) } + let(:service) { described_class.new(user, params) } + + it 'raises AccessDeniedError exception' do + expect { service.build }.to raise_error Gitlab::Access::AccessDeniedError + end + end + + context 'with nil user' do + let(:service) { described_class.new(nil, params) } + + it 'returns a valid user' do + expect(service.build).to be_valid + end + end + end + + describe '#execute' do + let(:admin_user) { create(:admin) } + + context 'with an admin user' do + let(:service) { described_class.new(admin_user, params) } + + context 'when required parameters are provided' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' } + end + + it 'returns a persisted user' do + expect(service.execute).to be_persisted + end + + it 'persists the given attributes' do + user = service.execute + user.reload + + expect(user).to have_attributes( + name: params[:name], + username: params[:username], + email: params[:email], + password: params[:password], + created_by_id: admin_user.id + ) + end + + it 'user is not confirmed if skip_confirmation param is not present' do + expect(service.execute).not_to be_confirmed + end + + it 'logs the user creation' do + expect(service).to receive(:log_info).with("User \"John Doe\" (jd@example.com) was created") + + service.execute + end + + it 'executes system hooks ' do + system_hook_service = spy(:system_hook_service) + + expect(service).to receive(:system_hook_service).and_return(system_hook_service) + + user = service.execute + + expect(system_hook_service).to have_received(:execute_hooks_for).with(user, :create) + end + + it 'does not send a notification email' do + notification_service = spy(:notification_service) + + expect(service).not_to receive(:notification_service) + + service.execute + + expect(notification_service).not_to have_received(:new_user) + end + end + + context 'when force_random_password parameter is true' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', force_random_password: true } + end + + it 'generates random password' do + user = service.execute + + expect(user.password).not_to eq 'mydummypass' + expect(user.password).to be_present + end + end + + context 'when skip_confirmation parameter is true' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true } + end + + it 'confirms the user' do + expect(service.execute).to be_confirmed + end + end + + context 'when reset_password parameter is true' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', reset_password: true } + end + + it 'resets password even if a password parameter is given' do + expect(service.execute).to be_recently_sent_password_reset + end + + it 'sends a notification email' do + notification_service = spy(:notification_service) + + expect(service).to receive(:notification_service).and_return(notification_service) + + user = service.execute + + expect(notification_service).to have_received(:new_user).with(user, an_instance_of(String)) + end + end + end + + context 'with nil user' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true } + end + let(:service) { described_class.new(nil, params) } + + context 'when "send_user_confirmation_email" application setting is true' do + before do + current_application_settings = double(:current_application_settings, send_user_confirmation_email: true, signup_enabled?: true) + allow(service).to receive(:current_application_settings).and_return(current_application_settings) + end + + it 'does not confirm the user' do + expect(service.execute).not_to be_confirmed + end + end + + context 'when "send_user_confirmation_email" application setting is false' do + before do + current_application_settings = double(:current_application_settings, send_user_confirmation_email: false, signup_enabled?: true) + allow(service).to receive(:current_application_settings).and_return(current_application_settings) + end + + it 'confirms the user' do + expect(service.execute).to be_confirmed + end + + it 'persists the given attributes' do + user = service.execute + user.reload + + expect(user).to have_attributes( + name: params[:name], + username: params[:username], + email: params[:email], + password: params[:password], + created_by_id: nil, + admin: false + ) + end + end + end + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index f1d226b6ae3..648b0380f18 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -37,9 +37,10 @@ module TestEnv 'conflict-too-large' => '39fa04f', 'deleted-image-test' => '6c17798', 'wip' => 'b9238ee', - 'csv' => '3dd0896' + 'csv' => '3dd0896', + 'v1.1.0' => 'b83d6e3' }.freeze - + # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily # need to keep all the branches in sync. # We currently only need a subset of the branches diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index c101f6f164d..e4aeaeca508 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' describe 'projects/pipelines/show' do include Devise::Test::ControllerHelpers + let(:user) { create(:user) } let(:project) { create(:project) } - let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, user: user) } before do controller.prepend_view_path('app/views/projects') @@ -21,6 +22,7 @@ describe 'projects/pipelines/show' do assign(:project, project) assign(:pipeline, pipeline) + assign(:commit, project.commit) allow(view).to receive(:can?).and_return(true) end @@ -31,6 +33,12 @@ describe 'projects/pipelines/show' do expect(rendered).to have_css('.js-pipeline-graph') expect(rendered).to have_css('.js-grouped-pipeline-dropdown') + # header + expect(rendered).to have_text("##{pipeline.id}") + expect(rendered).to have_css('time', text: pipeline.created_at.strftime("%b %d, %Y")) + expect(rendered).to have_selector(%Q(img[alt$="#{pipeline.user.name}'s avatar"])) + expect(rendered).to have_link(pipeline.user.name, href: user_path(pipeline.user)) + # stages expect(rendered).to have_text('Build') expect(rendered).to have_text('Test') diff --git a/yarn.lock b/yarn.lock index fb3bfce4d11..f254668646c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4450,6 +4450,10 @@ verror@1.3.6: dependencies: extsprintf "1.0.2" +visibilityjs@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63" + vm-browserify@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" |