diff options
489 files changed, 8781 insertions, 4406 deletions
diff --git a/.gitignore b/.gitignore index 82b3d08f7a8..aecaae95b8c 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ eslint-report.html /config/redis.queues.yml /config/redis.shared_state.yml /config/unicorn.rb +/config/puma.rb /config/secrets.yml /config/sidekiq.yml /config/registry.key diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b3593df8b13..c3163b687b4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.5-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29" .dedicated-runner: &dedicated-runner retry: 1 @@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git - gitlab-org .default-cache: &default-cache - key: "ruby-2.4.4-debian-stretch-with-yarn" + key: "ruby-2.4.5-debian-stretch-with-yarn" paths: - vendor/ruby - .yarn-cache/ @@ -75,11 +75,6 @@ stages: - mysql:5.7 - redis:alpine -.rails5-variables: &rails5-variables - script: - - export RAILS5=${RAILS5} - - export BUNDLE_GEMFILE=${BUNDLE_GEMFILE} - .rails5: &rails5 allow_failure: true only: @@ -139,7 +134,7 @@ stages: - export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}" - apk add --update openssl - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME - - chmod 755 $SCRIPT_NAME + - chmod 755 $(basename $SCRIPT_NAME) .rake-exec: &rake-exec <<: *dedicated-no-docs-no-db-pull-cache-job @@ -150,7 +145,6 @@ stages: <<: *dedicated-runner <<: *except-docs-and-qa <<: *pull-cache - <<: *rails5-variables stage: test script: - JOB_NAME=( $CI_JOB_NAME ) @@ -594,7 +588,7 @@ static-analysis: script: - scripts/static-analysis cache: - key: "ruby-2.4.4-debian-stretch-with-yarn-and-rubocop" + key: "ruby-2.4.5-debian-stretch-with-yarn-and-rubocop" paths: - vendor/ruby - .yarn-cache/ @@ -929,3 +923,93 @@ no_ee_check: - scripts/no-ee-check only: - //@gitlab-org/gitlab-ce + +# GitLab Review apps +review: + image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base + stage: test + allow_failure: true + before_script: + - gem install gitlab --no-document + variables: + GIT_DEPTH: "1" + HOST_SUFFIX: "$CI_ENVIRONMENT_SLUG" + DOMAIN: "-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN" + GITLAB_HELM_CHART_REF: "master" + script: + - export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION) + - export GITALY_VERSION=$(<GITALY_SERVER_VERSION) + - export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION) + - source ./scripts/review_apps/review-apps.sh + - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng + - check_kube_domain + - download_gitlab_chart + - ensure_namespace + - install_tiller + - create_secret + - install_external_dns + - deploy + environment: + name: review/$CI_COMMIT_REF_NAME + url: https://gitlab-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN + on_stop: stop_review + only: + refs: + - branches@gitlab-org/gitlab-ce + - branches@gitlab-org/gitlab-ee + kubernetes: active + except: + refs: + - master + - /(^docs[\/-].*|.*-docs$)/ + +stop_review: + <<: *single-script-job + image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base + stage: test + allow_failure: true + cache: {} + dependencies: [] + variables: + SCRIPT_NAME: "review_apps/review-apps.sh" + script: + - source $(basename "${SCRIPT_NAME}") + - delete + - cleanup + when: manual + environment: + name: review/$CI_COMMIT_REF_NAME + action: stop + only: + refs: + - branches@gitlab-org/gitlab-ce + - branches@gitlab-org/gitlab-ee + kubernetes: active + except: + - master + - /(^docs[\/-].*|.*-docs$)/ + +schedule:review_apps_cleanup: + <<: *dedicated-no-docs-pull-cache-job + image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base + stage: build + allow_failure: true + cache: {} + dependencies: [] + before_script: + - gem install gitlab --no-document + variables: + GIT_DEPTH: "1" + script: + - ruby -rrubygems scripts/review_apps/automated_cleanup.rb + environment: + name: review/auto-cleanup + action: stop + only: + refs: + - schedules@gitlab-org/gitlab-ce + - schedules@gitlab-org/gitlab-ee + kubernetes: active + except: + - tags + - /(^docs[\/-].*|.*-docs$)/ diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index 64b54b171f7..69cf7fe1548 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -16,7 +16,6 @@ Set the title to: `[Security] Description of the original issue` - [ ] Add a link to the MR to the [links section](#links) - [ ] Add a link to an EE MR if required - [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**. -- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping. #### Backports @@ -26,7 +25,8 @@ Set the title to: `[Security] Description of the original issue` - [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable) - [ ] Create each MR targetting the security branch `security-X-Y` - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR -- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager. +- [ ] Add the ~"Merge into Security" label to all of the MRs. +- [ ] Make sure all MRs have a link in the [links section](#links) [secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script diff --git a/.rubocop.yml b/.rubocop.yml index 0f4018326a1..a95ded8af1c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -76,10 +76,14 @@ Naming/FileName: - 'qa/qa/specs/**/*' - 'qa/bin/*' - 'config/**/*' + - 'ee/config/**/*' - 'lib/generators/**/*' - 'locale/unfound_translations.rb' - 'ee/locale/unfound_translations.rb' - 'ee/lib/generators/**/*' + - 'qa/qa/scenario/test/integration/ldap_no_tls.rb' + - 'qa/qa/scenario/test/integration/ldap_tls.rb' + IgnoreExecutableScripts: true AllowedAcronyms: - EE diff --git a/.ruby-version b/.ruby-version index 79a614418f7..59aa62c1fa4 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4.4 +2.4.5 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 9da0a092a0d..6da4de57dc6 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -8.4.0
\ No newline at end of file +8.4.1 @@ -153,6 +153,11 @@ group :unicorn do gem 'unicorn-worker-killer', '~> 0.4.4' end +group :puma do + gem 'puma', '~> 3.12', require: false + gem 'puma_worker_killer', require: false +end + # State machine gem 'state_machines-activerecord', '~> 0.5.1' @@ -417,8 +422,7 @@ end gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly' gem 'grpc', '~> 1.15.0' -# Locked until https://github.com/google/protobuf/issues/4210 is closed -gem 'google-protobuf', '= 3.5.1' +gem 'google-protobuf', '~> 3.6' gem 'toml-rb', '~> 1.0.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index a39788bee9f..e533b564d15 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -303,7 +303,7 @@ GEM mime-types (~> 3.0) representable (~> 3.0) retriable (>= 2.0, < 4.0) - google-protobuf (3.5.1) + google-protobuf (3.6.1) googleapis-common-protos-types (1.0.2) google-protobuf (~> 3.0) googleauth (0.6.6) @@ -547,7 +547,7 @@ GEM orm_adapter (0.5.0) os (1.0.0) parallel (1.12.1) - parser (2.5.1.0) + parser (2.5.1.2) ast (~> 2.4.0) parslet (1.8.2) peek (1.0.1) @@ -598,6 +598,10 @@ GEM pry-rails (0.3.6) pry (>= 0.10.4) public_suffix (3.0.3) + puma (3.12.0) + puma_worker_killer (0.1.0) + get_process_mem (~> 0.2) + puma (>= 2.7, < 4) pyu-ruby-sasl (0.0.3.3) rack (1.6.10) rack-accept (0.4.5) @@ -1005,7 +1009,7 @@ DEPENDENCIES gitlab_omniauth-ldap (~> 2.0.4) gon (~> 6.2) google-api-client (~> 0.23) - google-protobuf (= 3.5.1) + google-protobuf (~> 3.6) gpgme grape (~> 1.1) grape-entity (~> 0.7.1) @@ -1076,6 +1080,8 @@ DEPENDENCIES prometheus-client-mmap (~> 0.9.4) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) + puma (~> 3.12) + puma_worker_killer rack-attack (~> 4.4.1) rack-cors (~> 1.0.0) rack-oauth2 (~> 1.2.1) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 1421edb1d39..b24911f3bb2 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -306,7 +306,7 @@ GEM mime-types (~> 3.0) representable (~> 3.0) retriable (>= 2.0, < 4.0) - google-protobuf (3.5.1) + google-protobuf (3.6.1) googleapis-common-protos-types (1.0.2) google-protobuf (~> 3.0) googleauth (0.6.6) @@ -551,7 +551,7 @@ GEM orm_adapter (0.5.0) os (1.0.0) parallel (1.12.1) - parser (2.5.1.0) + parser (2.5.1.2) ast (~> 2.4.0) parslet (1.8.2) peek (1.0.1) @@ -602,6 +602,10 @@ GEM pry-rails (0.3.6) pry (>= 0.10.4) public_suffix (3.0.3) + puma (3.12.0) + puma_worker_killer (0.1.0) + get_process_mem (~> 0.2) + puma (>= 2.7, < 4) pyu-ruby-sasl (0.0.3.3) rack (2.0.5) rack-accept (0.4.5) @@ -1014,7 +1018,7 @@ DEPENDENCIES gitlab_omniauth-ldap (~> 2.0.4) gon (~> 6.2) google-api-client (~> 0.23) - google-protobuf (= 3.5.1) + google-protobuf (~> 3.6) gpgme grape (~> 1.1) grape-entity (~> 0.7.1) @@ -1085,6 +1089,8 @@ DEPENDENCIES prometheus-client-mmap (~> 0.9.4) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) + puma (~> 3.12) + puma_worker_killer rack-attack (~> 4.4.1) rack-cors (~> 1.0.0) rack-oauth2 (~> 1.2.1) diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index de4566bb119..05de970e387 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -6,10 +6,12 @@ import Pager from './pager'; import { localTimeAgo } from './lib/utils/datetime_utility'; export default class Activities { - constructor() { - Pager.init(20, true, false, data => data, this.updateTooltips); + constructor(container = '') { + this.container = container; - $('.event-filter-link').on('click', (e) => { + Pager.init(20, true, false, data => data, this.updateTooltips, this.container); + + $('.event-filter-link').on('click', e => { e.preventDefault(); this.toggleFilter(e.currentTarget); this.reloadActivities(); @@ -22,7 +24,7 @@ export default class Activities { reloadActivities() { $('.content_list').html(''); - Pager.init(20, true, false, data => data, this.updateTooltips); + Pager.init(20, true, false, data => data, this.updateTooltips, this.container); } toggleFilter(sender) { diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 75477ebb3b3..623cda5679a 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -53,6 +53,9 @@ export default Vue.extend({ const { issuesSize } = this.list; return `${n__('%d issue', '%d issues', issuesSize)}`; }, + isNewIssueShown() { + return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed'); + } }, watch: { filter: { diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 4e8fe16160a..427a0868b0c 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -46,7 +46,7 @@ export default { selectable: true, data: (term, callback) => { this.loading = true; - return Api.groupProjects(this.groupId, term, {}, projects => { + return Api.groupProjects(this.groupId, term, {with_issues_enabled: true}, projects => { this.loading = false; callback(projects); }); diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js index 1474d93dde6..a37838694ec 100644 --- a/app/assets/javascripts/breadcrumb.js +++ b/app/assets/javascripts/breadcrumb.js @@ -1,6 +1,6 @@ import $ from 'jquery'; -export const addTooltipToEl = (el) => { +export const addTooltipToEl = el => { const textEl = el.querySelector('.js-breadcrumb-item-text'); if (textEl && textEl.scrollWidth > textEl.offsetWidth) { @@ -14,17 +14,18 @@ export default () => { const breadcrumbs = document.querySelector('.js-breadcrumbs-list'); if (breadcrumbs) { - const topLevelLinks = [...breadcrumbs.children].filter(el => !el.classList.contains('dropdown')) + const topLevelLinks = [...breadcrumbs.children] + .filter(el => !el.classList.contains('dropdown')) .map(el => el.querySelector('a')) .filter(el => el); const $expander = $('.js-breadcrumbs-collapsed-expander'); topLevelLinks.forEach(el => addTooltipToEl(el)); - $expander.closest('.dropdown') - .on('show.bs.dropdown hide.bs.dropdown', (e) => { - $('.js-breadcrumbs-collapsed-expander', e.currentTarget).toggleClass('open') - .tooltip('hide'); - }); + $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', e => { + $('.js-breadcrumbs-collapsed-expander', e.currentTarget) + .toggleClass('open') + .tooltip('hide'); + }); } }; diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index e338376fcaa..97a1645aa51 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -12,16 +12,16 @@ export default class BuildArtifacts { } // eslint-disable-next-line class-methods-use-this disablePropagation() { - $('.top-block').on('click', '.download', function (e) { + $('.top-block').on('click', '.download', function(e) { return e.stopPropagation(); }); - return $('.tree-holder').on('click', 'tr[data-link] a', function (e) { + return $('.tree-holder').on('click', 'tr[data-link] a', function(e) { return e.stopImmediatePropagation(); }); } // eslint-disable-next-line class-methods-use-this setupEntryClick() { - return $('.tree-holder').on('click', 'tr[data-link]', function () { + return $('.tree-holder').on('click', 'tr[data-link]', function() { visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink)); }); } @@ -37,11 +37,15 @@ export default class BuildArtifacts { // We want the tooltip to show if you hover anywhere on the row // But be placed below and in the middle of the file name $('.js-artifact-tree-row') - .on('mouseenter', (e) => { - $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show'); + .on('mouseenter', e => { + $(e.currentTarget) + .find('.js-artifact-tree-tooltip') + .tooltip('show'); }) - .on('mouseleave', (e) => { - $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide'); + .on('mouseleave', e => { + $(e.currentTarget) + .find('.js-artifact-tree-tooltip') + .tooltip('hide'); }); } } diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js index b33adff609f..1089d0a72d3 100644 --- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js @@ -7,11 +7,13 @@ import statusCodes from '../lib/utils/http_status'; import VariableList from './ci_variable_list'; function generateErrorBoxContent(errors) { - const errorList = [].concat(errors).map(errorString => ` + const errorList = [].concat(errors).map( + errorString => ` <li> ${_.escape(errorString)} </li> - `); + `, + ); return ` <p> @@ -25,13 +27,7 @@ function generateErrorBoxContent(errors) { // Used for the variable list on CI/CD projects/groups settings page export default class AjaxVariableList { - constructor({ - container, - saveButton, - errorBox, - formField = 'variables', - saveEndpoint, - }) { + constructor({ container, saveButton, errorBox, formField = 'variables', saveEndpoint }) { this.container = container; this.saveButton = saveButton; this.errorBox = errorBox; @@ -58,18 +54,21 @@ export default class AjaxVariableList { // to match it up in `updateRowsWithPersistedVariables` this.variableList.toggleEnableRow(false); - return axios.patch(this.saveEndpoint, { - variables_attributes: this.variableList.getAllData(), - }, { - // We want to be able to process the `res.data` from a 400 error response - // and print the validation messages such as duplicate variable keys - validateStatus: status => ( - status >= statusCodes.OK && - status < statusCodes.MULTIPLE_CHOICES - ) || - status === statusCodes.BAD_REQUEST, - }) - .then((res) => { + return axios + .patch( + this.saveEndpoint, + { + variables_attributes: this.variableList.getAllData(), + }, + { + // We want to be able to process the `res.data` from a 400 error response + // and print the validation messages such as duplicate variable keys + validateStatus: status => + (status >= statusCodes.OK && status < statusCodes.MULTIPLE_CHOICES) || + status === statusCodes.BAD_REQUEST, + }, + ) + .then(res => { loadingIcon.classList.toggle('hide', true); this.variableList.toggleEnableRow(true); @@ -90,18 +89,21 @@ export default class AjaxVariableList { } updateRowsWithPersistedVariables(persistedVariables = []) { - const persistedVariableMap = [].concat(persistedVariables).reduce((variableMap, variable) => ({ - ...variableMap, - [variable.key]: variable, - }), {}); + const persistedVariableMap = [].concat(persistedVariables).reduce( + (variableMap, variable) => ({ + ...variableMap, + [variable.key]: variable, + }), + {}, + ); - this.container.querySelectorAll('.js-row').forEach((row) => { + this.container.querySelectorAll('.js-row').forEach(row => { // If we submitted a row that was destroyed, remove it so we don't try // to destroy it again which would cause a BE error const destroyInput = row.querySelector('.js-ci-variable-input-destroy'); if (convertPermissionToBoolean(destroyInput.value)) { row.remove(); - // Update the ID input so any future edits and `_destroy` will apply on the BE + // Update the ID input so any future edits and `_destroy` will apply on the BE } else { const key = row.querySelector('.js-ci-variable-input-key').value; const persistedVariable = persistedVariableMap[key]; diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index 47efb3a8cee..7bdc18ce03e 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -16,10 +16,7 @@ function createEnvironmentItem(value) { } export default class VariableList { - constructor({ - container, - formField, - }) { + constructor({ container, formField }) { this.$container = $(container); this.formField = formField; this.environmentDropdownMap = new WeakMap(); @@ -71,7 +68,7 @@ export default class VariableList { this.initRow(rowEl); }); - this.$container.on('click', '.js-row-remove-button', (e) => { + this.$container.on('click', '.js-row-remove-button', e => { e.preventDefault(); this.removeRow($(e.currentTarget).closest('.js-row')); }); @@ -81,7 +78,7 @@ export default class VariableList { .join(','); // Remove any empty rows except the last row - this.$container.on('blur', inputSelector, (e) => { + this.$container.on('blur', inputSelector, e => { const $row = $(e.currentTarget).closest('.js-row'); if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) { @@ -136,7 +133,7 @@ export default class VariableList { $rowClone.removeAttr('data-is-persisted'); // Reset the inputs to their defaults - Object.keys(this.inputMap).forEach((name) => { + Object.keys(this.inputMap).forEach(name => { const entry = this.inputMap[name]; $rowClone.find(entry.selector).val(entry.default); }); @@ -171,7 +168,7 @@ export default class VariableList { } checkIfRowTouched($row) { - return Object.keys(this.inputMap).some((name) => { + return Object.keys(this.inputMap).some(name => { const entry = this.inputMap[name]; const $el = $row.find(entry.selector); return $el.length && $el.val() !== entry.default; @@ -190,11 +187,14 @@ export default class VariableList { getAllData() { // Ignore the last empty row because we don't want to try persist // a blank variable and run into validation problems. - const validRows = this.$container.find('.js-row').toArray().slice(0, -1); + const validRows = this.$container + .find('.js-row') + .toArray() + .slice(0, -1); - return validRows.map((rowEl) => { + return validRows.map(rowEl => { const resultant = {}; - Object.keys(this.inputMap).forEach((name) => { + Object.keys(this.inputMap).forEach(name => { const entry = this.inputMap[name]; const $input = $(rowEl).find(entry.selector); if ($input.length) { @@ -207,11 +207,16 @@ export default class VariableList { } getEnvironmentValues() { - const valueMap = this.$container.find(this.inputMap.environment_scope.selector).toArray() - .reduce((prevValueMap, envInput) => ({ - ...prevValueMap, - [envInput.value]: envInput.value, - }), {}); + const valueMap = this.$container + .find(this.inputMap.environment_scope.selector) + .toArray() + .reduce( + (prevValueMap, envInput) => ({ + ...prevValueMap, + [envInput.value]: envInput.value, + }), + {}, + ); return Object.keys(valueMap).map(createEnvironmentItem); } diff --git a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js index 7cd5916ac9c..e7111c666a2 100644 --- a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js @@ -2,10 +2,7 @@ import $ from 'jquery'; import VariableList from './ci_variable_list'; // Used for the variable list on scheduled pipeline edit page -export default function setupNativeFormVariableList({ - container, - formField = 'variables', -}) { +export default function setupNativeFormVariableList({ container, formField = 'variables' }) { const $container = $(container); const variableList = new VariableList({ diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index d90db7b103c..106ac3cb516 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -76,12 +76,8 @@ export default class ClusterStore { this.state.status = serverState.status; this.state.statusReason = serverState.status_reason; - serverState.applications.forEach((serverAppEntry) => { - const { - name: appId, - status, - status_reason: statusReason, - } = serverAppEntry; + serverState.applications.forEach(serverAppEntry => { + const { name: appId, status, status_reason: statusReason } = serverAppEntry; this.state.applications[appId] = { ...(this.state.applications[appId] || {}), diff --git a/app/assets/javascripts/comment_type_toggle.js b/app/assets/javascripts/comment_type_toggle.js index c74184949df..a259667bb75 100644 --- a/app/assets/javascripts/comment_type_toggle.js +++ b/app/assets/javascripts/comment_type_toggle.js @@ -24,36 +24,44 @@ class CommentTypeToggle { setConfig() { const config = { - InputSetter: [{ - input: this.noteTypeInput, - valueAttribute: 'data-value', - }, - { - input: this.submitButton, - valueAttribute: 'data-submit-text', - }], + InputSetter: [ + { + input: this.noteTypeInput, + valueAttribute: 'data-value', + }, + { + input: this.submitButton, + valueAttribute: 'data-submit-text', + }, + ], }; if (this.closeButton) { - config.InputSetter.push({ - input: this.closeButton, - valueAttribute: 'data-close-text', - }, { - input: this.closeButton, - valueAttribute: 'data-close-text', - inputAttribute: 'data-alternative-text', - }); + config.InputSetter.push( + { + input: this.closeButton, + valueAttribute: 'data-close-text', + }, + { + input: this.closeButton, + valueAttribute: 'data-close-text', + inputAttribute: 'data-alternative-text', + }, + ); } if (this.reopenButton) { - config.InputSetter.push({ - input: this.reopenButton, - valueAttribute: 'data-reopen-text', - }, { - input: this.reopenButton, - valueAttribute: 'data-reopen-text', - inputAttribute: 'data-alternative-text', - }); + config.InputSetter.push( + { + input: this.reopenButton, + valueAttribute: 'data-reopen-text', + }, + { + input: this.reopenButton, + valueAttribute: 'data-reopen-text', + inputAttribute: 'data-alternative-text', + }, + ); } return config; diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 30d9b656fec..d4ecfa4aa93 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -9,44 +9,60 @@ const viewModes = ['two-up', 'swipe']; export default class ImageFile { constructor(file) { this.file = file; - this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) { - return function(deletedWidth, deletedHeight) { - return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) { - _this.initViewModes(); - - // Load two-up view after images are loaded - // so that we can display the correct width and height information - const $images = $('.two-up.view img', _this.file); - - $images.waitForImages(function() { - _this.initView('two-up'); + this.requestImageInfo( + $('.two-up.view .frame.deleted img', this.file), + (function(_this) { + return function(deletedWidth, deletedHeight) { + return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function( + width, + height, + ) { + _this.initViewModes(); + + // Load two-up view after images are loaded + // so that we can display the correct width and height information + const $images = $('.two-up.view img', _this.file); + + $images.waitForImages(function() { + _this.initView('two-up'); + }); }); - }); - }; - })(this)); + }; + })(this), + ); } initViewModes() { const viewMode = viewModes[0]; $('.view-modes', this.file).removeClass('hide'); - $('.view-modes-menu', this.file).on('click', 'li', (function(_this) { - return function(event) { - if (!$(event.currentTarget).hasClass('active')) { - return _this.activateViewMode(event.currentTarget.className); - } - }; - })(this)); + $('.view-modes-menu', this.file).on( + 'click', + 'li', + (function(_this) { + return function(event) { + if (!$(event.currentTarget).hasClass('active')) { + return _this.activateViewMode(event.currentTarget.className); + } + }; + })(this), + ); return this.activateViewMode(viewMode); } activateViewMode(viewMode) { - $('.view-modes-menu li', this.file).removeClass('active').filter("." + viewMode).addClass('active'); - return $(".view:visible:not(." + viewMode + ")", this.file).fadeOut(200, (function(_this) { - return function() { - $(".view." + viewMode, _this.file).fadeIn(200); - return _this.initView(viewMode); - }; - })(this)); + $('.view-modes-menu li', this.file) + .removeClass('active') + .filter('.' + viewMode) + .addClass('active'); + return $('.view:visible:not(.' + viewMode + ')', this.file).fadeOut( + 200, + (function(_this) { + return function() { + $('.view.' + viewMode, _this.file).fadeIn(200); + return _this.initView(viewMode); + }; + })(this), + ); } initView(viewMode) { @@ -63,135 +79,154 @@ export default class ImageFile { $body.css('user-select', 'none'); }); - $body.off('mouseup').off('mousemove').on('mouseup', function() { - dragging = false; - $body.css('user-select', ''); - }) - .on('mousemove', function(e) { - var left; - if (!dragging) return; - - left = e.pageX - ($offsetEl.offset().left + padding); - - callback(e, left); - }); + $body + .off('mouseup') + .off('mousemove') + .on('mouseup', function() { + dragging = false; + $body.css('user-select', ''); + }) + .on('mousemove', function(e) { + var left; + if (!dragging) return; + + left = e.pageX - ($offsetEl.offset().left + padding); + + callback(e, left); + }); } prepareFrames(view) { var maxHeight, maxWidth; maxWidth = 0; maxHeight = 0; - $('.frame', view).each((function(_this) { - return function(index, frame) { - var height, width; - width = $(frame).width(); - height = $(frame).height(); - maxWidth = width > maxWidth ? width : maxWidth; - return maxHeight = height > maxHeight ? height : maxHeight; - }; - })(this)).css({ - width: maxWidth, - height: maxHeight - }); + $('.frame', view) + .each( + (function(_this) { + return function(index, frame) { + var height, width; + width = $(frame).width(); + height = $(frame).height(); + maxWidth = width > maxWidth ? width : maxWidth; + return (maxHeight = height > maxHeight ? height : maxHeight); + }; + })(this), + ) + .css({ + width: maxWidth, + height: maxHeight, + }); return [maxWidth, maxHeight]; } views = { 'two-up': function() { - return $('.two-up.view .wrap', this.file).each((function(_this) { - return function(index, wrap) { - $('img', wrap).each(function() { - var currentWidth; - currentWidth = $(this).width(); - if (currentWidth > availWidth / 2) { - return $(this).width(availWidth / 2); - } - }); - return _this.requestImageInfo($('img', wrap), function(width, height) { - $('.image-info .meta-width', wrap).text(width + "px"); - $('.image-info .meta-height', wrap).text(height + "px"); - return $('.image-info', wrap).removeClass('hide'); - }); - }; - })(this)); + return $('.two-up.view .wrap', this.file).each( + (function(_this) { + return function(index, wrap) { + $('img', wrap).each(function() { + var currentWidth; + currentWidth = $(this).width(); + if (currentWidth > availWidth / 2) { + return $(this).width(availWidth / 2); + } + }); + return _this.requestImageInfo($('img', wrap), function(width, height) { + $('.image-info .meta-width', wrap).text(width + 'px'); + $('.image-info .meta-height', wrap).text(height + 'px'); + return $('.image-info', wrap).removeClass('hide'); + }); + }; + })(this), + ); }, - 'swipe': function() { + swipe() { var maxHeight, maxWidth; maxWidth = 0; maxHeight = 0; - return $('.swipe.view', this.file).each((function(_this) { - return function(index, view) { - var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; - ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref; - $swipeFrame = $('.swipe-frame', view); - $swipeWrap = $('.swipe-wrap', view); - $swipeBar = $('.swipe-bar', view); - - $swipeFrame.css({ - width: maxWidth + 16, - height: maxHeight + 28 - }); - $swipeWrap.css({ - width: maxWidth + 1, - height: maxHeight + 2 - }); - // Set swipeBar left position to match image frame - $swipeBar.css({ - left: 1 - }); - - wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10); - - _this.initDraggable($swipeBar, wrapPadding, function(e, left) { - if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) { - $swipeWrap.width((maxWidth + 1) - left); - $swipeBar.css('left', left); - } - }); - }; - })(this)); + return $('.swipe.view', this.file).each( + (function(_this) { + return function(index, view) { + var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; + (ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref); + $swipeFrame = $('.swipe-frame', view); + $swipeWrap = $('.swipe-wrap', view); + $swipeBar = $('.swipe-bar', view); + + $swipeFrame.css({ + width: maxWidth + 16, + height: maxHeight + 28, + }); + $swipeWrap.css({ + width: maxWidth + 1, + height: maxHeight + 2, + }); + // Set swipeBar left position to match image frame + $swipeBar.css({ + left: 1, + }); + + wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10); + + _this.initDraggable($swipeBar, wrapPadding, function(e, left) { + if (left > 0 && left < $swipeFrame.width() - wrapPadding * 2) { + $swipeWrap.width(maxWidth + 1 - left); + $swipeBar.css('left', left); + } + }); + }; + })(this), + ); }, 'onion-skin': function() { var dragTrackWidth, maxHeight, maxWidth; maxWidth = 0; maxHeight = 0; dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width(); - return $('.onion-skin.view', this.file).each((function(_this) { - return function(index, view) { - var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false; - ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref; - $frame = $('.onion-skin-frame', view); - $frameAdded = $('.frame.added', view); - $track = $('.drag-track', view); - $dragger = $('.dragger', $track); - - $frame.css({ - width: maxWidth + 16, - height: maxHeight + 28 - }); - $('.swipe-wrap', view).css({ - width: maxWidth + 1, - height: maxHeight + 2 - }); - $dragger.css({ - left: dragTrackWidth - }); - - $frameAdded.css('opacity', 1); - framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10); - - _this.initDraggable($dragger, framePadding, function(e, left) { - var opacity = left / dragTrackWidth; - - if (opacity >= 0 && opacity <= 1) { - $dragger.css('left', left); - $frameAdded.css('opacity', opacity); - } - }); - }; - })(this)); - } - } + return $('.onion-skin.view', this.file).each( + (function(_this) { + return function(index, view) { + var $frame, + $track, + $dragger, + $frameAdded, + framePadding, + ref, + dragging = false; + (ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref); + $frame = $('.onion-skin-frame', view); + $frameAdded = $('.frame.added', view); + $track = $('.drag-track', view); + $dragger = $('.dragger', $track); + + $frame.css({ + width: maxWidth + 16, + height: maxHeight + 28, + }); + $('.swipe-wrap', view).css({ + width: maxWidth + 1, + height: maxHeight + 2, + }); + $dragger.css({ + left: dragTrackWidth, + }); + + $frameAdded.css('opacity', 1); + framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10); + + _this.initDraggable($dragger, framePadding, function(e, left) { + var opacity = left / dragTrackWidth; + + if (opacity >= 0 && opacity <= 1) { + $dragger.css('left', left); + $frameAdded.css('opacity', opacity); + } + }); + }; + })(this), + ); + }, + }; requestImageInfo(img, callback) { const domImg = img.get(0); @@ -199,11 +234,14 @@ export default class ImageFile { if (domImg.complete) { return callback.call(this, domImg.naturalWidth, domImg.naturalHeight); } else { - return img.on('load', (function(_this) { - return function() { - return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight); - }; - })(this)); + return img.on( + 'load', + (function(_this) { + return function() { + return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight); + }; + })(this), + ); } } } diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 3d89bf1316e..340a93e4e66 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -19,11 +19,13 @@ export default () => { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); if (pipelineTableViewEl) { - // Update MR and Commits tabs - pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => { - if (event.detail.pipelines && + // Update MR and Commits tabs + pipelineTableViewEl.addEventListener('update-pipelines-count', event => { + if ( + event.detail.pipelines && event.detail.pipelines.count && - event.detail.pipelines.count.all) { + event.detail.pipelines.count.all + ) { const badge = document.querySelector('.js-pipelines-mr-count'); badge.textContent = event.detail.pipelines.count.all; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 4849b0fa3db..a2aa3d197e3 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -1,77 +1,73 @@ <script> - import PipelinesService from '../../pipelines/services/pipelines_service'; - import PipelineStore from '../../pipelines/stores/pipelines_store'; - import pipelinesMixin from '../../pipelines/mixins/pipelines'; +import PipelinesService from '../../pipelines/services/pipelines_service'; +import PipelineStore from '../../pipelines/stores/pipelines_store'; +import pipelinesMixin from '../../pipelines/mixins/pipelines'; - export default { - mixins: [ - pipelinesMixin, - ], - props: { - endpoint: { - type: String, - required: true, - }, - helpPagePath: { - type: String, - required: true, - }, - autoDevopsHelpPath: { - type: String, - required: true, - }, - errorStateSvgPath: { - type: String, - required: true, - }, - viewType: { - type: String, - required: false, - default: 'child', - }, +export default { + mixins: [pipelinesMixin], + props: { + endpoint: { + type: String, + required: true, }, + helpPagePath: { + type: String, + required: true, + }, + autoDevopsHelpPath: { + type: String, + required: true, + }, + errorStateSvgPath: { + type: String, + required: true, + }, + viewType: { + type: String, + required: false, + default: 'child', + }, + }, - data() { - const store = new PipelineStore(); + data() { + const store = new PipelineStore(); - return { - store, - state: store.state, - }; - }, + return { + store, + state: store.state, + }; + }, - computed: { - shouldRenderTable() { - return !this.isLoading && - this.state.pipelines.length > 0 && - !this.hasError; - }, - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, + computed: { + shouldRenderTable() { + return !this.isLoading && this.state.pipelines.length > 0 && !this.hasError; }, - created() { - this.service = new PipelinesService(this.endpoint); + shouldRenderErrorState() { + return this.hasError && !this.isLoading; }, - methods: { - successCallback(resp) { - // depending of the endpoint the response can either bring a `pipelines` key or not. - const pipelines = resp.data.pipelines || resp.data; - this.setCommonData(pipelines); + }, + created() { + this.service = new PipelinesService(this.endpoint); + }, + methods: { + successCallback(resp) { + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = resp.data.pipelines || resp.data; + this.setCommonData(pipelines); - const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { - detail: { - pipelines: resp.data, - }, - }); + const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { + detail: { + pipelines: resp.data, + }, + }); - // notifiy to update the count in tabs - if (this.$el.parentElement) { - this.$el.parentElement.dispatchEvent(updatePipelinesEvent); - } - }, + // notifiy to update the count in tabs + if (this.$el.parentElement) { + this.$el.parentElement.dispatchEvent(updatePipelinesEvent); + } }, - }; + }, +}; </script> <template> <div class="content-list pipelines"> diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js index 102b4ee8463..3a0ab119df6 100644 --- a/app/assets/javascripts/commit_merge_requests.js +++ b/app/assets/javascripts/commit_merge_requests.js @@ -50,7 +50,7 @@ export function createContent(mergeRequests) { if (mergeRequests.length === 0) { $content.text(s__('Commits|No related merge requests found')); } else { - mergeRequests.forEach((mergeRequest) => { + mergeRequests.forEach(mergeRequest => { const $header = createHeader($content.children().length, mergeRequests.length); const $item = createItem(mergeRequest); $content.append($header); @@ -64,8 +64,9 @@ export function createContent(mergeRequests) { export function fetchCommitMergeRequests() { const $container = $('.merge-requests'); - axios.get($container.data('projectCommitPath')) - .then((response) => { + axios + .get($container.data('projectCommitPath')) + .then(response => { const $content = createContent(response.data); $container.html($content); diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 9a3ea7a55b6..54e2589c707 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -32,22 +32,31 @@ export default class CommitsList { if (search === this.lastSearch) return Promise.resolve(); const commitsUrl = `${form.attr('action')}?${form.serialize()}`; this.content.fadeTo('fast', 0.5); - const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, { - [obj.name]: obj.value, - }), {}); + const params = form.serializeArray().reduce( + (acc, obj) => + Object.assign(acc, { + [obj.name]: obj.value, + }), + {}, + ); - return axios.get(form.attr('action'), { - params, - }) + return axios + .get(form.attr('action'), { + params, + }) .then(({ data }) => { this.lastSearch = search; this.content.html(data.html); this.content.fadeTo('fast', 1.0); // Change url so if user reload a page - search results are saved - window.history.replaceState({ - page: commitsUrl, - }, document.title, commitsUrl); + window.history.replaceState( + { + page: commitsUrl, + }, + document.title, + commitsUrl, + ); }) .catch(() => { this.content.fadeTo('fast', 1.0); @@ -75,8 +84,15 @@ export default class CommitsList { processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`); // Update commits count in the previous commits header. - commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); - $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`); + commitsCount += Number( + $(processedData) + .nextUntil('li.js-commit-header') + .first() + .find('li.commit').length, + ); + $commitsHeadersLast + .find('span.commits-count') + .text(`${commitsCount} ${pluralize('commit', commitsCount)}`); } localTimeAgo($processedData.find('.js-timeago')); diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index 50e2949ab55..fba30aea9ae 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -5,6 +5,14 @@ import 'bootstrap'; // custom jQuery functions $.fn.extend({ - disable() { return $(this).prop('disabled', true).addClass('disabled'); }, - enable() { return $(this).prop('disabled', false).removeClass('disabled'); }, + disable() { + return $(this) + .prop('disabled', true) + .addClass('disabled'); + }, + enable() { + return $(this) + .prop('disabled', false) + .removeClass('disabled'); + }, }); diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index b0c85c2572e..1000c310e35 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -13,19 +13,23 @@ function openConfirmDangerModal($form, text) { $submit.disable(); $input.focus(); - $('.js-confirm-danger-input').off('input').on('input', function handleInput() { - const confirmText = rstrip($(this).val()); - if (confirmText === confirmTextMatch) { - $submit.enable(); - } else { - $submit.disable(); - } - }); - $('.js-confirm-danger-submit').off('click').on('click', () => $form.submit()); + $('.js-confirm-danger-input') + .off('input') + .on('input', function handleInput() { + const confirmText = rstrip($(this).val()); + if (confirmText === confirmTextMatch) { + $submit.enable(); + } else { + $submit.disable(); + } + }); + $('.js-confirm-danger-submit') + .off('click') + .on('click', () => $form.submit()); } export default function initConfirmDangerModal() { - $(document).on('click', '.js-confirm-danger', (e) => { + $(document).on('click', '.js-confirm-danger', e => { e.preventDefault(); const $btn = $(e.target); const $form = $btn.closest('form'); diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 3a50e73ad85..dff0adba25a 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -20,8 +20,11 @@ export default class ContextualSidebar { } bindEvents() { - document.addEventListener('click', (e) => { - if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) { + document.addEventListener('click', e => { + if ( + !e.target.closest('.nav-sidebar') && + (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md') + ) { this.toggleCollapsedSidebar(true); } }); diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js index 8ef9aa7f529..916b190f469 100644 --- a/app/assets/javascripts/create_item_dropdown.js +++ b/app/assets/javascripts/create_item_dropdown.js @@ -36,7 +36,7 @@ export default class CreateItemDropdown { }, selectable: true, toggleLabel(selected) { - return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel; + return selected && 'id' in selected ? _.escape(selected.title) : this.defaultToggleLabel; }, fieldName: this.fieldName, text(item) { @@ -46,7 +46,7 @@ export default class CreateItemDropdown { return _.escape(item.id); }, onFilter: this.toggleCreateNewButton.bind(this), - clicked: (options) => { + clicked: options => { options.e.preventDefault(); this.onSelect(); }, @@ -77,9 +77,8 @@ export default class CreateItemDropdown { getData(term, callback) { this.getDataOption(term, (data = []) => { // Ensure the selected item isn't already in the data to avoid duplicates - const alreadyHasSelectedItem = this.selectedItem && data.some(item => - item.id === this.selectedItem.id, - ); + const alreadyHasSelectedItem = + this.selectedItem && data.some(item => item.id === this.selectedItem.id); let uniqueData = data; if (!alreadyHasSelectedItem) { @@ -106,9 +105,7 @@ export default class CreateItemDropdown { if (newValue) { this.selectedItem = this.createNewItemFromValue(newValue); - this.$dropdownContainer - .find('.js-dropdown-create-new-item code') - .text(newValue); + this.$dropdownContainer.find('.js-dropdown-create-new-item code').text(newValue); } this.toggleFooter(!newValue); diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index a999c21b2e9..28ca7d97314 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -37,7 +37,7 @@ export default class CreateLabelDropdown { addBinding() { const self = this; - this.$colorSuggestions.on('click', function (e) { + this.$colorSuggestions.on('click', function(e) { const $this = $(this); self.addColorValue(e, $this); }); @@ -47,7 +47,7 @@ export default class CreateLabelDropdown { this.$dropdownBack.on('click', this.resetForm.bind(this)); - this.$cancelButton.on('click', function (e) { + this.$cancelButton.on('click', function(e) { e.preventDefault(); e.stopPropagation(); @@ -79,13 +79,9 @@ export default class CreateLabelDropdown { } resetForm() { - this.$newLabelField - .val('') - .trigger('change'); + this.$newLabelField.val('').trigger('change'); - this.$newColorField - .val('') - .trigger('change'); + this.$newColorField.val('').trigger('change'); this.$colorPreview .css('background-color', '') @@ -97,31 +93,34 @@ export default class CreateLabelDropdown { e.preventDefault(); e.stopPropagation(); - Api.newLabel(this.namespacePath, this.projectPath, { - title: this.$newLabelField.val(), - color: this.$newColorField.val(), - }, (label) => { - this.$newLabelCreateButton.enable(); - - if (label.message) { - let errors; - - if (typeof label.message === 'string') { - errors = label.message; + Api.newLabel( + this.namespacePath, + this.projectPath, + { + title: this.$newLabelField.val(), + color: this.$newColorField.val(), + }, + label => { + this.$newLabelCreateButton.enable(); + + if (label.message) { + let errors; + + if (typeof label.message === 'string') { + errors = label.message; + } else { + errors = Object.keys(label.message) + .map(key => `${humanize(key)} ${label.message[key].join(', ')}`) + .join('<br/>'); + } + + this.$newLabelError.html(errors).show(); } else { - errors = Object.keys(label.message).map(key => - `${humanize(key)} ${label.message[key].join(', ')}`, - ).join('<br/>'); - } + this.$dropdownBack.trigger('click'); - this.$newLabelError - .html(errors) - .show(); - } else { - this.$dropdownBack.trigger('click'); - - $(document).trigger('created.label', label); - } - }); + $(document).trigger('created.label', label); + } + }, + ); } } diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index aa52f120fe7..3589599986d 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -95,8 +95,10 @@ export default { .catch(() => new Flash(s__('DeployKeys|Error enabling deploy key'))); }, disableKey(deployKey, callback) { - // eslint-disable-next-line no-alert - if (window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) { + if ( + // eslint-disable-next-line no-alert + window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?')) + ) { this.service .disableKey(deployKey.id) .then(this.fetchKeys) diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js index 9dc3b21f6f6..268a37008c5 100644 --- a/app/assets/javascripts/deploy_keys/service/index.js +++ b/app/assets/javascripts/deploy_keys/service/index.js @@ -8,17 +8,14 @@ export default class DeployKeysService { } getKeys() { - return this.axios.get() - .then(response => response.data); + return this.axios.get().then(response => response.data); } enableKey(id) { - return this.axios.put(`${id}/enable`) - .then(response => response.data); + return this.axios.put(`${id}/enable`).then(response => response.data); } disableKey(id) { - return this.axios.put(`${id}/disable`) - .then(response => response.data); + return this.axios.put(`${id}/disable`).then(response => response.data); } } diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index a044fc1ab42..245f1a7c558 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -21,9 +21,12 @@ export default class Diff { }); const tab = document.getElementById('diffs'); - if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile); + if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) + FilesCommentButton.init($diffFile); - const firstFile = $('.files').first().get(0); + const firstFile = $('.files') + .first() + .get(0); const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note'); $diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote)); @@ -73,9 +76,10 @@ export default class Diff { const view = file.data('view'); const params = { since, to, bottom, offset, unfold, view }; - axios.get(link, { params }) - .then(({ data }) => $target.parent().replaceWith(data)) - .catch(() => flash(__('An error occurred while loading diff'))); + axios + .get(link, { params }) + .then(({ data }) => $target.parent().replaceWith(data)) + .catch(() => flash(__('An error occurred while loading diff'))); } openAnchoredDiff(cb) { diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index edca45f22f9..a8d615dd8f0 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -41,6 +41,11 @@ export default { required: true, }, }, + data() { + return { + assignedDiscussions: false, + }; + }, computed: { ...mapState({ isLoading: state => state.diffs.isLoading, @@ -58,9 +63,9 @@ export default { plainDiffPath: state => state.diffs.plainDiffPath, emailPatchPath: state => state.diffs.emailPatchPath, }), - ...mapState('diffs', ['showTreeList']), + ...mapState('diffs', ['showTreeList', 'isLoading']), ...mapGetters('diffs', ['isParallelView']), - ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), + ...mapGetters(['isNotesFetched', 'getNoteableData']), targetBranch() { return { branchName: this.targetBranchName, @@ -147,11 +152,12 @@ export default { } }, setDiscussions() { - if (this.isNotesFetched) { + if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) { requestIdleCallback( - () => { - this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode); - }, + () => + this.assignDiscussionsToDiff().then(() => { + this.assignedDiscussions = true; + }), { timeout: 1000 }, ); } diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index f72c7a84e5c..958e57c5652 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -29,7 +29,7 @@ export default { }, computed: { ...mapState('diffs', ['currentDiffFileId']), - ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), + ...mapGetters(['isNotesFetched']), isCollapsed() { return this.file.collapsed || false; }, @@ -79,7 +79,7 @@ export default { .then(() => { requestIdleCallback( () => { - this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode); + this.assignDiscussionsToDiff(); }, { timeout: 1000 }, ); diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index cfe4273742f..34e836a570a 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -1,17 +1,30 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; +import { TooltipDirective as Tooltip } from '@gitlab-org/gitlab-ui'; +import { convertPermissionToBoolean } from '~/lib/utils/common_utils'; import Icon from '~/vue_shared/components/icon.vue'; import FileRow from '~/vue_shared/components/file_row.vue'; import FileRowStats from './file_row_stats.vue'; +const treeListStorageKey = 'mr_diff_tree_list'; + export default { + directives: { + Tooltip, + }, components: { Icon, FileRow, }, data() { + const treeListStored = localStorage.getItem(treeListStorageKey); + const renderTreeList = treeListStored !== null ? + convertPermissionToBoolean(treeListStored) : true; + return { search: '', + renderTreeList, + focusSearch: false, }; }, computed: { @@ -20,15 +33,35 @@ export default { filteredTreeList() { const search = this.search.toLowerCase().trim(); - if (search === '') return this.tree; + if (search === '') return this.renderTreeList ? this.tree : this.allBlobs; return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0); }, + rowDisplayTextKey() { + if (this.renderTreeList && this.search.trim() === '') { + return 'name'; + } + + return 'path'; + }, }, methods: { ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), clearSearch() { this.search = ''; + this.toggleFocusSearch(false); + }, + toggleRenderTreeList(toggle) { + this.renderTreeList = toggle; + localStorage.setItem(treeListStorageKey, this.renderTreeList); + }, + toggleFocusSearch(toggle) { + this.focusSearch = toggle; + }, + blurSearch() { + if (this.search.trim() === '') { + this.toggleFocusSearch(false); + } }, }, FileRowStats, @@ -37,28 +70,67 @@ export default { <template> <div class="tree-list-holder d-flex flex-column"> - <div class="append-bottom-8 position-relative tree-list-search"> - <icon - name="search" - class="position-absolute tree-list-icon" - /> - <input - v-model="search" - :placeholder="s__('MergeRequest|Filter files')" - type="search" - class="form-control" - /> - <button - v-show="search" - :aria-label="__('Clear search')" - type="button" - class="position-absolute tree-list-icon tree-list-clear-icon border-0 p-0" - @click="clearSearch" - > + <div class="append-bottom-8 position-relative tree-list-search d-flex"> + <div class="flex-fill d-flex"> <icon - name="close" + name="search" + class="position-absolute tree-list-icon" + /> + <input + v-model="search" + :placeholder="s__('MergeRequest|Filter files')" + type="search" + class="form-control" + @focus="toggleFocusSearch(true)" + @blur="blurSearch" /> - </button> + <button + v-show="search" + :aria-label="__('Clear search')" + type="button" + class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0" + @click="clearSearch" + > + <icon + name="close" + /> + </button> + </div> + <div + v-show="!focusSearch" + class="btn-group prepend-left-8 tree-list-view-toggle" + > + <button + v-tooltip.hover + :aria-label="__('List view')" + :title="__('List view')" + :class="{ + active: !renderTreeList + }" + class="btn btn-default pt-0 pb-0 d-flex align-items-center" + type="button" + @click="toggleRenderTreeList(false)" + > + <icon + name="hamburger" + /> + </button> + <button + v-tooltip.hover + :aria-label="__('Tree view')" + :title="__('Tree view')" + :class="{ + active: renderTreeList + }" + class="btn btn-default pt-0 pb-0 d-flex align-items-center" + type="button" + @click="toggleRenderTreeList(true)" + > + <icon + name="file-tree" + /> + </button> + </div> </div> <div class="tree-list-scroll" @@ -72,6 +144,8 @@ export default { :hide-extra-on-tree="true" :extra-component="$options.FileRowStats" :show-changed-icon="true" + :display-text-key="rowDisplayTextKey" + :should-truncate-start="true" @toggleTreeOpen="toggleTreeOpen" @clickFile="scrollToFile" /> diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 1e0b27b538d..ca8ae605cb4 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -5,7 +5,6 @@ import createFlash from '~/flash'; import { s__ } from '~/locale'; import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils'; import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility'; -import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils'; import { getDiffPositionByLineCode, getNoteFormData } from './utils'; import * as types from './mutation_types'; import { @@ -36,18 +35,17 @@ export const fetchDiffFiles = ({ state, commit }) => { // This is adding line discussions to the actual lines in the diff tree // once for parallel and once for inline mode -export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => { +export const assignDiscussionsToDiff = ( + { commit, state, rootState }, + discussions = rootState.notes.discussions, +) => { const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); - Object.values(allLineDiscussions).forEach(discussions => { - if (discussions.length > 0) { - const { fileHash } = discussions[0]; - commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { - fileHash, - discussions, - diffPositionByLineCode, - }); - } + discussions.filter(discussion => discussion.diff_discussion).forEach(discussion => { + commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { + discussion, + diffPositionByLineCode, + }); }); }; @@ -190,9 +188,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => { return dispatch('saveNote', postData, { root: true }) .then(result => dispatch('updateDiscussion', result.discussion, { root: true })) - .then(discussion => - dispatch('assignDiscussionsToDiff', reduceDiscussionsToLineCodes([discussion])), - ) + .then(discussion => dispatch('assignDiscussionsToDiff', [discussion])) .catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); }; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 0b4485ecdb5..5a8aebd2086 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -90,53 +90,67 @@ export default { })); }, - [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) { - const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash); - const firstDiscussion = discussions[0]; - const isDiffDiscussion = firstDiscussion.diff_discussion; - const hasLineCode = firstDiscussion.line_code; - const diffPosition = diffPositionByLineCode[firstDiscussion.line_code]; - - if ( - selectedFile && - isDiffDiscussion && - hasLineCode && - diffPosition && + [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) { + const { latestDiff } = state; + + const discussionLineCode = discussion.line_code; + const fileHash = discussion.diff_file.file_hash; + const lineCheck = ({ lineCode }) => + lineCode === discussionLineCode && isDiscussionApplicableToLine({ - discussion: firstDiscussion, - diffPosition, - latestDiff: state.latestDiff, - }) - ) { - const targetLine = selectedFile.parallelDiffLines.find( - line => - (line.left && line.left.lineCode === firstDiscussion.line_code) || - (line.right && line.right.lineCode === firstDiscussion.line_code), - ); - if (targetLine) { - if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) { - Object.assign(targetLine.left, { - discussions, - }); - } else { - Object.assign(targetLine.right, { - discussions, + discussion, + diffPosition: diffPositionByLineCode[lineCode], + latestDiff, + }); + + state.diffFiles = state.diffFiles.map(diffFile => { + if (diffFile.fileHash === fileHash) { + const file = { ...diffFile }; + + if (file.highlightedDiffLines) { + file.highlightedDiffLines = file.highlightedDiffLines.map(line => { + if (lineCheck(line)) { + return { + ...line, + discussions: line.discussions.concat(discussion), + }; + } + + return line; }); } - } - - if (selectedFile.highlightedDiffLines) { - const targetInlineLine = selectedFile.highlightedDiffLines.find( - line => line.lineCode === firstDiscussion.line_code, - ); - if (targetInlineLine) { - Object.assign(targetInlineLine, { - discussions, + if (file.parallelDiffLines) { + file.parallelDiffLines = file.parallelDiffLines.map(line => { + const left = line.left && lineCheck(line.left); + const right = line.right && lineCheck(line.right); + + if (left || right) { + return { + left: { + ...line.left, + discussions: left ? line.left.discussions.concat(discussion) : [], + }, + right: { + ...line.right, + discussions: right ? line.right.discussions.concat(discussion) : [], + }, + }; + } + + return line; }); } + + if (!file.parallelDiffLines || !file.highlightedDiffLines) { + file.discussions = file.discussions.concat(discussion); + } + + return file; } - } + + return diffFile; + }); }, [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index d2778bcdf1c..9987fbcb6a7 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -136,7 +136,7 @@ export default function dropzoneInput(form) { // removeAllFiles(true) stops uploading files (if any) // and remove them from dropzone files queue. - $cancelButton.on('click', (e) => { + $cancelButton.on('click', e => { e.preventDefault(); e.stopPropagation(); Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true); @@ -146,8 +146,10 @@ export default function dropzoneInput(form) { // clear dropzone files queue, change status of failed files to undefined, // and add that files to the dropzone files queue again. // addFile() adds file to dropzone files queue and upload it. - $retryLink.on('click', (e) => { - const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone')); + $retryLink.on('click', e => { + const dropzoneInstance = Dropzone.forElement( + e.target.closest('.js-main-target-form').querySelector('.div-dropzone'), + ); const failedFiles = dropzoneInstance.files; e.preventDefault(); @@ -156,7 +158,7 @@ export default function dropzoneInput(form) { // uploading of files that are being uploaded at the moment. dropzoneInstance.removeAllFiles(true); - failedFiles.map((failedFile) => { + failedFiles.map(failedFile => { const file = failedFile; if (file.status === Dropzone.ERROR) { @@ -168,7 +170,7 @@ export default function dropzoneInput(form) { }); }); // eslint-disable-next-line consistent-return - handlePaste = (event) => { + handlePaste = event => { const pasteEvent = event.originalEvent; if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { const image = isImage(pasteEvent); @@ -182,7 +184,7 @@ export default function dropzoneInput(form) { } }; - isImage = (data) => { + isImage = data => { let i = 0; while (i < data.clipboardData.items.length) { const item = data.clipboardData.items[i]; @@ -203,8 +205,12 @@ export default function dropzoneInput(form) { const caretStart = textarea.selectionStart; const caretEnd = textarea.selectionEnd; const textEnd = $(child).val().length; - const beforeSelection = $(child).val().substring(0, caretStart); - const afterSelection = $(child).val().substring(caretEnd, textEnd); + const beforeSelection = $(child) + .val() + .substring(0, caretStart); + const afterSelection = $(child) + .val() + .substring(caretEnd, textEnd); $(child).val(beforeSelection + formattedText + afterSelection); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.style.height = `${textarea.scrollHeight}px`; @@ -212,11 +218,11 @@ export default function dropzoneInput(form) { return formTextarea.trigger('input'); }; - addFileToForm = (path) => { + addFileToForm = path => { $(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`); }; - getFilename = (e) => { + getFilename = e => { let value; if (window.clipboardData && window.clipboardData.getData) { value = window.clipboardData.getData('Text'); @@ -231,7 +237,7 @@ export default function dropzoneInput(form) { const closeSpinner = () => $uploadingProgressContainer.addClass('hide'); - const showError = (message) => { + const showError = message => { $uploadingErrorContainer.removeClass('hide'); $uploadingErrorMessage.html(message); }; @@ -252,14 +258,15 @@ export default function dropzoneInput(form) { showSpinner(); closeAlertMessage(); - axios.post(uploadsPath, formData) + axios + .post(uploadsPath, formData) .then(({ data }) => { const md = data.link.markdown; insertToTextArea(filename, md); closeSpinner(); }) - .catch((e) => { + .catch(e => { showError(e.response.data.message); closeSpinner(); }); @@ -267,7 +274,8 @@ export default function dropzoneInput(form) { updateAttachingMessage = (files, messageContainer) => { let attachingMessage; - const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued').length; + const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued') + .length; // Dinamycally change uploading files text depending on files number in // dropzone files queue. @@ -282,7 +290,10 @@ export default function dropzoneInput(form) { form.find('.markdown-selector').click(function onMarkdownClick(e) { e.preventDefault(); - $(this).closest('.gfm-form').find('.div-dropzone').click(); + $(this) + .closest('.gfm-form') + .find('.div-dropzone') + .click(); formTextarea.focus(); }); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index c7b5a35cc14..dbfcf8cc921 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -3,8 +3,7 @@ import Pikaday from 'pikaday'; import dateFormat from 'dateformat'; import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; -import { timeFor } from './lib/utils/datetime_utility'; -import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; +import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; import boardsStore from './boards/stores/boards_store'; class DueDateSelect { diff --git a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js index e9defb62cf8..c5f9fcf6358 100644 --- a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js @@ -13,9 +13,11 @@ const rainbowCodePoint = 127752; // parseInt('1F308', 16) function isRainbowFlagEmoji(emojiUnicode) { const characters = Array.from(emojiUnicode); // Length 4 because flags are made of 2 characters which are surrogate pairs - return emojiUnicode.length === 4 && + return ( + emojiUnicode.length === 4 && characters[0].codePointAt(0) === baseFlagCodePoint && - characters[1].codePointAt(0) === rainbowCodePoint; + characters[1].codePointAt(0) === rainbowCodePoint + ); } // Chrome <57 renders keycaps oddly @@ -26,22 +28,28 @@ function isKeycapEmoji(emojiUnicode) { } // Check for a skin tone variation emoji which aren't always supported -const tone1 = 127995;// parseInt('1F3FB', 16) -const tone5 = 127999;// parseInt('1F3FF', 16) +const tone1 = 127995; // parseInt('1F3FB', 16) +const tone5 = 127999; // parseInt('1F3FF', 16) function isSkinToneComboEmoji(emojiUnicode) { - return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => { - const cp = char.codePointAt(0); - return cp >= tone1 && cp <= tone5; - }); + return ( + emojiUnicode.length > 2 && + Array.from(emojiUnicode).some(char => { + const cp = char.codePointAt(0); + return cp >= tone1 && cp <= tone5; + }) + ); } // macOS supports most skin tone emoji's but // doesn't support the skin tone versions of horse racing -const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) +const horseRacingCodePoint = 127943; // parseInt('1F3C7', 16) function isHorceRacingSkinToneComboEmoji(emojiUnicode) { const firstCharacter = Array.from(emojiUnicode)[0]; - return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint && - isSkinToneComboEmoji(emojiUnicode); + return ( + firstCharacter && + firstCharacter.codePointAt(0) === horseRacingCodePoint && + isSkinToneComboEmoji(emojiUnicode) + ); } // Check for `family_*`, `kiss_*`, `couple_*` @@ -52,7 +60,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16) function isPersonZwjEmoji(emojiUnicode) { let hasPersonEmoji = false; let hasZwj = false; - Array.from(emojiUnicode).forEach((character) => { + Array.from(emojiUnicode).forEach(character => { const cp = character.codePointAt(0); if (cp === zwj) { hasZwj = true; @@ -80,10 +88,7 @@ function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { // in `isEmojiUnicodeSupported` logic function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode); - return ( - (unicodeSupportMap.skinToneModifier && isSkinToneResult) || - !isSkinToneResult - ); + return (unicodeSupportMap.skinToneModifier && isSkinToneResult) || !isSkinToneResult; } // Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice @@ -91,8 +96,7 @@ function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) { const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode); return ( - (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || - !isHorseRacingSkinToneResult + (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || !isHorseRacingSkinToneResult ); } @@ -100,10 +104,7 @@ function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnico // in `isEmojiUnicodeSupported` logic function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode); - return ( - (unicodeSupportMap.personZwj && isPersonZwjResult) || - !isPersonZwjResult - ); + return (unicodeSupportMap.personZwj && isPersonZwjResult) || !isPersonZwjResult; } // Takes in a support map and determines whether @@ -111,16 +112,20 @@ function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { // // Combines all the edge case tests into a one-stop shop method function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) { - const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome && + const isOlderThanChrome57 = + unicodeSupportMap.meta && + unicodeSupportMap.meta.isChrome && unicodeSupportMap.meta.chromeVersion < 57; // For comments about each scenario, see the comments above each individual respective function - return unicodeSupportMap[unicodeVersion] && + return ( + unicodeSupportMap[unicodeVersion] && !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) && checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) && checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) && - checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode); + checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) + ); } export { diff --git a/app/assets/javascripts/experimental_flags.js b/app/assets/javascripts/experimental_flags.js index 1d60847147b..42b3fb8c6da 100644 --- a/app/assets/javascripts/experimental_flags.js +++ b/app/assets/javascripts/experimental_flags.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; export default () => { - $('.js-experiment-feature-toggle').on('change', (e) => { + $('.js-experiment-feature-toggle').on('change', e => { const el = e.target; Cookies.set(el.name, el.value, { diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 6a4874e1ab8..3233f5c4f71 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -25,13 +25,15 @@ export default { if (!this.userCanCreateNote) { // data-can-create-note is an empty string when true, otherwise undefined - this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('canCreateNote') === ''; + this.userCanCreateNote = + $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('canCreateNote') === ''; } this.isParallelView = Cookies.get('diff_view') === 'parallel'; if (this.userCanCreateNote) { - $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) + $diffFile + .on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e)); } }, @@ -64,9 +66,11 @@ export default { }, validateButtonParent(buttonParentElement) { - return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) && + return ( + !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) && !buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) && !buttonParentElement.classList.contains(NO_COMMENT_CLASS) && - !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS); + !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS) + ); }, }; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index b17ba3c21db..64b09c8b62c 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -65,12 +65,15 @@ export default class FilterableList { this.isBusy = true; - return axios.get(this.getFilterEndpoint(), { - params, - }).then((res) => { - this.onFilterSuccess(res, params); - this.onFilterComplete(); - }).catch(() => this.onFilterComplete()); + return axios + .get(this.getFilterEndpoint(), { + params, + }) + .then(res => { + this.onFilterSuccess(res, params); + this.onFilterComplete(); + }) + .catch(() => this.onFilterComplete()); } onFilterSuccess(response, queryData) { @@ -81,9 +84,13 @@ export default class FilterableList { // Change url so if user reload a page - search results are saved const currentPath = this.getPagePath(queryData); - return window.history.replaceState({ - page: currentPath, - }, document.title, currentPath); + return window.history.replaceState( + { + page: currentPath, + }, + document.title, + currentPath, + ); } onFilterComplete() { diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js index b70125c80ca..e22f542b7bf 100644 --- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -58,11 +58,16 @@ export const alternativeTokenKeys = [ export const conditions = [ { - url: 'assignee_id=0', + url: 'assignee_id=None', tokenKey: 'assignee', value: 'none', }, { + url: 'assignee_id=Any', + tokenKey: 'assignee', + value: 'any', + }, + { url: 'milestone_title=No+Milestone', tokenKey: 'milestone', value: 'none', diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index a29de9ae899..749c09f897c 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -8,14 +8,19 @@ const hideFlash = (flashEl, fadeTransition = true) => { }); } - flashEl.addEventListener('transitionend', () => { - flashEl.remove(); - window.dispatchEvent(new Event('resize')); - if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown'); - }, { - once: true, - passive: true, - }); + flashEl.addEventListener( + 'transitionend', + () => { + flashEl.remove(); + window.dispatchEvent(new Event('resize')); + if (document.body.classList.contains('flash-shown')) + document.body.classList.remove('flash-shown'); + }, + { + once: true, + passive: true, + }, + ); if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend')); }; @@ -30,12 +35,12 @@ const createAction = config => ` </a> `; -const createFlashEl = (message, type, isInContentWrapper = false) => ` +const createFlashEl = (message, type, isFixedLayout = false) => ` <div class="flash-${type}" > <div - class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}" + class="flash-text ${isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''}" > ${_.escape(message)} </div> @@ -69,12 +74,13 @@ const createFlash = function createFlash( addBodyClass = false, ) { const flashContainer = parent.querySelector('.flash-container'); + const navigation = parent.querySelector('.content'); if (!flashContainer) return null; - const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper'); + const isFixedLayout = navigation ? navigation.parentNode.classList.contains('container-limited') : true; - flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper); + flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout); const flashEl = flashContainer.querySelector(`.flash-${type}`); removeFlashClickListener(flashEl, fadeTransition); @@ -83,7 +89,9 @@ const createFlash = function createFlash( flashEl.innerHTML += createAction(actionConfig); if (actionConfig.clickHandler) { - flashEl.querySelector('.flash-action').addEventListener('click', e => actionConfig.clickHandler(e)); + flashEl + .querySelector('.flash-action') + .addEventListener('click', e => actionConfig.clickHandler(e)); } } @@ -94,11 +102,5 @@ const createFlash = function createFlash( return flashContainer; }; -export { - createFlash as default, - createFlashEl, - createAction, - hideFlash, - removeFlashClickListener, -}; +export { createFlash as default, createFlashEl, createAction, hideFlash, removeFlashClickListener }; window.Flash = createFlash; diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index f820f0dc3f0..3ac00c51df4 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -11,9 +11,13 @@ let sidebar; export const mousePos = []; -export const setSidebar = (el) => { sidebar = el; }; +export const setSidebar = el => { + sidebar = el; +}; export const getOpenMenu = () => currentOpenMenu; -export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; }; +export const setOpenMenu = (menu = null) => { + currentOpenMenu = menu; +}; export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); @@ -21,9 +25,10 @@ let headerHeight = 50; export const getHeaderHeight = () => headerHeight; -export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop'); +export const isSidebarCollapsed = () => + sidebar && sidebar.classList.contains('sidebar-collapsed-desktop'); -export const canShowActiveSubItems = (el) => { +export const canShowActiveSubItems = el => { if (el.classList.contains('active') && !isSidebarCollapsed()) { return false; } @@ -31,7 +36,10 @@ export const canShowActiveSubItems = (el) => { return true; }; -export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; +export const canShowSubItems = () => + bp.getBreakpointSize() === 'sm' || + bp.getBreakpointSize() === 'md' || + bp.getBreakpointSize() === 'lg'; export const getHideSubItemsInterval = () => { if (!currentOpenMenu || !mousePos.length) return 0; @@ -41,11 +49,12 @@ export const getHideSubItemsInterval = () => { const currentMousePosY = currentMousePos.y; const [menuTop, menuBottom] = menuCornerLocs; - if (currentMousePosY < menuTop.y || - currentMousePosY > menuBottom.y) return 0; + if (currentMousePosY < menuTop.y || currentMousePosY > menuBottom.y) return 0; - if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) && - slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) { + if ( + slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) && + slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop) + ) { return HIDE_INTERVAL_TIMEOUT; } @@ -56,11 +65,12 @@ export const calculateTop = (boundingRect, outerHeight) => { const windowHeight = window.innerHeight; const bottomOverflow = windowHeight - (boundingRect.top + outerHeight); - return bottomOverflow < 0 ? (boundingRect.top - outerHeight) + boundingRect.height : - boundingRect.top; + return bottomOverflow < 0 + ? boundingRect.top - outerHeight + boundingRect.height + : boundingRect.top; }; -export const hideMenu = (el) => { +export const hideMenu = el => { if (!el) return; const parentEl = el.parentNode; @@ -101,7 +111,7 @@ export const moveSubItemsToPosition = (el, subItems) => { } }; -export const showSubLevelItems = (el) => { +export const showSubLevelItems = el => { const subItems = el.querySelector('.sidebar-sub-level-items'); const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only'); @@ -128,16 +138,20 @@ export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => { }, timeout); }; -export const mouseLeaveTopItem = (el) => { +export const mouseLeaveTopItem = el => { const subItems = el.querySelector('.sidebar-sub-level-items'); - if (!canShowSubItems() || !canShowActiveSubItems(el) || - (subItems && subItems === currentOpenMenu)) return; + if ( + !canShowSubItems() || + !canShowActiveSubItems(el) || + (subItems && subItems === currentOpenMenu) + ) + return; el.classList.remove(IS_OVER_CLASS); }; -export const documentMouseMove = (e) => { +export const documentMouseMove = e => { mousePos.push({ x: e.clientX, y: e.clientY, @@ -146,7 +160,7 @@ export const documentMouseMove = (e) => { if (mousePos.length > 6) mousePos.shift(); }; -export const subItemsMouseLeave = (relatedTarget) => { +export const subItemsMouseLeave = relatedTarget => { clearTimeout(timeoutId); if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) { @@ -174,7 +188,7 @@ export default () => { headerHeight = document.querySelector('.nav-sidebar').offsetTop; - items.forEach((el) => { + items.forEach(el => { const subItems = el.querySelector('.sidebar-sub-level-items'); if (subItems) { diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index 87c6e37b9fb..a5b8c357e8a 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -116,7 +116,8 @@ export default class GlFieldError { this.form.focusOnFirstInvalid.apply(this.form); // For UX, wait til after first invalid submission to check each keyup - this.inputElement.off('keyup.fieldValidator') + this.inputElement + .off('keyup.fieldValidator') .on('keyup.fieldValidator', this.updateValidity.bind(this)); } diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index b9c51045b1d..3764e7ab422 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -16,9 +16,12 @@ export default class GlFieldErrors { initValidators() { // register selectors here as needed const validateSelectors = [':text', ':password', '[type=email]'] - .map(selector => `input${selector}`).join(','); + .map(selector => `input${selector}`) + .join(','); - this.state.inputs = this.form.find(validateSelectors).toArray() + this.state.inputs = this.form + .find(validateSelectors) + .toArray() .filter(input => !input.classList.contains(customValidationFlag)) .map(input => new GlFieldError({ input, formErrors: this })); @@ -42,7 +45,7 @@ export default class GlFieldErrors { /* Public method for triggering validity updates manually */ updateFormValidityState() { - this.state.inputs.forEach((field) => { + this.state.inputs.forEach(field => { if (field.state.submitted) { field.updateValidity(); } @@ -50,8 +53,9 @@ export default class GlFieldErrors { } focusOnFirstInvalid() { - const firstInvalid = this.state.inputs - .filter(input => !input.inputDomElement.validity.valid)[0]; + const firstInvalid = this.state.inputs.filter( + input => !input.inputDomElement.validity.valid, + )[0]; firstInvalid.inputElement.focus(); } } diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index e672284a2d0..f842d2d74db 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -39,7 +39,10 @@ export default class GLForm { this.form.find('.div-dropzone').remove(); this.form.addClass('gfm-form'); // remove notify commit author checkbox for non-commit notes - gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); + gl.utils.disableButtonIfEmptyField( + this.form.find('.js-note-text'), + this.form.find('.js-comment-button, .js-note-new-discussion'), + ); this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM); dropzoneInput(this.form); @@ -55,11 +58,9 @@ export default class GLForm { } setupAutosize() { - this.textarea.off('autosize:resized') - .on('autosize:resized', this.setHeightData.bind(this)); + this.textarea.off('autosize:resized').on('autosize:resized', this.setHeightData.bind(this)); - this.textarea.off('mouseup.autosize') - .on('mouseup.autosize', this.destroyAutosize.bind(this)); + this.textarea.off('mouseup.autosize').on('mouseup.autosize', this.destroyAutosize.bind(this)); setTimeout(() => { autosize(this.textarea); @@ -91,10 +92,14 @@ export default class GLForm { addEventListeners() { this.textarea.on('focus', function focusTextArea() { - $(this).closest('.md-area').addClass('is-focused'); + $(this) + .closest('.md-area') + .addClass('is-focused'); }); this.textarea.on('blur', function blurTextArea() { - $(this).closest('.md-area').removeClass('is-focused'); + $(this) + .closest('.md-area') + .removeClass('is-focused'); }); } } diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index beaac61e887..dcda625f587 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -7,8 +7,9 @@ export default function groupAvatar() { }); $('.js-group-avatar-input').on('change', function onChangeAvatarInput() { const form = $(this).closest('form'); - // eslint-disable-next-line no-useless-escape - const filename = $(this).val().replace(/^.*[\\\/]/, ''); + const filename = $(this) + .val() + .replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape return form.find('.js-avatar-filename').text(filename); }); } diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index d33e3a37580..9b74560f914 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -23,7 +23,8 @@ export default class GroupLabelSubscription { event.preventDefault(); const url = this.$unsubscribeButtons.attr('data-url'); - axios.post(url) + axios + .post(url) .then(() => { this.toggleSubscriptionButtons(); this.$unsubscribeButtons.removeAttr('data-url'); @@ -39,7 +40,8 @@ export default class GroupLabelSubscription { this.$unsubscribeButtons.attr('data-url', url); - axios.post(url) + axios + .post(url) .then(() => GroupLabelSubscription.setNewTooltip($btn)) .then(() => this.toggleSubscriptionButtons()) .catch(() => flash(__('There was an error when subscribing to this label.'))); @@ -58,6 +60,8 @@ export default class GroupLabelSubscription { const newTitle = tooltipTitles[type]; $('.js-unsubscribe-button', $button.closest('.label-actions-list')) - .tooltip('hide').attr('title', newTitle).tooltip('_fixTitle'); + .tooltip('hide') + .attr('title', newTitle) + .tooltip('_fixTitle'); } } diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 87ab5480c15..829924ba63c 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -1,44 +1,44 @@ <script> - import icon from '~/vue_shared/components/icon.vue'; - import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; - import { - ITEM_TYPE, - VISIBILITY_TYPE_ICON, - GROUP_VISIBILITY_TYPE, - PROJECT_VISIBILITY_TYPE, - } from '../constants'; - import itemStatsValue from './item_stats_value.vue'; +import icon from '~/vue_shared/components/icon.vue'; +import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { + ITEM_TYPE, + VISIBILITY_TYPE_ICON, + GROUP_VISIBILITY_TYPE, + PROJECT_VISIBILITY_TYPE, +} from '../constants'; +import itemStatsValue from './item_stats_value.vue'; - export default { - components: { - icon, - timeAgoTooltip, - itemStatsValue, +export default { + components: { + icon, + timeAgoTooltip, + itemStatsValue, + }, + props: { + item: { + type: Object, + required: true, }, - props: { - item: { - type: Object, - required: true, - }, + }, + computed: { + visibilityIcon() { + return VISIBILITY_TYPE_ICON[this.item.visibility]; }, - computed: { - visibilityIcon() { - return VISIBILITY_TYPE_ICON[this.item.visibility]; - }, - visibilityTooltip() { - if (this.item.type === ITEM_TYPE.GROUP) { - return GROUP_VISIBILITY_TYPE[this.item.visibility]; - } - return PROJECT_VISIBILITY_TYPE[this.item.visibility]; - }, - isProject() { - return this.item.type === ITEM_TYPE.PROJECT; - }, - isGroup() { - return this.item.type === ITEM_TYPE.GROUP; - }, + visibilityTooltip() { + if (this.item.type === ITEM_TYPE.GROUP) { + return GROUP_VISIBILITY_TYPE[this.item.visibility]; + } + return PROJECT_VISIBILITY_TYPE[this.item.visibility]; }, - }; + isProject() { + return this.item.type === ITEM_TYPE.PROJECT; + }, + isGroup() { + return this.item.type === ITEM_TYPE.GROUP; + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue index ef9f2bca76c..c542ca946d3 100644 --- a/app/assets/javascripts/groups/components/item_stats_value.vue +++ b/app/assets/javascripts/groups/components/item_stats_value.vue @@ -1,52 +1,52 @@ <script> - import tooltip from '~/vue_shared/directives/tooltip'; - import icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import icon from '~/vue_shared/components/icon.vue'; - export default { - components: { - icon, +export default { + components: { + icon, + }, + directives: { + tooltip, + }, + props: { + title: { + type: String, + required: false, + default: '', }, - directives: { - tooltip, + cssClass: { + type: String, + required: false, + default: '', }, - props: { - title: { - type: String, - required: false, - default: '', - }, - cssClass: { - type: String, - required: false, - default: '', - }, - iconName: { - type: String, - required: true, - }, - tooltipPlacement: { - type: String, - required: false, - default: 'bottom', - }, - /** - * value could either be number or string - * as `memberCount` is always passed as string - * while `subgroupCount` & `projectCount` - * are always number - */ - value: { - type: [Number, String], - required: false, - default: '', - }, + iconName: { + type: String, + required: true, }, - computed: { - isValuePresent() { - return this.value !== ''; - }, + tooltipPlacement: { + type: String, + required: false, + default: 'bottom', }, - }; + /** + * value could either be number or string + * as `memberCount` is always passed as string + * while `subgroupCount` & `projectCount` + * are always number + */ + value: { + type: [Number, String], + required: false, + default: '', + }, + }, + computed: { + isValuePresent() { + return this.value !== ''; + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js index a120d501e35..012177479c6 100644 --- a/app/assets/javascripts/groups/new_group_child.js +++ b/app/assets/javascripts/groups/new_group_child.js @@ -37,20 +37,22 @@ export default class NewGroupChild { getDroplabConfig() { return { - InputSetter: [{ - input: this.newGroupChildButton, - valueAttribute: 'data-value', - inputAttribute: 'data-action', - }, { - input: this.newGroupChildButton, - valueAttribute: 'data-text', - }], + InputSetter: [ + { + input: this.newGroupChildButton, + valueAttribute: 'data-value', + inputAttribute: 'data-action', + }, + { + input: this.newGroupChildButton, + valueAttribute: 'data-text', + }, + ], }; } bindEvents() { - this.newGroupChildButton - .addEventListener('click', this.onClickNewGroupChildButton.bind(this)); + this.newGroupChildButton.addEventListener('click', this.onClickNewGroupChildButton.bind(this)); } onClickNewGroupChildButton(e) { diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js index 4a7569078a1..16f95d5a0cc 100644 --- a/app/assets/javascripts/groups/store/groups_store.js +++ b/app/assets/javascripts/groups/store/groups_store.js @@ -17,13 +17,14 @@ export default class GroupsStore { } setSearchedGroups(rawGroups) { - const formatGroups = groups => groups.map((group) => { - const formattedGroup = this.formatGroupItem(group); - if (formattedGroup.children && formattedGroup.children.length) { - formattedGroup.children = formatGroups(formattedGroup.children); - } - return formattedGroup; - }); + const formatGroups = groups => + groups.map(group => { + const formattedGroup = this.formatGroupItem(group); + if (formattedGroup.children && formattedGroup.children.length) { + formattedGroup.children = formatGroups(formattedGroup.children); + } + return formattedGroup; + }); if (rawGroups && rawGroups.length) { this.state.groups = formatGroups(rawGroups); @@ -62,10 +63,10 @@ export default class GroupsStore { formatGroupItem(rawGroupItem) { const groupChildren = rawGroupItem.children || []; - const groupIsOpen = (groupChildren.length > 0) || false; - const childrenCount = this.hideProjects ? - rawGroupItem.subgroup_count : - rawGroupItem.children_count; + const groupIsOpen = groupChildren.length > 0 || false; + const childrenCount = this.hideProjects + ? rawGroupItem.subgroup_count + : rawGroupItem.children_count; return { id: rawGroupItem.id, diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js index e0eb118ddf7..26510fcdb2a 100644 --- a/app/assets/javascripts/groups/transfer_dropdown.js +++ b/app/assets/javascripts/groups/transfer_dropdown.js @@ -22,7 +22,7 @@ export default class TransferDropdown { search: { fields: ['text'] }, data: extraOptions.concat(this.data), text: item => item.text, - clicked: (options) => { + clicked: options => { const { e } = options; e.preventDefault(); this.assignSelected(options.selectedObj); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index e37fc5c4be6..b4a3037c1b7 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -23,7 +23,7 @@ export default function groupsSelect() { axios[params.type.toLowerCase()](params.url, { params: params.data, }) - .then((res) => { + .then(res => { const results = res.data || []; const headers = normalizeHeaders(res.headers); const currentPage = parseInt(headers['X-PAGE'], 10) || 0; @@ -36,7 +36,8 @@ export default function groupsSelect() { more, }, }); - }).catch(params.error); + }) + .catch(params.error); }, data(search, page) { return { @@ -68,7 +69,9 @@ export default function groupsSelect() { } }, formatResult(object) { - return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`; + return `<div class='group-result'> <div class='group-name'>${ + object.full_name + }</div> <div class='group-path'>${object.full_path}</div> </div>`; }, formatSelection(object) { return object.full_name; diff --git a/app/assets/javascripts/helpers/avatar_helper.js b/app/assets/javascripts/helpers/avatar_helper.js index d3b1d0f11fd..35ac7b2629c 100644 --- a/app/assets/javascripts/helpers/avatar_helper.js +++ b/app/assets/javascripts/helpers/avatar_helper.js @@ -19,7 +19,9 @@ export function renderIdenticon(entity, options = {}) { const bgClass = getIdenticonBackgroundClass(entity.id); const title = getIdenticonTitle(entity.name); - return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape(title)}</div>`; + return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape( + title, + )}</div>`; } export function renderAvatar(entity, options = {}) { diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js index fab0255c378..3587f073a00 100644 --- a/app/assets/javascripts/image_diff/image_diff.js +++ b/app/assets/javascripts/image_diff/image_diff.js @@ -60,8 +60,10 @@ export default class ImageDiff { } renderBadge(discussionEl, index) { - const imageBadge = imageDiffHelper - .generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl); + const imageBadge = imageDiffHelper.generateBadgeFromDiscussionDOM( + this.imageFrameEl, + discussionEl, + ); this.imageBadges.push(imageBadge); diff --git a/app/assets/javascripts/image_diff/init_discussion_tab.js b/app/assets/javascripts/image_diff/init_discussion_tab.js index 2f16c6ef115..dbe4c06a4e9 100644 --- a/app/assets/javascripts/image_diff/init_discussion_tab.js +++ b/app/assets/javascripts/image_diff/init_discussion_tab.js @@ -8,5 +8,6 @@ export default () => { const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file'); [...diffFileEls].forEach(diffFileEl => - imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge)); + imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge), + ); }; diff --git a/app/assets/javascripts/image_diff/replaced_image_diff.js b/app/assets/javascripts/image_diff/replaced_image_diff.js index 4abd13fb472..8d9e65155d8 100644 --- a/app/assets/javascripts/image_diff/replaced_image_diff.js +++ b/app/assets/javascripts/image_diff/replaced_image_diff.js @@ -26,7 +26,7 @@ export default class ReplacedImageDiff extends ImageDiff { this.imageEls = {}; const viewTypeNames = Object.getOwnPropertyNames(viewTypes); - viewTypeNames.forEach((viewType) => { + viewTypeNames.forEach(viewType => { this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img'); }); } @@ -79,13 +79,12 @@ export default class ReplacedImageDiff extends ImageDiff { // Re-render indicator in new view if (indicator.removed) { - const normalizedIndicator = imageDiffHelper - .resizeCoordinatesToImageElement(this.imageEl, { - x: indicator.x, - y: indicator.y, - width: indicator.image.width, - height: indicator.image.height, - }); + const normalizedIndicator = imageDiffHelper.resizeCoordinatesToImageElement(this.imageEl, { + x: indicator.x, + y: indicator.y, + width: indicator.image.width, + height: indicator.image.height, + }); imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator); } } diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index eda8cdad908..f1beb1a8ea5 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -60,66 +60,71 @@ class ImporterStatus { attributes = Object.assign(repoData, attributes); } - return axios.post(this.importUrl, attributes) - .then(({ data }) => { - const job = $(`tr#repo_${id}`); - job.attr('id', `project_${data.id}`); - - job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`); - $('table.import-jobs tbody').prepend(job); - - job.addClass('table-active'); - const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing'); - job.find('.import-actions').html(sprintf( - _.escape(__('%{loadingIcon} Started')), { - loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(connectingVerb)}"></i>`, - }, - false, - )); - }) - .catch((error) => { - let details = error; - - const $statusField = $(`#repo_${this.id} .job-status`); - $statusField.text(__('Failed')); - - if (error.response && error.response.data && error.response.data.errors) { - details = error.response.data.errors; - } - - flash(sprintf(__('An error occurred while importing project: %{details}'), { details })); - }); + return axios + .post(this.importUrl, attributes) + .then(({ data }) => { + const job = $(`tr#repo_${id}`); + job.attr('id', `project_${data.id}`); + + job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`); + $('table.import-jobs tbody').prepend(job); + + job.addClass('table-active'); + const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing'); + job.find('.import-actions').html( + sprintf( + _.escape(__('%{loadingIcon} Started')), + { + loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape( + connectingVerb, + )}"></i>`, + }, + false, + ), + ); + }) + .catch(error => { + let details = error; + + const $statusField = $(`#repo_${this.id} .job-status`); + $statusField.text(__('Failed')); + + if (error.response && error.response.data && error.response.data.errors) { + details = error.response.data.errors; + } + + flash(sprintf(__('An error occurred while importing project: %{details}'), { details })); + }); } autoUpdate() { - return axios.get(this.jobsUrl) - .then(({ data = [] }) => { - data.forEach((job) => { - const jobItem = $(`#project_${job.id}`); - const statusField = jobItem.find('.job-status'); - - const spinner = '<i class="fa fa-spinner fa-spin"></i>'; - - switch (job.import_status) { - case 'finished': - jobItem.removeClass('table-active').addClass('table-success'); - statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`); - break; - case 'scheduled': - statusField.html(`${spinner} ${__('Scheduled')}`); - break; - case 'started': - statusField.html(`${spinner} ${__('Started')}`); - break; - case 'failed': - statusField.html(__('Failed')); - break; - default: - statusField.html(job.import_status); - break; - } - }); + return axios.get(this.jobsUrl).then(({ data = [] }) => { + data.forEach(job => { + const jobItem = $(`#project_${job.id}`); + const statusField = jobItem.find('.job-status'); + + const spinner = '<i class="fa fa-spinner fa-spin"></i>'; + + switch (job.import_status) { + case 'finished': + jobItem.removeClass('table-active').addClass('table-success'); + statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`); + break; + case 'scheduled': + statusField.html(`${spinner} ${__('Scheduled')}`); + break; + case 'started': + statusField.html(`${spinner} ${__('Started')}`); + break; + case 'failed': + statusField.html(__('Failed')); + break; + default: + statusField.html(job.import_status); + break; + } }); + }); } setAutoUpdate() { @@ -141,7 +146,4 @@ function initImporterStatus() { } } -export { - initImporterStatus as default, - ImporterStatus, -}; +export { initImporterStatus as default, ImporterStatus }; diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index 5c5a6e01848..e708e5d0978 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import { stickyMonitor } from './lib/utils/sticky'; -export default (stickyTop) => { +export default stickyTop => { stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); $('.js-diff-stats-dropdown').glDropdown({ diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js index 3c71258e53b..a77828e8cf2 100644 --- a/app/assets/javascripts/init_notes.js +++ b/app/assets/javascripts/init_notes.js @@ -2,13 +2,7 @@ import Notes from './notes'; export default () => { const dataEl = document.querySelector('.js-notes-data'); - const { - notesUrl, - notesIds, - now, - diffView, - enableGFM, - } = JSON.parse(dataEl.innerHTML); + const { notesUrl, notesIds, now, diffView, enableGFM } = JSON.parse(dataEl.innerHTML); // Create a singleton so that we don't need to assign // into the window object, we can just access the current isntance with Notes.instance diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index bd90d0eaa32..08b858305ab 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -97,7 +97,8 @@ export default class IntegrationSettingsForm { testSettings(formData) { this.toggleSubmitBtnState(true); - return axios.put(this.testEndPoint, formData) + return axios + .put(this.testEndPoint, formData) .then(({ data }) => { if (data.error) { let flashActions; @@ -105,7 +106,7 @@ export default class IntegrationSettingsForm { if (data.test_failed) { flashActions = { title: 'Save anyway', - clickHandler: (e) => { + clickHandler: e => { e.preventDefault(); this.$form.submit(); }, diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js index 07cf1eff279..612c524ca1c 100644 --- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js +++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js @@ -27,7 +27,10 @@ class AutoWidthDropdownSelect { // We have to look at the parent because // `offsetParent` on a `display: none;` is `null` - const offsetParentWidth = $(this).parent().offsetParent().width(); + const offsetParentWidth = $(this) + .parent() + .offsetParent() + .width(); // Reset any width to let it naturally flow $dropdown.css('width', 'auto'); if ($dropdown.outerWidth(false) > offsetParentWidth) { diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index 9848bcc2e64..b844e4c5e5b 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -32,7 +32,7 @@ export default { onFormSubmitFailure() { this.form.find('[type="submit"]').enable(); - return new Flash("Issue update failed"); + return new Flash('Issue update failed'); }, getSelectedIssues() { @@ -63,7 +63,7 @@ export default { const result = []; const labelsToKeep = this.$labelDropdown.data('indeterminate'); - this.getLabelsFromSelection().forEach((id) => { + this.getLabelsFromSelection().forEach(id => { if (labelsToKeep.indexOf(id) === -1) { result.push(id); } @@ -89,8 +89,8 @@ export default { issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), add_label_ids: [], - remove_label_ids: [] - } + remove_label_ids: [], + }, }; if (this.willUpdateLabels) { formData.update.add_label_ids = this.$labelDropdown.data('marked'); @@ -134,7 +134,7 @@ export default { // Collect unique label IDs for all checked issues this.getElement('.selected-issuable:checked').each((i, el) => { issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); - issuableLabels.forEach((labelId) => { + issuableLabels.forEach(labelId => { // Store unique IDs if (uniqueIds.indexOf(labelId) === -1) { uniqueIds.push(labelId); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 0140960b367..c81a2230310 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,6 +1,3 @@ -/* eslint-disable no-new, no-unused-vars, consistent-return, no-else-return */ -/* global GitLab */ - import $ from 'jquery'; import Pikaday from 'pikaday'; import Autosave from './autosave'; @@ -8,7 +5,7 @@ import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; import ZenMode from './zen_mode'; import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; -import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; +import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; export default class IssuableForm { constructor(form) { @@ -19,9 +16,11 @@ export default class IssuableForm { this.handleSubmit = this.handleSubmit.bind(this); this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; - new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); - new UsersSelect(); - new ZenMode(); + this.gfmAutoComplete = new GfmAutoComplete( + gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources, + ).setup(); + this.usersSelect = new UsersSelect(); + this.zenMode = new ZenMode(); this.titleField = this.form.find('input[name*="[title]"]'); this.descriptionField = this.form.find('textarea[name*="[description]"]'); @@ -57,8 +56,16 @@ export default class IssuableForm { } initAutosave() { - new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']); - return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']); + this.autosave = new Autosave(this.titleField, [ + document.location.pathname, + document.location.search, + 'title', + ]); + return new Autosave(this.descriptionField, [ + document.location.pathname, + document.location.search, + 'description', + ]); } handleSubmit() { @@ -74,7 +81,7 @@ export default class IssuableForm { this.$wipExplanation = this.form.find('.js-wip-explanation'); this.$noWipExplanation = this.form.find('.js-no-wip-explanation'); if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) { - return; + return undefined; } this.form.on('click', '.js-toggle-wip', this.toggleWip); this.titleField.on('keyup blur', this.renderWipExplanation); @@ -89,10 +96,9 @@ export default class IssuableForm { if (this.workInProgress()) { this.$wipExplanation.show(); return this.$noWipExplanation.hide(); - } else { - this.$wipExplanation.hide(); - return this.$noWipExplanation.show(); } + this.$wipExplanation.hide(); + return this.$noWipExplanation.show(); } toggleWip(event) { @@ -110,7 +116,7 @@ export default class IssuableForm { } addWip() { - this.titleField.val(`WIP: ${(this.titleField.val())}`); + this.titleField.val(`WIP: ${this.titleField.val()}`); } initTargetBranchDropdown() { diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index ba14aaeed2c..ac19034f69d 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -77,11 +77,11 @@ 'shouldRenderCalloutMessage', 'shouldRenderTriggeredLabel', 'hasEnvironment', - 'isJobStuck', 'hasTrace', 'emptyStateIllustration', 'isScrollingDown', 'emptyStateAction', + 'hasRunnersForProject', ]), shouldRenderContent() { @@ -195,9 +195,9 @@ <!-- Body Section --> <stuck-block - v-if="isJobStuck" + v-if="job.stuck" class="js-job-stuck" - :has-no-runners-for-project="job.runners.available" + :has-no-runners-for-project="hasRunnersForProject" :tags="job.tags" :runners-path="runnerSettingsUrl" /> diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue index 81cc0823792..6486b25c8a7 100644 --- a/app/assets/javascripts/jobs/components/job_container_item.vue +++ b/app/assets/javascripts/jobs/components/job_container_item.vue @@ -1,5 +1,4 @@ <script> -import _ from 'underscore'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; @@ -9,11 +8,9 @@ export default { CiIcon, Icon, }, - directives: { tooltip, }, - props: { job: { type: Object, @@ -24,10 +21,9 @@ export default { required: true, }, }, - computed: { tooltipText() { - return `${_.escape(this.job.name)} - ${this.job.status.tooltip}`; + return `${this.job.name} - ${this.job.status.tooltip}`; }, }, }; @@ -36,7 +32,10 @@ export default { <template> <div class="build-job" - :class="{ retried: job.retried, active: isActive }" + :class="{ + retried: job.retried, + active: isActive + }" > <a v-tooltip diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 906769ee6a2..28a02230d89 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -31,7 +31,7 @@ export default { }, }, computed: { - ...mapState(['job', 'stages', 'jobs', 'selectedStage']), + ...mapState(['job', 'stages', 'jobs', 'selectedStage', 'isLoadingStages']), coverage() { return `${this.job.coverage}%`; }, @@ -59,10 +59,10 @@ export default { return ''; } - let t = this.job.metadata.timeout_human_readable; - if (this.job.metadata.timeout_source !== '') { - t += ` (from ${this.job.metadata.timeout_source})`; - } + let t = this.job.metadata.timeout_human_readable; + if (this.job.metadata.timeout_source !== '') { + t += ` (from ${this.job.metadata.timeout_source})`; + } return t; }, @@ -270,6 +270,7 @@ export default { /> <stages-dropdown + v-if="!isLoadingStages" :stages="stages" :pipeline="job.pipeline" :selected-stage="selectedStage" diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index e5e1d56e287..dc26b246d71 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -22,7 +22,6 @@ export default { required: true, }, }, - computed: { hasRef() { return !_.isEmpty(this.pipeline.ref); diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue index a60643b2c65..1d5789b175a 100644 --- a/app/assets/javascripts/jobs/components/stuck_block.vue +++ b/app/assets/javascripts/jobs/components/stuck_block.vue @@ -23,14 +23,7 @@ export default { <template> <div class="bs-callout bs-callout-warning"> <p - v-if="hasNoRunnersForProject" - class="js-stuck-no-runners append-bottom-0" - > - {{ s__(`Job|This job is stuck, because the project - doesn't have any runners online assigned to it.`) }} - </p> - <p - v-else-if="tags.length" + v-if="tags.length" class="js-stuck-with-tags append-bottom-0" > {{ s__(`This job is stuck, because you don't have @@ -44,6 +37,13 @@ export default { </span> </p> <p + v-else-if="hasNoRunnersForProject" + class="js-stuck-no-runners append-bottom-0" + > + {{ s__(`Job|This job is stuck, because the project + doesn't have any runners online assigned to it.`) }} + </p> + <p v-else class="js-stuck-no-active-runner append-bottom-0" > diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 4ce395a9106..4de01f8e532 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -41,17 +41,10 @@ export const emptyStateIllustration = state => (state.job && state.job.status && state.job.status.illustration) || {}; export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {}; -/** - * When the job is pending and there are no available runners - * we need to render the stuck block; - * - * @returns {Boolean} - */ -export const isJobStuck = state => - (!_.isEmpty(state.job.status) && state.job.status.group === 'pending') && - (!_.isEmpty(state.job.runners) && state.job.runners.available === false); export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; +export const hasRunnersForProject = state => state.job.runners.available && !state.job.runners.online; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index 4195d787f12..cd440d21c1f 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -71,7 +71,7 @@ export default { * after the first request, * and we do not want to hijack that */ - if (state.selectedStage === 'More' && job.stage) { + if (state.selectedStage === '' && job.stage) { state.selectedStage = job.stage; } }, diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index 0eb269ca38f..04825187c99 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -1,5 +1,3 @@ -import { __ } from '~/locale'; - export default () => ({ jobEndpoint: null, traceEndpoint: null, @@ -29,7 +27,7 @@ export default () => ({ // sidebar dropdown & list of jobs isLoadingStages: false, isLoadingJobs: false, - selectedStage: __('More'), + selectedStage: '', stages: [], jobs: [], }); diff --git a/app/assets/javascripts/lib/utils/datefix.js b/app/assets/javascripts/lib/utils/datefix.js deleted file mode 100644 index 19e4085dbbb..00000000000 --- a/app/assets/javascripts/lib/utils/datefix.js +++ /dev/null @@ -1,28 +0,0 @@ -export const pad = (val, len = 2) => `0${val}`.slice(-len); - -/** - * Formats dates in Pickaday - * @param {String} dateString Date in yyyy-mm-dd format - * @return {Date} UTC format - */ -export const parsePikadayDate = dateString => { - const parts = dateString.split('-'); - const year = parseInt(parts[0], 10); - const month = parseInt(parts[1] - 1, 10); - const day = parseInt(parts[2], 10); - - return new Date(year, month, day); -}; - -/** - * Used `onSelect` method in pickaday - * @param {Date} date UTC format - * @return {String} Date formated in yyyy-mm-dd - */ -export const pikadayToString = date => { - const day = pad(date.getDate()); - const month = pad(date.getMonth() + 1); - const year = date.getFullYear(); - - return `${year}-${month}-${day}`; -}; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 833dbefd3dc..46740308f17 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import _ from 'underscore'; import timeago from 'timeago.js'; import dateFormat from 'dateformat'; import { pluralize } from './text_utility'; @@ -46,6 +47,8 @@ const getMonthNames = abbreviated => { ]; }; +export const pad = (val, len = 2) => `0${val}`.slice(-len); + /** * Given a date object returns the day of the week in English * @param {date} date @@ -74,10 +77,10 @@ let timeagoInstance; /** * Sets a timeago Instance */ -export function getTimeago() { +export const getTimeago = () => { if (!timeagoInstance) { - const localeRemaining = function getLocaleRemaining(number, index) { - return [ + const localeRemaining = (number, index) => + [ [s__('Timeago|just now'), s__('Timeago|right now')], [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')], [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], @@ -93,9 +96,9 @@ export function getTimeago() { [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], ][index]; - }; - const locale = function getLocale(number, index) { - return [ + + const locale = (number, index) => + [ [s__('Timeago|just now'), s__('Timeago|right now')], [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')], [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], @@ -111,7 +114,6 @@ export function getTimeago() { [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], ][index]; - }; timeago.register(timeagoLanguageCode, locale); timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining); @@ -119,7 +121,7 @@ export function getTimeago() { } return timeagoInstance; -} +}; /** * For the given element, renders a timeago instance. @@ -184,7 +186,7 @@ export const getDayDifference = (a, b) => { * @param {Number} seconds * @return {String} */ -export function timeIntervalInWords(intervalInSeconds) { +export const timeIntervalInWords = intervalInSeconds => { const secondsInteger = parseInt(intervalInSeconds, 10); const minutes = Math.floor(secondsInteger / 60); const seconds = secondsInteger - minutes * 60; @@ -196,9 +198,9 @@ export function timeIntervalInWords(intervalInSeconds) { text = `${seconds} ${pluralize('second', seconds)}`; } return text; -} +}; -export function dateInWords(date, abbreviated = false, hideYear = false) { +export const dateInWords = (date, abbreviated = false, hideYear = false) => { if (!date) return date; const month = date.getMonth(); @@ -240,7 +242,7 @@ export function dateInWords(date, abbreviated = false, hideYear = false) { } return `${monthName} ${date.getDate()}, ${year}`; -} +}; /** * Returns month name based on provided date. @@ -391,3 +393,95 @@ export const formatTime = milliseconds => { formattedTime += remainingSeconds; return formattedTime; }; + +/** + * Formats dates in Pickaday + * @param {String} dateString Date in yyyy-mm-dd format + * @return {Date} UTC format + */ +export const parsePikadayDate = dateString => { + const parts = dateString.split('-'); + const year = parseInt(parts[0], 10); + const month = parseInt(parts[1] - 1, 10); + const day = parseInt(parts[2], 10); + + return new Date(year, month, day); +}; + +/** + * Used `onSelect` method in pickaday + * @param {Date} date UTC format + * @return {String} Date formated in yyyy-mm-dd + */ +export const pikadayToString = date => { + const day = pad(date.getDate()); + const month = pad(date.getMonth() + 1); + const year = date.getFullYear(); + + return `${year}-${month}-${day}`; +}; + +/** + * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } + * Seconds can be negative or positive, zero or non-zero. Can be configured for any day + * or week length. + */ +export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) => { + const DAYS_PER_WEEK = daysPerWeek; + const HOURS_PER_DAY = hoursPerDay; + const MINUTES_PER_HOUR = 60; + const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; + const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; + + const timePeriodConstraints = { + weeks: MINUTES_PER_WEEK, + days: MINUTES_PER_DAY, + hours: MINUTES_PER_HOUR, + minutes: 1, + }; + + let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); + + return _.mapObject(timePeriodConstraints, minutesPerPeriod => { + const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); + + unorderedMinutes -= periodCount * minutesPerPeriod; + + return periodCount; + }); +}; + +/** + * Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it + * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. + */ +export const stringifyTime = timeObject => { + const reducedTime = _.reduce( + timeObject, + (memo, unitValue, unitName) => { + const isNonZero = !!unitValue; + return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; + }, + '', + ).trim(); + return reducedTime.length ? reducedTime : '0m'; +}; + +/** + * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns + * the first non-zero unit/value pair. + */ +export const abbreviateTime = timeStr => + timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0]; + +/** + * Calculates the milliseconds between now and a given date string. + * The result cannot become negative. + * + * @param endDate date string that the time difference is calculated for + * @return {number} number of milliseconds remaining until the given date + */ +export const calculateRemainingMilliseconds = endDate => { + const remainingMilliseconds = new Date(endDate).getTime() - Date.now(); + return Math.max(remainingMilliseconds, 0); +}; diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js deleted file mode 100644 index d92b8a7179f..00000000000 --- a/app/assets/javascripts/lib/utils/pretty_time.js +++ /dev/null @@ -1,63 +0,0 @@ -import _ from 'underscore'; - -/* - * TODO: Make these methods more configurable (e.g. stringifyTime condensed or - * non-condensed, abbreviateTimelengths) - * */ - -/* - * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } - * Seconds can be negative or positive, zero or non-zero. Can be configured for any day - * or week length. -*/ - -export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) { - const DAYS_PER_WEEK = daysPerWeek; - const HOURS_PER_DAY = hoursPerDay; - const MINUTES_PER_HOUR = 60; - const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; - const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; - - const timePeriodConstraints = { - weeks: MINUTES_PER_WEEK, - days: MINUTES_PER_DAY, - hours: MINUTES_PER_HOUR, - minutes: 1, - }; - - let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); - - return _.mapObject(timePeriodConstraints, minutesPerPeriod => { - const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); - - unorderedMinutes -= periodCount * minutesPerPeriod; - - return periodCount; - }); -} - -/* -* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it -* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. -*/ - -export function stringifyTime(timeObject) { - const reducedTime = _.reduce( - timeObject, - (memo, unitValue, unitName) => { - const isNonZero = !!unitValue; - return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; - }, - '', - ).trim(); - return reducedTime.length ? reducedTime : '0m'; -} - -/* -* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns -* the first non-zero unit/value pair. -*/ - -export function abbreviateTime(timeStr) { - return timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0]; -} diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index e26a6b986be..c52cfb806a2 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -2,6 +2,8 @@ import $ from 'jquery'; import { insertText } from '~/lib/utils/common_utils'; +const LINK_TAG_PATTERN = '[{text}](url)'; + function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); } @@ -76,6 +78,21 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr removedFirstNewLine = false; currentLineEmpty = false; + // check for link pattern and selected text is an URL + // if so fill in the url part instead of the text part of the pattern. + if (tag === LINK_TAG_PATTERN) { + if (URL) { + try { + const ignoredUrl = new URL(selected); + // valid url + tag = '[text]({text})'; + select = 'text'; + } catch (e) { + // ignore - no valid url + } + } + } + // Remove the first newline if (selected.indexOf('\n') === 0) { removedFirstNewLine = true; diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index df5cd1b8c51..0beedcacf33 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import Pikaday from 'pikaday'; -import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; +import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; // Add datepickers to all `js-access-expiration-date` elements. If those elements are // children of an element with the `clearable-input` class, and have a sibling diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 67338aa96c3..98182d92c2f 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -149,7 +149,7 @@ export default { .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))), this.service .getEnvironmentsData() - .then((data) => this.store.storeEnvironmentsData(data)) + .then(data => this.store.storeEnvironmentsData(data)) .catch(() => Flash(s__('Metrics|There was an error getting environments information.'))), ]) .then(() => { @@ -157,6 +157,7 @@ export default { this.state = 'noData'; return; } + this.showEmptyState = false; }) .then(this.resize) @@ -195,7 +196,10 @@ export default { name="chevron-down" /> </button> - <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> + <div + v-if="store.environmentsData.length > 0" + class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up" + > <ul> <li v-for="environment in store.environmentsData" diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index ed5c8b15945..5c6e2e09e46 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -121,6 +121,7 @@ export default { draw() { const breakpointSize = bp.getBreakpointSize(); const query = this.graphData.queries[0]; + const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width; this.margin = measurements.large.margin; if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') { this.graphHeight = 300; @@ -130,13 +131,13 @@ export default { this.unitOfDisplay = query.unit || ''; this.yAxisLabel = this.graphData.y_label || 'Values'; this.legendTitle = query.label || 'Average'; - this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right; + this.graphWidth = svgWidth - this.margin.left - this.margin.right; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.baseGraphHeight = this.graphHeight - 50; this.baseGraphWidth = this.graphWidth; // pixel offsets inside the svg and outside are not 1:1 - this.realPixelRatio = this.$refs.baseSvg.clientWidth / this.baseGraphWidth; + this.realPixelRatio = svgWidth / this.baseGraphWidth; this.renderAxesPaths(); this.formatDeployments(); diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index c68860d98ae..e707f44bf5a 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -95,29 +95,20 @@ export default { return awardList.filter(award => award.user.id === this.getUserData.id).length; }, awardTitle(awardsList) { - const hasReactionByCurrentUser = this.hasReactionByCurrentUser( - awardsList, - ); + const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList); const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; let awardList = awardsList; // Filter myself from list if I am awarded. if (hasReactionByCurrentUser) { - awardList = awardList.filter( - award => award.user.id !== this.getUserData.id, - ); + awardList = awardList.filter(award => award.user.id !== this.getUserData.id); } // Get only 9-10 usernames to show in tooltip text. - const namesToShow = awardList - .slice(0, TOOLTIP_NAME_COUNT) - .map(award => award.user.name); + const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); // Get the remaining list to use in `and x more` text. - const remainingAwardList = awardList.slice( - TOOLTIP_NAME_COUNT, - awardList.length, - ); + const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); // Add myself to the begining of the list so title will start with You. if (hasReactionByCurrentUser) { @@ -128,9 +119,7 @@ export default { // We have 10+ awarded user, join them with comma and add `and x more`. if (remainingAwardList.length) { - title = `${namesToShow.join(', ')}, and ${ - remainingAwardList.length - } more.`; + title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; } else if (namesToShow.length > 1) { // Join all names with comma but not the last one, it will be added with and text. title = namesToShow.slice(0, namesToShow.length - 1).join(', '); @@ -170,9 +159,7 @@ export default { awardName: parsedName, }; - this.toggleAwardRequest(data).catch(() => - Flash('Something went wrong on our end.'), - ); + this.toggleAwardRequest(data).catch(() => Flash('Something went wrong on our end.')); }, }, }; diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js index fa4a1c56b20..4532226aa07 100644 --- a/app/assets/javascripts/notes/stores/collapse_utils.js +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -68,10 +68,7 @@ export const collapseSystemNotes = notes => { lastDescriptionSystemNote = note; lastDescriptionSystemNoteIndex = acc.length; } else if (lastDescriptionSystemNote) { - const timeDifferenceMinutes = getTimeDifferenceMinutes( - lastDescriptionSystemNote, - note, - ); + const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note); // are they less than 10 minutes appart? if (timeDifferenceMinutes > 10) { diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 21c334a9d33..e4f36154fcd 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -1,6 +1,5 @@ import _ from 'underscore'; import * as constants from '../constants'; -import { reduceDiscussionsToLineCodes } from './utils'; import { collapseSystemNotes } from './collapse_utils'; export const discussions = state => collapseSystemNotes(state.discussions); @@ -31,9 +30,6 @@ export const notesById = state => return acc; }, {}); -export const discussionsStructuredByLineCode = state => - reduceDiscussionsToLineCodes(state.discussions); - export const noteableType = state => { const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index f105b7d0d11..d41b02b4a4b 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -4,5 +4,4 @@ import notesModule from './modules'; Vue.use(Vuex); -export default () => - new Vuex.Store(notesModule()); +export default () => new Vuex.Store(notesModule()); diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 0e41ff03d67..dd57539e4d8 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -25,18 +25,6 @@ export const getQuickActionText = note => { return text; }; -export const reduceDiscussionsToLineCodes = selectedDiscussions => - selectedDiscussions.reduce((acc, note) => { - if (note.diff_discussion && note.line_code) { - // For context about line notes: there might be multiple notes with the same line code - const items = acc[note.line_code] || []; - items.push(note); - - Object.assign(acc, { [note.line_code]: items }); - } - return acc; - }, {}); - export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 3b58c54b3f4..386a9b2c740 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -7,14 +7,21 @@ const ENDLESS_SCROLL_BOTTOM_PX = 400; const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; export default { - init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) { + init( + limit = 0, + preload = false, + disable = false, + prepareData = $.noop, + callback = $.noop, + container = '', + ) { this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']); this.limit = limit; this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; this.disable = disable; this.prepareData = prepareData; this.callback = callback; - this.loading = $('.loading').first(); + this.loading = $(`${container} .loading`).first(); if (preload) { this.offset = 0; this.getOld(); diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js index 15e737fff05..d9cf62db3f7 100644 --- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js @@ -31,7 +31,7 @@ export default class AbuseReports { $messageCellElement.text(originalMessage); } else { $messageCellElement.data('messageTruncated', 'true'); - $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`); + $messageCellElement.text(`${originalMessage.substr(0, MAX_MESSAGE_LENGTH - 3)}...`); } } } diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js index ff4d6ab15f9..4616a075729 100644 --- a/app/assets/javascripts/pages/admin/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -23,7 +23,7 @@ export default function adminInit() { } }); - $('body').on('click', '.js-toggle-colors-link', (e) => { + $('body').on('click', '.js-toggle-colors-link', e => { e.preventDefault(); $('.js-toggle-colors-container').toggleClass('hide'); }); @@ -33,12 +33,15 @@ export default function adminInit() { $(this).tab('show'); }); - $('.log-bottom').on('click', (e) => { + $('.log-bottom').on('click', e => { e.preventDefault(); const $visibleLog = $('.file-content:visible'); - $visibleLog.animate({ - scrollTop: $visibleLog.find('ol').height(), - }, 'fast'); + $visibleLog.animate( + { + scrollTop: $visibleLog.find('ol').height(), + }, + 'fast', + ); }); $('.change-owner-link').on('click', function changeOwnerLinkClick(e) { @@ -47,7 +50,7 @@ export default function adminInit() { modal.show(); }); - $('.change-owner-cancel-link').on('click', (e) => { + $('.change-owner-cancel-link').on('click', e => { e.preventDefault(); modal.hide(); $('.change-owner-link').show(); diff --git a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js index 7281f907ec7..455c637a6b3 100644 --- a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js +++ b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js @@ -1,10 +1,14 @@ import { __ } from '~/locale'; export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE = __('Regex pattern'); -export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __('To define internal users, first enable new users set to external'); +export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __( + 'To define internal users, first enable new users set to external', +); function setUserInternalRegexPlaceholder(checkbox) { - const userInternalRegex = document.getElementById('application_setting_user_default_internal_regex'); + const userInternalRegex = document.getElementById( + 'application_setting_user_default_internal_regex', + ); if (checkbox && userInternalRegex) { if (checkbox.checked) { userInternalRegex.readOnly = false; diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index e7ceccb6f47..d5ded3f9a79 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -17,20 +17,24 @@ export default () => { const previewPath = $('textarea#broadcast_message_message').data('previewPath'); - $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() { - const message = $(this).val(); - if (message === '') { - $('.js-broadcast-message-preview').text('Your message here'); - } else { - axios.post(previewPath, { - broadcast_message: { - message, - }, - }) - .then(({ data }) => { - $('.js-broadcast-message-preview').html(data.message); - }) - .catch(() => flash(__('An error occurred while rendering preview broadcast message'))); - } - }, 250)); + $('textarea#broadcast_message_message').on( + 'input', + _.debounce(function onMessageInput() { + const message = $(this).val(); + if (message === '') { + $('.js-broadcast-message-preview').text('Your message here'); + } else { + axios + .post(previewPath, { + broadcast_message: { + message, + }, + }) + .then(({ data }) => { + $('.js-broadcast-message-preview').html(data.message); + }) + .catch(() => flash(__('An error occurred while rendering preview broadcast message'))); + } + }, 250), + ); }; diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue index bc84666779e..e2fec3c7172 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -1,39 +1,42 @@ <script> - import axios from '~/lib/utils/axios_utils'; - import createFlash from '~/flash'; - import GlModal from '~/vue_shared/components/gl_modal.vue'; - import { redirectTo } from '~/lib/utils/url_utility'; - import { s__ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { redirectTo } from '~/lib/utils/url_utility'; +import { s__ } from '~/locale'; - export default { - components: { - GlModal, +export default { + components: { + GlModal, + }, + props: { + url: { + type: String, + required: true, }, - props: { - url: { - type: String, - required: true, - }, + }, + computed: { + text() { + return s__( + 'AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.', + ); }, - computed: { - text() { - return s__('AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.'); - }, + }, + methods: { + onSubmit() { + return axios + .post(this.url) + .then(response => { + // follow the rediect to refresh the page + redirectTo(response.request.responseURL); + }) + .catch(error => { + createFlash(s__('AdminArea|Stopping jobs failed')); + throw error; + }); }, - methods: { - onSubmit() { - return axios.post(this.url) - .then((response) => { - // follow the rediect to refresh the page - redirectTo(response.request.responseURL); - }) - .catch((error) => { - createFlash(s__('AdminArea|Stopping jobs failed')); - throw error; - }); - }, - }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js index 31c96eb87af..d6b1e747aec 100644 --- a/app/assets/javascripts/pages/admin/projects/index.js +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -4,6 +4,7 @@ import NamespaceSelect from '../../../namespace_select'; document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new - document.querySelectorAll('.js-namespace-select') + document + .querySelectorAll('.js-namespace-select') .forEach(dropdown => new NamespaceSelect({ dropdown })); }); diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue index ff66d3a8ac4..3c383735f4a 100644 --- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue +++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue @@ -1,81 +1,84 @@ <script> - import _ from 'underscore'; - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { s__, sprintf } from '~/locale'; +import _ from 'underscore'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { s__, sprintf } from '~/locale'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + deleteProjectUrl: { + type: String, + required: false, + default: '', }, - props: { - deleteProjectUrl: { - type: String, - required: false, - default: '', - }, - projectName: { - type: String, - required: false, - default: '', - }, - csrfToken: { - type: String, - required: false, - default: '', - }, + projectName: { + type: String, + required: false, + default: '', }, - data() { - return { - enteredProjectName: '', - }; + csrfToken: { + type: String, + required: false, + default: '', }, - computed: { - title() { - return sprintf(s__('AdminProjects|Delete Project %{projectName}?'), - { - projectName: `'${_.escape(this.projectName)}'`, - }, - false, - ); - }, - text() { - return sprintf(s__(`AdminProjects| + }, + data() { + return { + enteredProjectName: '', + }; + }, + computed: { + title() { + return sprintf( + s__('AdminProjects|Delete Project %{projectName}?'), + { + projectName: `'${_.escape(this.projectName)}'`, + }, + false, + ); + }, + text() { + return sprintf( + s__(`AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`), - { - projectName: `<strong>${_.escape(this.projectName)}</strong>`, - strong_start: '<strong>', - strong_end: '</strong>', - }, - false, - ); - }, - confirmationTextLabel() { - return sprintf(s__('AdminUsers|To confirm, type %{projectName}'), - { - projectName: `<code>${_.escape(this.projectName)}</code>`, - }, - false, - ); - }, - primaryButtonLabel() { - return s__('AdminProjects|Delete project'); - }, - canSubmit() { - return this.enteredProjectName === this.projectName; - }, + { + projectName: `<strong>${_.escape(this.projectName)}</strong>`, + strong_start: '<strong>', + strong_end: '</strong>', + }, + false, + ); + }, + confirmationTextLabel() { + return sprintf( + s__('AdminUsers|To confirm, type %{projectName}'), + { + projectName: `<code>${_.escape(this.projectName)}</code>`, + }, + false, + ); + }, + primaryButtonLabel() { + return s__('AdminProjects|Delete project'); + }, + canSubmit() { + return this.enteredProjectName === this.projectName; + }, + }, + methods: { + onCancel() { + this.enteredProjectName = ''; }, - methods: { - onCancel() { - this.enteredProjectName = ''; - }, - onSubmit() { - this.$refs.form.submit(); - this.enteredProjectName = ''; - }, + onSubmit() { + this.$refs.form.submit(); + this.enteredProjectName = ''; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js index ddbefec87b6..6fa8760545d 100644 --- a/app/assets/javascripts/pages/admin/projects/index/index.js +++ b/app/assets/javascripts/pages/admin/projects/index/index.js @@ -28,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => { }, }); - $(document).on('shown.bs.modal', (event) => { + $(document).on('shown.bs.modal', event => { if (event.relatedTarget.classList.contains('delete-project-button')) { const buttonProps = event.relatedTarget.dataset; deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl; diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue index 8d5efcdcd96..4b33fcc759a 100644 --- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -1,114 +1,119 @@ <script> - import _ from 'underscore'; - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { s__, sprintf } from '~/locale'; +import _ from 'underscore'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { s__, sprintf } from '~/locale'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + deleteUserUrl: { + type: String, + required: false, + default: '', }, - props: { - deleteUserUrl: { - type: String, - required: false, - default: '', - }, - blockUserUrl: { - type: String, - required: false, - default: '', - }, - deleteContributions: { - type: Boolean, - required: false, - default: false, - }, - username: { - type: String, - required: false, - default: '', - }, - csrfToken: { - type: String, - required: false, - default: '', - }, + blockUserUrl: { + type: String, + required: false, + default: '', }, - data() { - return { - enteredUsername: '', - }; + deleteContributions: { + type: Boolean, + required: false, + default: false, }, - computed: { - title() { - const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?'); - const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?'); + username: { + type: String, + required: false, + default: '', + }, + csrfToken: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + enteredUsername: '', + }; + }, + computed: { + title() { + const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?'); + const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?'); - return sprintf( - this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, { - username: `'${_.escape(this.username)}'`, - }, false); - }, - text() { - const keepContributionsText = s__(`AdminArea| + return sprintf( + this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, + { + username: `'${_.escape(this.username)}'`, + }, + false, + ); + }, + text() { + const keepContributionsText = s__(`AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); - const deleteContributionsText = s__(`AdminArea| + const deleteContributionsText = s__(`AdminArea| You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); - return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText, - { - username: `<strong>${_.escape(this.username)}</strong>`, - strong_start: '<strong>', - strong_end: '</strong>', - }, - false, - ); - }, - confirmationTextLabel() { - return sprintf(s__('AdminUsers|To confirm, type %{username}'), - { - username: `<code>${_.escape(this.username)}</code>`, - }, - false, - ); - }, - primaryButtonLabel() { - const keepContributionsLabel = s__('AdminUsers|Delete user'); - const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions'); + return sprintf( + this.deleteContributions ? deleteContributionsText : keepContributionsText, + { + username: `<strong>${_.escape(this.username)}</strong>`, + strong_start: '<strong>', + strong_end: '</strong>', + }, + false, + ); + }, + confirmationTextLabel() { + return sprintf( + s__('AdminUsers|To confirm, type %{username}'), + { + username: `<code>${_.escape(this.username)}</code>`, + }, + false, + ); + }, + primaryButtonLabel() { + const keepContributionsLabel = s__('AdminUsers|Delete user'); + const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions'); - return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel; - }, - secondaryButtonLabel() { - return s__('AdminUsers|Block user'); - }, - canSubmit() { - return this.enteredUsername === this.username; - }, + return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel; }, - methods: { - onCancel() { - this.enteredUsername = ''; - }, - onSecondaryAction() { - const { form } = this.$refs; + secondaryButtonLabel() { + return s__('AdminUsers|Block user'); + }, + canSubmit() { + return this.enteredUsername === this.username; + }, + }, + methods: { + onCancel() { + this.enteredUsername = ''; + }, + onSecondaryAction() { + const { form } = this.$refs; - form.action = this.blockUserUrl; - this.$refs.method.value = 'put'; + form.action = this.blockUserUrl; + this.$refs.method.value = 'put'; - form.submit(); - }, - onSubmit() { - this.$refs.form.submit(); - this.enteredUsername = ''; - }, + form.submit(); + }, + onSubmit() { + this.$refs.form.submit(); + this.enteredUsername = ''; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js index 06599c3fd5f..45046688b57 100644 --- a/app/assets/javascripts/pages/admin/users/index.js +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -32,12 +32,14 @@ document.addEventListener('DOMContentLoaded', () => { }, }); - $(document).on('shown.bs.modal', (event) => { + $(document).on('shown.bs.modal', event => { if (event.relatedTarget.classList.contains('delete-user-button')) { const buttonProps = event.relatedTarget.dataset; deleteModal.deleteUserUrl = buttonProps.deleteUserUrl; deleteModal.blockUserUrl = buttonProps.blockUserUrl; - deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions'); + deleteModal.deleteContributions = event.relatedTarget.hasAttribute( + 'data-delete-contributions', + ); deleteModal.username = buttonProps.username; } }); diff --git a/app/assets/javascripts/pages/admin/users/new/index.js b/app/assets/javascripts/pages/admin/users/new/index.js index 58bfa8d64e7..3e6a090cb0e 100644 --- a/app/assets/javascripts/pages/admin/users/new/index.js +++ b/app/assets/javascripts/pages/admin/users/new/index.js @@ -4,7 +4,9 @@ export default class UserInternalRegexHandler { constructor() { this.regexPattern = $('[data-user-internal-regex-pattern]').data('user-internal-regex-pattern'); if (this.regexPattern && this.regexPattern !== '') { - this.regexOptions = $('[data-user-internal-regex-options]').data('user-internal-regex-options'); + this.regexOptions = $('[data-user-internal-regex-options]').data( + 'user-internal-regex-options', + ); this.external = $('#user_external'); this.warningMessage = $('#warning_external_automatically_set'); this.addListenerToEmailField(); @@ -13,7 +15,7 @@ export default class UserInternalRegexHandler { } addListenerToEmailField() { - $('#user_email').on('input', (event) => { + $('#user_email').on('input', event => { this.setExternalCheckbox(event.currentTarget.value); }); } diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 72f3f70b98f..1b56b97f751 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -79,7 +79,8 @@ export default class Todos { .then(({ data }) => { this.updateRowState(target); this.updateBadges(data); - }).catch(() => { + }) + .catch(() => { this.updateRowState(target, true); return flash(__('Error updating todo status.')); }); @@ -118,10 +119,12 @@ export default class Todos { axios[target.dataset.method](target.dataset.href, { ids: this.todo_ids, - }).then(({ data }) => { - this.updateAllState(target, data); - this.updateBadges(data); - }).catch(() => flash(__('Error updating status for all todos.'))); + }) + .then(({ data }) => { + this.updateAllState(target, data); + this.updateBadges(data); + }) + .catch(() => flash(__('Error updating status for all todos.'))); } updateAllState(target, data) { @@ -133,7 +136,7 @@ export default class Todos { target.removeAttribute('disabled'); target.classList.remove('disabled'); - this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : []; + this.todo_ids = target === markAllDoneBtn ? data.updated_ids : []; undoAllBtn.classList.toggle('hidden'); markAllDoneBtn.classList.toggle('hidden'); todoListContainer.classList.toggle('hidden'); diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue index 48668562f09..a4778077bc4 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -1,94 +1,117 @@ <script> - import axios from '~/lib/utils/axios_utils'; +import axios from '~/lib/utils/axios_utils'; - import Flash from '~/flash'; - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { n__, s__, sprintf } from '~/locale'; - import { redirectTo } from '~/lib/utils/url_utility'; - import eventHub from '../event_hub'; +import Flash from '~/flash'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { n__, s__, sprintf } from '~/locale'; +import { redirectTo } from '~/lib/utils/url_utility'; +import eventHub from '../event_hub'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + issueCount: { + type: Number, + required: true, }, - props: { - issueCount: { - type: Number, - required: true, - }, - mergeRequestCount: { - type: Number, - required: true, - }, - milestoneId: { - type: Number, - required: true, - }, - milestoneTitle: { - type: String, - required: true, - }, - milestoneUrl: { - type: String, - required: true, - }, + mergeRequestCount: { + type: Number, + required: true, }, - computed: { - text() { - const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle }); - - if (this.issueCount === 0 && this.mergeRequestCount === 0) { - return sprintf( - s__(`Milestones| -You’re about to permanently delete the milestone %{milestoneTitle}. -This milestone is not currently used in any issues or merge requests.`), - { - milestoneTitle, - }, - false, - ); - } + milestoneId: { + type: Number, + required: true, + }, + milestoneTitle: { + type: String, + required: true, + }, + milestoneUrl: { + type: String, + required: true, + }, + }, + computed: { + text() { + const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { + milestoneTitle: this.milestoneTitle, + }); + if (this.issueCount === 0 && this.mergeRequestCount === 0) { return sprintf( s__(`Milestones| -You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. -Once deleted, it cannot be undone or recovered.`), +You’re about to permanently delete the milestone %{milestoneTitle}. +This milestone is not currently used in any issues or merge requests.`), { milestoneTitle, - issuesWithCount: n__('%d issue', '%d issues', this.issueCount), - mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount), }, false, ); - }, - title() { - return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle }); - }, - }, - methods: { - onSubmit() { - eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); + } - return axios.delete(this.milestoneUrl) - .then((response) => { - eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true }); + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. +Once deleted, it cannot be undone or recovered.`), + { + milestoneTitle, + issuesWithCount: n__('%d issue', '%d issues', this.issueCount), + mergeRequestsWithCount: n__( + '%d merge request', + '%d merge requests', + this.mergeRequestCount, + ), + }, + false, + ); + }, + title() { + return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { + milestoneTitle: this.milestoneTitle, + }); + }, + }, + methods: { + onSubmit() { + eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); - // follow the rediect to milestones overview page - redirectTo(response.request.responseURL); - }) - .catch((error) => { - eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false }); + return axios + .delete(this.milestoneUrl) + .then(response => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { + milestoneUrl: this.milestoneUrl, + successful: true, + }); - if (error.response && error.response.status === 404) { - Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle })); - } else { - Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle })); - } - throw error; + // follow the rediect to milestones overview page + redirectTo(response.request.responseURL); + }) + .catch(error => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { + milestoneUrl: this.milestoneUrl, + successful: false, }); - }, + + if (error.response && error.response.status === 404) { + Flash( + sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { + milestoneTitle: this.milestoneTitle, + }), + ); + } else { + Flash( + sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { + milestoneTitle: this.milestoneTitle, + }), + ); + } + throw error; + }); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js index d51b5c221e3..1d559dc6e41 100644 --- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js @@ -7,7 +7,9 @@ export default () => { Vue.use(Translate); const onRequestFinished = ({ milestoneUrl, successful }) => { - const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + const button = document.querySelector( + `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`, + ); if (!successful) { button.removeAttribute('disabled'); @@ -16,14 +18,16 @@ export default () => { button.querySelector('.js-loading-icon').classList.add('hidden'); }; - const onRequestStarted = (milestoneUrl) => { - const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + const onRequestStarted = milestoneUrl => { + const button = document.querySelector( + `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`, + ); button.setAttribute('disabled', ''); button.querySelector('.js-loading-icon').classList.remove('hidden'); eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = (event) => { + const onDeleteButtonClick = event => { const button = event.currentTarget; const modalProps = { milestoneId: parseInt(button.dataset.milestoneId, 10), @@ -37,12 +41,12 @@ export default () => { }; const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); - deleteMilestoneButtons.forEach((button) => { + deleteMilestoneButtons.forEach(button => { button.addEventListener('click', onDeleteButtonClick); }); eventHub.$once('deleteMilestoneModal.mounted', () => { - deleteMilestoneButtons.forEach((button) => { + deleteMilestoneButtons.forEach(button => { button.removeAttribute('disabled'); }); }); diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js index 8e79341e96a..fcc62a2b2af 100644 --- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js @@ -7,20 +7,24 @@ Vue.use(Translate); export default () => { const onRequestFinished = ({ milestoneUrl, successful }) => { - const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`); + const button = document.querySelector( + `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`, + ); if (!successful) { button.removeAttribute('disabled'); } }; - const onRequestStarted = (milestoneUrl) => { - const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`); + const onRequestStarted = milestoneUrl => { + const button = document.querySelector( + `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`, + ); button.setAttribute('disabled', ''); eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = (event) => { + const onDeleteButtonClick = event => { const button = event.currentTarget; const modalProps = { milestoneTitle: button.dataset.milestoneTitle, @@ -32,12 +36,12 @@ export default () => { }; const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button'); - promoteMilestoneButtons.forEach((button) => { + promoteMilestoneButtons.forEach(button => { button.addEventListener('click', onDeleteButtonClick); }); eventHub.$once('promoteMilestoneModal.mounted', () => { - promoteMilestoneButtons.forEach((button) => { + promoteMilestoneButtons.forEach(button => { button.removeAttribute('disabled'); }); }); diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js index 04e50963699..883be18b336 100644 --- a/app/assets/javascripts/pages/profiles/index.js +++ b/app/assets/javascripts/pages/profiles/index.js @@ -3,9 +3,12 @@ import '~/profile/gl_crop'; import Profile from '~/profile/profile'; document.addEventListener('DOMContentLoaded', () => { - $(document).on('input.ssh_key', '#key_key', function () { // eslint-disable-line func-names + // eslint-disable-next-line func-names + $(document).on('input.ssh_key', '#key_key', function() { const $title = $('#key_title'); - const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); + const comment = $(this) + .val() + .match(/^\S+ \S+ (.+)\n?$/); // Extract the SSH Key title from its comment if (comment && comment.length > 1) { diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js index 8e8f47c21d8..417935e2ad0 100644 --- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js +++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js @@ -5,7 +5,9 @@ document.addEventListener('DOMContentLoaded', () => { const twoFactorNode = document.querySelector('.js-two-factor-auth'); const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true'; if (skippable) { - const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; + const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${ + twoFactorNode.dataset.two_factor_skip_url + }">Configure it later</a>`; const flashAlert = document.querySelector('.flash-alert .container-fluid'); if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button); } diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js index a9658fd1eb4..13ff47d53c2 100644 --- a/app/assets/javascripts/pages/projects/branches/new/index.js +++ b/app/assets/javascripts/pages/projects/branches/new/index.js @@ -1,6 +1,11 @@ import $ from 'jquery'; import NewBranchForm from '~/new_branch_form'; -document.addEventListener('DOMContentLoaded', () => ( - new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)) -)); +document.addEventListener( + 'DOMContentLoaded', + () => + new NewBranchForm( + $('.js-create-branch-form'), + JSON.parse(document.getElementById('availableRefs').innerHTML), + ), +); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 80159a82bd4..3ccad513c05 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -31,14 +31,16 @@ document.addEventListener('DOMContentLoaded', () => { const chartData = data => ({ labels: Object.keys(data), - datasets: [{ - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, - data: _.values(data), - }], + datasets: [ + { + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: _.values(data), + }, + ], }); const hourData = chartData(projectChartData.hour); @@ -51,7 +53,9 @@ document.addEventListener('DOMContentLoaded', () => { responsiveChart($('#month-chart'), monthData); const data = projectChartData.languages; - const ctx = $('#languages-chart').get(0).getContext('2d'); + const ctx = $('#languages-chart') + .get(0) + .getContext('2d'); const options = { scaleOverlay: true, responsive: true, diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js index 71f629fbc13..f79c386b59e 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/index.js +++ b/app/assets/javascripts/pages/projects/graphs/show/index.js @@ -7,7 +7,8 @@ import ContributorsStatGraph from './stat_graph_contributors'; document.addEventListener('DOMContentLoaded', () => { const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath; - axios.get(url) + axios + .get(url) .then(({ data }) => { const graph = new ContributorsStatGraph(); graph.init(data); diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js index 58bb8c5b0c8..76613394af6 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js @@ -3,7 +3,11 @@ import $ from 'jquery'; import _ from 'underscore'; import { n__, s__, createDateTimeFormat, sprintf } from '~/locale'; -import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; +import { + ContributorsGraph, + ContributorsAuthorGraph, + ContributorsMasterGraph, +} from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; export default (function() { @@ -14,7 +18,7 @@ export default (function() { ContributorsStatGraph.prototype.init = function(log) { var author_commits, total_commits; this.parsed_log = ContributorsStatGraphUtil.parse_log(log); - this.set_current_field("commits"); + this.set_current_field('commits'); total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); this.add_master_graph(total_commits); @@ -31,23 +35,26 @@ export default (function() { var limited_author_data; this.authors = []; limited_author_data = author_data.slice(0, 100); - return _.each(limited_author_data, (function(_this) { - return function(d) { - var author_graph, author_header; - author_header = _this.create_author_header(d); - $(".contributors-list").append(author_header); - - author_graph = new ContributorsAuthorGraph(d.dates); - _this.authors[d.author_name] = author_graph; - return author_graph.draw(); - }; - })(this)); + return _.each( + limited_author_data, + (function(_this) { + return function(d) { + var author_graph, author_header; + author_header = _this.create_author_header(d); + $('.contributors-list').append(author_header); + + author_graph = new ContributorsAuthorGraph(d.dates); + _this.authors[d.author_name] = author_graph; + return author_graph.draw(); + }; + })(this), + ); }; ContributorsStatGraph.prototype.format_author_commit_info = function(author) { var commits; commits = $('<span/>', { - "class": 'graph-author-commits-count' + class: 'graph-author-commits-count', }); commits.text(n__('%d commit', '%d commits', author.commits)); return $('<span/>').append(commits); @@ -56,13 +63,13 @@ export default (function() { ContributorsStatGraph.prototype.create_author_header = function(author) { var author_commit_info, author_commit_info_span, author_email, author_name, list_item; list_item = $('<li/>', { - "class": 'person', - style: 'display: block;' + class: 'person', + style: 'display: block;', }); author_name = $('<h4>' + author.author_name + '</h4>'); author_email = $('<p class="graph-author-email">' + author.author_email + '</p>'); author_commit_info_span = $('<span/>', { - "class": 'commits' + class: 'commits', }); author_commit_info = this.format_author_commit_info(author); author_commit_info_span.html(author_commit_info); @@ -80,37 +87,41 @@ export default (function() { }; ContributorsStatGraph.prototype.redraw_authors = function() { - $("ol").html(""); + $('ol').html(''); const { x_domain } = ContributorsGraph.prototype; - const author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); - - return _.each(author_commits, (function(_this) { - return function(d) { - _this.redraw_author_commit_info(d); - if (_this.authors[d.author_name] != null) { - $(_this.authors[d.author_name].list_item).appendTo("ol"); - _this.authors[d.author_name].set_data(d.dates); - return _this.authors[d.author_name].redraw(); - } - return ''; - }; - })(this)); + const author_commits = ContributorsStatGraphUtil.get_author_data( + this.parsed_log, + this.field, + x_domain, + ); + + return _.each( + author_commits, + (function(_this) { + return function(d) { + _this.redraw_author_commit_info(d); + if (_this.authors[d.author_name] != null) { + $(_this.authors[d.author_name].list_item).appendTo('ol'); + _this.authors[d.author_name].set_data(d.dates); + return _this.authors[d.author_name].redraw(); + } + return ''; + }; + })(this), + ); }; ContributorsStatGraph.prototype.set_current_field = function(field) { - return this.field = field; + return (this.field = field); }; ContributorsStatGraph.prototype.change_date_header = function() { const { x_domain } = ContributorsGraph.prototype; - const formattedDateRange = sprintf( - s__('ContributorsPage|%{startDate} – %{endDate}'), - { - startDate: this.dateFormat.format(new Date(x_domain[0])), - endDate: this.dateFormat.format(new Date(x_domain[1])), - }, - ); + const formattedDateRange = sprintf(s__('ContributorsPage|%{startDate} – %{endDate}'), { + startDate: this.dateFormat.format(new Date(x_domain[0])), + endDate: this.dateFormat.format(new Date(x_domain[1])), + }); return $('#date_header').text(formattedDateRange); }; @@ -120,7 +131,7 @@ export default (function() { if ($author != null) { author_list_item = $(this.authors[author.author_name].list_item); author_commit_info = this.format_author_commit_info(author); - return author_list_item.find("span").html(author_commit_info); + return author_list_item.find('span').html(author_commit_info); } return ''; }; diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index 5f91686347a..377dce6c746 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -11,10 +11,32 @@ import { brushX } from 'd3-brush'; import { timeParse } from 'd3-time-format'; import { dateTickFormat } from '~/lib/utils/tick_formats'; -const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; +const d3 = { + extent, + max, + select, + scaleTime, + scaleLinear, + axisLeft, + axisBottom, + area, + brushX, + timeParse, +}; const hasProp = {}.hasOwnProperty; -const extend = function(child, parent) { for (const 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; }; +const extend = function(child, parent) { + for (const 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; +}; export const ContributorsGraph = (function() { function ContributorsGraph() {} @@ -23,7 +45,7 @@ export const ContributorsGraph = (function() { top: 20, right: 10, bottom: 30, - left: 40 + left: 40, }; ContributorsGraph.prototype.x_domain = null; @@ -33,35 +55,39 @@ export const ContributorsGraph = (function() { ContributorsGraph.prototype.dates = []; ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) { - const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); + const parentPaddingWidth = + parseFloat($parentElement.css('padding-left')) + + parseFloat($parentElement.css('padding-right')); const marginWidth = this.MARGIN.left + this.MARGIN.right; return baseWidth - parentPaddingWidth - marginWidth; }; ContributorsGraph.set_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = data; + return (ContributorsGraph.prototype.x_domain = data); }; ContributorsGraph.set_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; + return (ContributorsGraph.prototype.y_domain = [ + 0, + d3.max(data, function(d) { + return (d.commits = d.commits || d.additions || d.deletions); + }), + ]); }; ContributorsGraph.init_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { + return (ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { return d.date; - }); + })); }; ContributorsGraph.init_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; + return (ContributorsGraph.prototype.y_domain = [ + 0, + d3.max(data, function(d) { + return (d.commits = d.commits || d.additions || d.deletions); + }), + ]); }; ContributorsGraph.init_domain = function(data) { @@ -70,7 +96,7 @@ export const ContributorsGraph = (function() { }; ContributorsGraph.set_dates = function(data) { - return ContributorsGraph.prototype.dates = data; + return (ContributorsGraph.prototype.dates = data); }; ContributorsGraph.prototype.set_x_domain = function() { @@ -87,20 +113,33 @@ export const ContributorsGraph = (function() { }; ContributorsGraph.prototype.create_scale = function(width, height) { - this.x = d3.scaleTime().range([0, width]).clamp(true); - return this.y = d3.scaleLinear().range([height, 0]).nice(); + this.x = d3 + .scaleTime() + .range([0, width]) + .clamp(true); + return (this.y = d3 + .scaleLinear() + .range([height, 0]) + .nice()); }; ContributorsGraph.prototype.draw_x_axis = function() { - return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); + return this.svg + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0, ' + this.height + ')') + .call(this.x_axis); }; ContributorsGraph.prototype.draw_y_axis = function() { - return this.svg.append("g").attr("class", "y axis").call(this.y_axis); + return this.svg + .append('g') + .attr('class', 'y axis') + .call(this.y_axis); }; ContributorsGraph.prototype.set_data = function(data) { - return this.data = data; + return (this.data = data); }; return ContributorsGraph; @@ -137,9 +176,9 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.parse_dates = function(data) { - const parseDate = d3.timeParse("%Y-%m-%d"); + const parseDate = d3.timeParse('%Y-%m-%d'); return data.forEach(function(d) { - return d.date = parseDate(d.date); + return (d.date = parseDate(d.date)); }); }; @@ -148,42 +187,63 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.create_axes = function() { - this.x_axis = d3.axisBottom() + this.x_axis = d3 + .axisBottom() .scale(this.x) .tickFormat(dateTickFormat); - return this.y_axis = d3.axisLeft().scale(this.y).ticks(5); + return (this.y_axis = d3 + .axisLeft() + .scale(this.y) + .ticks(5)); }; ContributorsMasterGraph.prototype.create_svg = function() { - this.svg = d3.select("#contributors-master") - .append("svg") - .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) - .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) - .attr("class", "tint-box") - .append("g") - .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3 + .select('#contributors-master') + .append('svg') + .attr('width', this.width + this.MARGIN.left + this.MARGIN.right) + .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr('class', 'tint-box') + .append('g') + .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')'); return this.svg; }; ContributorsMasterGraph.prototype.create_area = function(x, y) { - return this.area = d3.area().x(function(d) { - return x(d.date); - }).y0(this.height).y1(function(d) { - d.commits = d.commits || d.additions || d.deletions; - return y(d.commits); - }); + return (this.area = d3 + .area() + .x(function(d) { + return x(d.date); + }) + .y0(this.height) + .y1(function(d) { + d.commits = d.commits || d.additions || d.deletions; + return y(d.commits); + })); }; ContributorsMasterGraph.prototype.create_brush = function() { - return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content); + return (this.brush = d3 + .brushX(this.x) + .extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]) + .on('end', this.update_content)); }; ContributorsMasterGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); + return this.svg + .append('path') + .datum(data) + .attr('class', 'area') + .attr('d', this.area); }; ContributorsMasterGraph.prototype.add_brush = function() { - return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); + return this.svg + .append('g') + .attr('class', 'selection') + .call(this.brush) + .selectAll('rect') + .attr('height', this.height); }; ContributorsMasterGraph.prototype.update_content = function() { @@ -193,7 +253,7 @@ export const ContributorsMasterGraph = (function(superClass) { } else { ContributorsGraph.set_x_domain(this.x_max_domain); } - return $("#brush_change").trigger('change'); + return $('#brush_change').trigger('change'); }; ContributorsMasterGraph.prototype.draw = function() { @@ -216,9 +276,9 @@ export const ContributorsMasterGraph = (function(superClass) { this.process_dates(this.data); ContributorsGraph.set_y_domain(this.data); this.set_y_domain(); - this.svg.select("path").datum(this.data); - this.svg.select("path").attr("d", this.area); - return this.svg.select(".y.axis").call(this.y_axis); + this.svg.select('path').datum(this.data); + this.svg.select('path').attr('d', this.area); + return this.svg.select('.y.axis').call(this.y_axis); }; return ContributorsMasterGraph; @@ -252,43 +312,58 @@ export const ContributorsAuthorGraph = (function(superClass) { }; ContributorsAuthorGraph.prototype.create_axes = function() { - this.x_axis = d3.axisBottom() + this.x_axis = d3 + .axisBottom() .scale(this.x) .ticks(8) .tickFormat(dateTickFormat); - return this.y_axis = d3.axisLeft().scale(this.y).ticks(5); + return (this.y_axis = d3 + .axisLeft() + .scale(this.y) + .ticks(5)); }; ContributorsAuthorGraph.prototype.create_area = function(x, y) { - return this.area = d3.area().x(function(d) { - const parseDate = d3.timeParse("%Y-%m-%d"); - return x(parseDate(d)); - }).y0(this.height).y1((function(_this) { - return function(d) { - if (_this.data[d] != null) { - return y(_this.data[d]); - } else { - return y(0); - } - }; - })(this)); + return (this.area = d3 + .area() + .x(function(d) { + const parseDate = d3.timeParse('%Y-%m-%d'); + return x(parseDate(d)); + }) + .y0(this.height) + .y1( + (function(_this) { + return function(d) { + if (_this.data[d] != null) { + return y(_this.data[d]); + } else { + return y(0); + } + }; + })(this), + )); }; ContributorsAuthorGraph.prototype.create_svg = function() { const persons = document.querySelectorAll('.person'); this.list_item = persons[persons.length - 1]; - this.svg = d3.select(this.list_item) - .append("svg") - .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) - .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) - .attr("class", "spark") - .append("g") - .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3 + .select(this.list_item) + .append('svg') + .attr('width', this.width + this.MARGIN.left + this.MARGIN.right) + .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr('class', 'spark') + .append('g') + .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')'); return this.svg; }; ContributorsAuthorGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); + return this.svg + .append('path') + .datum(data) + .attr('class', 'area-contributor') + .attr('d', this.area); }; ContributorsAuthorGraph.prototype.draw = function() { @@ -304,10 +379,10 @@ export const ContributorsAuthorGraph = (function(superClass) { ContributorsAuthorGraph.prototype.redraw = function() { this.set_domain(); - this.svg.select("path").datum(this.dates); - this.svg.select("path").attr("d", this.area); - this.svg.select(".x.axis").call(this.x_axis); - return this.svg.select(".y.axis").call(this.y_axis); + this.svg.select('path').datum(this.dates); + this.svg.select('path').attr('d', this.area); + this.svg.select('.x.axis').call(this.x_axis); + return this.svg.select('.y.axis').call(this.y_axis); }; return ContributorsAuthorGraph; diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js index cd0e2bc023c..988ae164955 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js @@ -26,12 +26,12 @@ export default { by_author = _.toArray(by_author); return { total: total, - by_author: by_author + by_author: by_author, }; }, add_date: function(date, collection) { collection[date] = {}; - return collection[date].date = date; + return (collection[date].date = date); }, add_author: function(author, by_author, by_email) { var data, normalized_email; @@ -49,28 +49,28 @@ export default { return this.store_deletions(entry, total, by_author); }, store_commits: function(total, by_author) { - this.add(total, "commits", 1); - return this.add(by_author, "commits", 1); + this.add(total, 'commits', 1); + return this.add(by_author, 'commits', 1); }, add: function(collection, field, value) { if (collection[field] == null) { collection[field] = 0; } - return collection[field] += value; + return (collection[field] += value); }, store_additions: function(entry, total, by_author) { if (entry.additions == null) { entry.additions = 0; } - this.add(total, "additions", entry.additions); - return this.add(by_author, "additions", entry.additions); + this.add(total, 'additions', entry.additions); + return this.add(by_author, 'additions', entry.additions); }, store_deletions: function(entry, total, by_author) { if (entry.deletions == null) { entry.deletions = 0; } - this.add(total, "deletions", entry.deletions); - return this.add(by_author, "deletions", entry.deletions); + this.add(total, 'deletions', entry.deletions); + return this.add(by_author, 'deletions', entry.deletions); }, get_total_data: function(parsed_log, field) { var log, total_data; @@ -95,15 +95,18 @@ export default { } log = parsed_log.by_author; author_data = []; - _.each(log, (function(_this) { - return function(log_entry) { - var parsed_log_entry; - parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); - if (!_.isEmpty(parsed_log_entry.dates)) { - return author_data.push(parsed_log_entry); - } - }; - })(this)); + _.each( + log, + (function(_this) { + return function(log_entry) { + var parsed_log_entry; + parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); + if (!_.isEmpty(parsed_log_entry.dates)) { + return author_data.push(parsed_log_entry); + } + }; + })(this), + ); return _.sortBy(author_data, function(d) { return d[field]; }).reverse(); @@ -120,16 +123,19 @@ export default { parsed_entry.additions = 0; parsed_entry.deletions = 0; - _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { - return function(value, key) { - if (_this.in_range(value.date, date_range)) { - parsed_entry.dates[value.date] = value[field]; - parsed_entry.commits += value.commits; - parsed_entry.additions += value.additions; - return parsed_entry.deletions += value.deletions; - } - }; - })(this)); + _.each( + _.omit(log_entry, 'author_name', 'author_email'), + (function(_this) { + return function(value, key) { + if (_this.in_range(value.date, date_range)) { + parsed_entry.dates[value.date] = value[field]; + parsed_entry.commits += value.commits; + parsed_entry.additions += value.additions; + return (parsed_entry.deletions += value.deletions); + } + }; + })(this), + ); return parsed_entry; }, in_range: function(date, date_range) { @@ -139,5 +145,5 @@ export default { } else { return false; } - } + }, }; diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index bc08ccf3584..bd8afa2d5ba 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -16,7 +16,8 @@ export default () => { ); const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); + const fileBlobPermalinkUrl = + fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js index 9f20a3e4e46..019efe077f7 100644 --- a/app/assets/javascripts/pages/projects/init_form.js +++ b/app/assets/javascripts/pages/projects/init_form.js @@ -1,7 +1,7 @@ import ZenMode from '~/zen_mode'; import GLForm from '~/gl_form'; -export default function ($formEl) { +export default function($formEl) { new ZenMode(); // eslint-disable-line no-new new GLForm($formEl); // eslint-disable-line no-new } diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index ef65196872c..8987c8e3f47 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -5,7 +5,7 @@ import ZenMode from '~/zen_mode'; import '~/notes/index'; import initIssueableApp from '~/issue_show'; -export default function () { +export default function() { initIssueableApp(); new Issue(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index 5d2247f6c6d..e8b646f3f6e 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -1,72 +1,86 @@ <script> - import _ from 'underscore'; - import axios from '~/lib/utils/axios_utils'; - import createFlash from '~/flash'; - import GlModal from '~/vue_shared/components/gl_modal.vue'; - import { s__, sprintf } from '~/locale'; - import { visitUrl } from '~/lib/utils/url_utility'; - import eventHub from '../event_hub'; +import _ from 'underscore'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; +import { visitUrl } from '~/lib/utils/url_utility'; +import eventHub from '../event_hub'; - export default { - components: { - GlModal, +export default { + components: { + GlModal, + }, + props: { + url: { + type: String, + required: true, }, - props: { - url: { - type: String, - required: true, - }, - labelTitle: { - type: String, - required: true, - }, - labelColor: { - type: String, - required: true, - }, - labelTextColor: { - type: String, - required: true, - }, - groupName: { - type: String, - required: true, - }, + labelTitle: { + type: String, + required: true, }, - computed: { - text() { - return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. - Existing project labels with the same title will be merged. This action cannot be reversed.`), { + labelColor: { + type: String, + required: true, + }, + labelTextColor: { + type: String, + required: true, + }, + groupName: { + type: String, + required: true, + }, + }, + computed: { + text() { + return sprintf( + s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. + Existing project labels with the same title will be merged. This action cannot be reversed.`), + { labelTitle: this.labelTitle, groupName: this.groupName, - }); - }, - title() { - const label = `<span + }, + ); + }, + title() { + const label = `<span class="label color-label" style="background-color: ${this.labelColor}; color: ${this.labelTextColor};" >${_.escape(this.labelTitle)}</span>`; - return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), { + return sprintf( + s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), + { labelTitle: label, - }, false); - }, + }, + false, + ); }, - methods: { - onSubmit() { - eventHub.$emit('promoteLabelModal.requestStarted', this.url); - return axios.post(this.url, { params: { format: 'json' } }) - .then((response) => { - eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true }); - visitUrl(response.data.url); - }) - .catch((error) => { - eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false }); - createFlash(error); + }, + methods: { + onSubmit() { + eventHub.$emit('promoteLabelModal.requestStarted', this.url); + return axios + .post(this.url, { params: { format: 'json' } }) + .then(response => { + eventHub.$emit('promoteLabelModal.requestFinished', { + labelUrl: this.url, + successful: true, + }); + visitUrl(response.data.url); + }) + .catch(error => { + eventHub.$emit('promoteLabelModal.requestFinished', { + labelUrl: this.url, + successful: false, }); - }, + createFlash(error); + }); }, - }; + }, +}; </script> <template> <gl-modal diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index 03cfef61311..36cf485f33d 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -10,20 +10,24 @@ const initLabelIndex = () => { initLabels(); const onRequestFinished = ({ labelUrl, successful }) => { - const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`); + const button = document.querySelector( + `.js-promote-project-label-button[data-url="${labelUrl}"]`, + ); if (!successful) { button.removeAttribute('disabled'); } }; - const onRequestStarted = (labelUrl) => { - const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`); + const onRequestStarted = labelUrl => { + const button = document.querySelector( + `.js-promote-project-label-button[data-url="${labelUrl}"]`, + ); button.setAttribute('disabled', ''); eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = (event) => { + const onDeleteButtonClick = event => { const button = event.currentTarget; const modalProps = { labelTitle: button.dataset.labelTitle, @@ -37,12 +41,12 @@ const initLabelIndex = () => { }; const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button'); - promoteLabelButtons.forEach((button) => { + promoteLabelButtons.forEach(button => { button.addEventListener('click', onDeleteButtonClick); }); eventHub.$once('promoteLabelModal.mounted', () => { - promoteLabelButtons.forEach((button) => { + promoteLabelButtons.forEach(button => { button.removeAttribute('disabled'); }); }); diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js index 70fbb3f301c..226d63f05c4 100644 --- a/app/assets/javascripts/pages/projects/network/network.js +++ b/app/assets/javascripts/pages/projects/network/network.js @@ -6,13 +6,15 @@ import BranchGraph from '../../../network/branch_graph'; export default (function() { function Network(opts) { var vph; - $("#filter_ref").click(function() { - return $(this).closest('form').submit(); + $('#filter_ref').click(function() { + return $(this) + .closest('form') + .submit(); }); - this.branch_graph = new BranchGraph($(".network-graph"), opts); + this.branch_graph = new BranchGraph($('.network-graph'), opts); vph = $(window).height() - 250; $('.network-graph').css({ - 'height': vph + 'px' + height: vph + 'px', }); } diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js index 544360dcd51..6197dc8a9db 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js @@ -1,12 +1,16 @@ import Vue from 'vue'; import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#pipeline-schedules-callout', - components: { - 'pipeline-schedules-callout': PipelineSchedulesCallout, - }, - render(createElement) { - return createElement('pipeline-schedules-callout'); - }, -})); +document.addEventListener( + 'DOMContentLoaded', + () => + new Vue({ + el: '#pipeline-schedules-callout', + components: { + 'pipeline-schedules-callout': PipelineSchedulesCallout, + }, + render(createElement) { + return createElement('pipeline-schedules-callout'); + }, + }), +); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index ef53d67e7cb..ab6f42d928c 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -1,63 +1,63 @@ <script> - import _ from 'underscore'; +import _ from 'underscore'; - export default { - props: { - initialCronInterval: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - inputNameAttribute: 'schedule[cron]', - cronInterval: this.initialCronInterval, - cronIntervalPresets: { - everyDay: '0 4 * * *', - everyWeek: '0 4 * * 0', - everyMonth: '0 4 1 * *', - }, - cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', - customInputEnabled: false, - }; +export default { + props: { + initialCronInterval: { + type: String, + required: false, + default: '', }, - computed: { - intervalIsPreset() { - return _.contains(this.cronIntervalPresets, this.cronInterval); - }, - // The text input is editable when there's a custom interval, or when it's - // a preset interval and the user clicks the 'custom' radio button - isEditable() { - return !!(this.customInputEnabled || !this.intervalIsPreset); + }, + data() { + return { + inputNameAttribute: 'schedule[cron]', + cronInterval: this.initialCronInterval, + cronIntervalPresets: { + everyDay: '0 4 * * *', + everyWeek: '0 4 * * 0', + everyMonth: '0 4 1 * *', }, + cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', + customInputEnabled: false, + }; + }, + computed: { + intervalIsPreset() { + return _.contains(this.cronIntervalPresets, this.cronInterval); }, - watch: { - cronInterval() { - // updates field validation state when model changes, as - // glFieldError only updates on input. - this.$nextTick(() => { - gl.pipelineScheduleFieldErrors.updateFormValidityState(); - }); - }, + // The text input is editable when there's a custom interval, or when it's + // a preset interval and the user clicks the 'custom' radio button + isEditable() { + return !!(this.customInputEnabled || !this.intervalIsPreset); }, - created() { - if (this.intervalIsPreset) { - this.enableCustomInput = false; - } + }, + watch: { + cronInterval() { + // updates field validation state when model changes, as + // glFieldError only updates on input. + this.$nextTick(() => { + gl.pipelineScheduleFieldErrors.updateFormValidityState(); + }); }, - methods: { - toggleCustomInput(shouldEnable) { - this.customInputEnabled = shouldEnable; + }, + created() { + if (this.intervalIsPreset) { + this.enableCustomInput = false; + } + }, + methods: { + toggleCustomInput(shouldEnable) { + this.customInputEnabled = shouldEnable; - if (shouldEnable) { - // We need to change the value so other radios don't remain selected - // because the model (cronInterval) hasn't changed. The server trims it. - this.cronInterval = `${this.cronInterval} `; - } - }, + if (shouldEnable) { + // We need to change the value so other radios don't remain selected + // because the model (cronInterval) hasn't changed. The server trims it. + this.cronInterval = `${this.cronInterval} `; + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue index 77508e62cef..33fc2420e4d 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue @@ -1,31 +1,31 @@ <script> - import Vue from 'vue'; - import Cookies from 'js-cookie'; - import Translate from '../../../../../vue_shared/translate'; - import illustrationSvg from '../icons/intro_illustration.svg'; +import Vue from 'vue'; +import Cookies from 'js-cookie'; +import Translate from '../../../../../vue_shared/translate'; +import illustrationSvg from '../icons/intro_illustration.svg'; - Vue.use(Translate); +Vue.use(Translate); - const cookieKey = 'pipeline_schedules_callout_dismissed'; +const cookieKey = 'pipeline_schedules_callout_dismissed'; - export default { - name: 'PipelineSchedulesCallout', - data() { - return { - docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, - calloutDismissed: Cookies.get(cookieKey) === 'true', - }; +export default { + name: 'PipelineSchedulesCallout', + data() { + return { + docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, + calloutDismissed: Cookies.get(cookieKey) === 'true', + }; + }, + created() { + this.illustrationSvg = illustrationSvg; + }, + methods: { + dismissCallout() { + this.calloutDismissed = true; + Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); }, - created() { - this.illustrationSvg = illustrationSvg; - }, - methods: { - dismissCallout() { - this.calloutDismissed = true; - Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); - }, - }, - }; + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js index 4ef0d11dd36..0057700c1b3 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js @@ -26,8 +26,7 @@ export default class TargetBranchDropdown { } formatBranchesList() { - return this.$dropdown.data('data') - .map(val => ({ name: val })); + return this.$dropdown.data('data').map(val => ({ name: val })); } setDropdownToggle() { diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js index c3ac54733a3..4d494efef6c 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js @@ -11,7 +11,9 @@ Vue.use(Translate); function initIntervalPatternInput() { const intervalPatternMount = document.getElementById('interval-pattern-input'); - const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : ''; + const initialCronInterval = intervalPatternMount + ? intervalPatternMount.dataset.initialInterval + : ''; return new Vue({ el: intervalPatternMount, diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js index 07b6992eba1..48353f3b4ef 100644 --- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js @@ -7,26 +7,29 @@ const options = { maintainAspectRatio: false, }; -const buildChart = (chartScope) => { +const buildChart = chartScope => { const data = { labels: chartScope.labels, - datasets: [{ - fillColor: '#707070', - strokeColor: '#707070', - pointColor: '#707070', - pointStrokeColor: '#EEE', - data: chartScope.totalValues, - }, - { - fillColor: '#1aaa55', - strokeColor: '#1aaa55', - pointColor: '#1aaa55', - pointStrokeColor: '#fff', - data: chartScope.successValues, - }, + datasets: [ + { + fillColor: '#707070', + strokeColor: '#707070', + pointColor: '#707070', + pointStrokeColor: '#EEE', + data: chartScope.totalValues, + }, + { + fillColor: '#1aaa55', + strokeColor: '#1aaa55', + pointColor: '#1aaa55', + pointStrokeColor: '#fff', + data: chartScope.successValues, + }, ], }; - const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d'); + const ctx = $(`#${chartScope.scope}Chart`) + .get(0) + .getContext('2d'); new Chart(ctx).Line(data, options); }; @@ -36,14 +39,16 @@ document.addEventListener('DOMContentLoaded', () => { const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); const data = { labels: chartTimesData.labels, - datasets: [{ - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, - data: chartTimesData.values, - }], + datasets: [ + { + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: chartTimesData.values, + }, + ], }; if (window.innerWidth < 768) { @@ -51,7 +56,11 @@ document.addEventListener('DOMContentLoaded', () => { options.scaleFontSize = 8; } - new Chart($('#build_timesChart').get(0).getContext('2d')).Bar(data, options); + new Chart( + $('#build_timesChart') + .get(0) + .getContext('2d'), + ).Bar(data, options); chartsData.forEach(scope => buildChart(scope)); }); diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index a84e2790680..fc337a7609b 100644 --- a/app/assets/javascripts/pages/projects/pipelines/index/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -6,35 +6,39 @@ import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#pipelines-list-vue', - components: { - pipelinesComponent, - }, - data() { - return { - store: new PipelinesStore(), - }; - }, - created() { - this.dataset = document.querySelector(this.$options.el).dataset; - }, - render(createElement) { - return createElement('pipelines-component', { - props: { - store: this.store, - endpoint: this.dataset.endpoint, - helpPagePath: this.dataset.helpPagePath, - emptyStateSvgPath: this.dataset.emptyStateSvgPath, - errorStateSvgPath: this.dataset.errorStateSvgPath, - noPipelinesSvgPath: this.dataset.noPipelinesSvgPath, - autoDevopsPath: this.dataset.helpAutoDevopsPath, - newPipelinePath: this.dataset.newPipelinePath, - canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline), - hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi), - ciLintPath: this.dataset.ciLintPath, - resetCachePath: this.dataset.resetCachePath, +document.addEventListener( + 'DOMContentLoaded', + () => + new Vue({ + el: '#pipelines-list-vue', + components: { + pipelinesComponent, }, - }); - }, -})); + data() { + return { + store: new PipelinesStore(), + }; + }, + created() { + this.dataset = document.querySelector(this.$options.el).dataset; + }, + render(createElement) { + return createElement('pipelines-component', { + props: { + store: this.store, + endpoint: this.dataset.endpoint, + helpPagePath: this.dataset.helpPagePath, + emptyStateSvgPath: this.dataset.emptyStateSvgPath, + errorStateSvgPath: this.dataset.errorStateSvgPath, + noPipelinesSvgPath: this.dataset.noPipelinesSvgPath, + autoDevopsPath: this.dataset.helpAutoDevopsPath, + newPipelinePath: this.dataset.newPipelinePath, + canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline), + hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi), + ciLintPath: this.dataset.ciLintPath, + resetCachePath: this.dataset.resetCachePath, + }, + }); + }, + }), +); diff --git a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js index 94dfeb96e8c..ba4ae04ab3d 100644 --- a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js +++ b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js @@ -2,9 +2,12 @@ import Pipelines from '~/pipelines'; export default () => { const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; - const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; + const pipelineStatusUrl = `${document + .querySelector('.js-pipeline-tab-link a') + .getAttribute('href')}/status.json`; - new Pipelines({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new Pipelines({ initTabs: true, pipelineStatusUrl, tabsOptions: { diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue index 06101290f6c..dced839c883 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -1,73 +1,71 @@ <script> - import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; +import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; - export default { - components: { - projectFeatureToggle, - }, +export default { + components: { + projectFeatureToggle, + }, - model: { - prop: 'value', - event: 'change', - }, + model: { + prop: 'value', + event: 'change', + }, - props: { - name: { - type: String, - required: false, - default: '', - }, - options: { - type: Array, - required: false, - default: () => [], - }, - value: { - type: Number, - required: false, - default: 0, - }, - disabledInput: { - type: Boolean, - required: false, - default: false, - }, + props: { + name: { + type: String, + required: false, + default: '', + }, + options: { + type: Array, + required: false, + default: () => [], + }, + value: { + type: Number, + required: false, + default: 0, }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + }, - computed: { - featureEnabled() { - return this.value !== 0; - }, + computed: { + featureEnabled() { + return this.value !== 0; + }, - displayOptions() { - if (this.featureEnabled) { - return this.options; - } - return [ - [0, 'Enable feature to choose access level'], - ]; - }, + displayOptions() { + if (this.featureEnabled) { + return this.options; + } + return [[0, 'Enable feature to choose access level']]; + }, - displaySelectInput() { - return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; - }, + displaySelectInput() { + return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; }, + }, - methods: { - toggleFeature(featureEnabled) { - if (featureEnabled === false || this.options.length < 1) { - this.$emit('change', 0); - } else { - const [firstOptionValue] = this.options[this.options.length - 1]; - this.$emit('change', firstOptionValue); - } - }, + methods: { + toggleFeature(featureEnabled) { + if (featureEnabled === false || this.options.length < 1) { + this.$emit('change', 0); + } else { + const [firstOptionValue] = this.options[this.options.length - 1]; + this.$emit('change', firstOptionValue); + } + }, - selectOption(e) { - this.$emit('change', Number(e.target.value)); - }, + selectOption(e) { + this.$emit('change', Number(e.target.value)); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue index 83437363af5..898d605463f 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -1,23 +1,23 @@ <script> - export default { - props: { - label: { - type: String, - required: false, - default: null, - }, - helpPath: { - type: String, - required: false, - default: null, - }, - helpText: { - type: String, - required: false, - default: null, - }, +export default { + props: { + label: { + type: String, + required: false, + default: null, }, - }; + helpPath: { + type: String, + required: false, + default: null, + }, + helpText: { + type: String, + required: false, + default: null, + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js index ce47562f259..bc5c29d12b5 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/constants.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js @@ -5,7 +5,9 @@ export const visibilityOptions = { }; export const visibilityLevelDescriptions = { - [visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.', + [visibilityOptions.PRIVATE]: + 'The project is accessible only by members of the project. Access must be granted explicitly to each user.', [visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.', - [visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.', + [visibilityOptions.PUBLIC]: + 'The project can be accessed by anyone, regardless of authentication.', }; diff --git a/app/assets/javascripts/pages/projects/shared/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js index 447877752fe..1e69ecb481d 100644 --- a/app/assets/javascripts/pages/projects/shared/project_avatar.js +++ b/app/assets/javascripts/pages/projects/shared/project_avatar.js @@ -8,8 +8,9 @@ export default function projectAvatar() { $('.js-project-avatar-input').bind('change', function onClickAvatarInput() { const form = $(this).closest('form'); - // eslint-disable-next-line no-useless-escape - const filename = $(this).val().replace(/^.*[\\\/]/, ''); + const filename = $(this) + .val() + .replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape return form.find('.js-avatar-filename').text(filename); }); } diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index c2629090f01..f5fd84d69ac 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -21,7 +21,8 @@ document.addEventListener('DOMContentLoaded', () => { const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset; - new Vue({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new Vue({ el: deleteWikiModalWrapperEl, data: { deleteWikiUrl: '', diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index e3e0ab91993..0c896c8599e 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -22,7 +22,7 @@ export default class Search { fields: ['full_name'], }, data(term, callback) { - return Api.groups(term, {}, (data) => { + return Api.groups(term, {}, data => { data.unshift({ full_name: 'Any', }); @@ -37,7 +37,7 @@ export default class Search { return obj.full_name; }, toggleLabel(obj) { - return `${($groupDropdown.data('defaultLabel'))} ${obj.full_name}`; + return `${$groupDropdown.data('defaultLabel')} ${obj.full_name}`; }, clicked: () => Search.submitSearch(), }); @@ -52,7 +52,7 @@ export default class Search { }, data: (term, callback) => { this.getProjectsData(term) - .then((data) => { + .then(data => { data.unshift({ name_with_namespace: 'Any', }); @@ -70,7 +70,7 @@ export default class Search { return obj.name_with_namespace; }, toggleLabel(obj) { - return `${($projectDropdown.data('defaultLabel'))} ${obj.name_with_namespace}`; + return `${$projectDropdown.data('defaultLabel')} ${obj.name_with_namespace}`; }, clicked: () => Search.submitSearch(), }); @@ -99,17 +99,24 @@ export default class Search { } clearSearchField() { - return $(this.searchInput).val('').trigger('keyup').focus(); + return $(this.searchInput) + .val('') + .trigger('keyup') + .focus(); } getProjectsData(term) { - return new Promise((resolve) => { + return new Promise(resolve => { if (this.groupId) { Api.groupProjects(this.groupId, term, {}, resolve); } else { - Api.projects(term, { - order_by: 'id', - }, resolve); + Api.projects( + term, + { + order_by: 'id', + }, + resolve, + ); } }); } diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js index 1e7c29aefaa..2b8f1e8b0ef 100644 --- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js +++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js @@ -20,7 +20,7 @@ export default class SigninTabsMemoizer { bootstrap() { const tabs = document.querySelectorAll(this.tabSelector); if (tabs.length > 0) { - tabs[0].addEventListener('click', (e) => { + tabs[0].addEventListener('click', e => { if (e.target && e.target.nodeName === 'A') { const anchorName = e.target.getAttribute('href'); this.saveData(anchorName); diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index d621f988d86..7a41805bada 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -22,10 +22,10 @@ export default class UsernameValidator { available: false, valid: false, pending: false, - empty: true + empty: true, }; - const debounceTimeout = _.debounce((username) => { + const debounceTimeout = _.debounce(username => { this.validateUsername(username); }, debounceTimeoutDuration); @@ -81,7 +81,8 @@ export default class UsernameValidator { this.state.pending = true; this.state.available = false; this.renderState(); - axios.get(`${gon.relative_url_root}/users/${username}/exists`) + axios + .get(`${gon.relative_url_root}/users/${username}/exists`) .then(({ data }) => this.setAvailabilityState(data.exists)) .catch(() => flash(__('An error occurred while validating username'))); } @@ -100,8 +101,7 @@ export default class UsernameValidator { clearFieldValidationState() { this.inputElement.siblings('p').hide(); - this.inputElement.removeClass(invalidInputClass) - .removeClass(successInputClass); + this.inputElement.removeClass(invalidInputClass).removeClass(successInputClass); } setUnavailableState() { diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js index 6b1626b0161..a191df00dfa 100644 --- a/app/assets/javascripts/pages/users/index.js +++ b/app/assets/javascripts/pages/users/index.js @@ -13,10 +13,12 @@ function initUserProfile(action) { new UserTabs({ parentEl: '.user-profile', action }); // hide project limit message - $('.hide-project-limit-message').on('click', (e) => { + $('.hide-project-limit-message').on('click', e => { e.preventDefault(); Cookies.set('hide_project_limit_message', 'false'); - $(this).parents('.project-limit-message').remove(); + $(this) + .parents('.project-limit-message') + .remove(); }); } diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 1de9945baad..04bcb16f036 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -170,7 +170,7 @@ export default class UserTabs { this.loadActivityCalendar('activity'); // eslint-disable-next-line no-new - new Activities(); + new Activities('#activity'); this.loaded.activity = true; } diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue index 760ea8fe1e6..7a558558c4d 100644 --- a/app/assets/javascripts/performance_bar/components/simple_metric.vue +++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue @@ -1,29 +1,29 @@ <script> - export default { - props: { - currentRequest: { - type: Object, - required: true, - }, - metric: { - type: String, - required: true, - }, +export default { + props: { + currentRequest: { + type: Object, + required: true, }, - computed: { - duration() { - return ( - this.currentRequest.details[this.metric] && - this.currentRequest.details[this.metric].duration - ); - }, - calls() { - return ( - this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls - ); - }, + metric: { + type: String, + required: true, }, - }; + }, + computed: { + duration() { + return ( + this.currentRequest.details[this.metric] && + this.currentRequest.details[this.metric].duration + ); + }, + calls() { + return ( + this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls + ); + }, + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index 6e5ef0ac0b2..29bfb7ee5df 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -9,8 +9,7 @@ export default ({ container }) => performanceBarApp: () => import('./components/performance_bar_app.vue'), }, data() { - const performanceBarData = document.querySelector(this.$options.el) - .dataset; + const performanceBarData = document.querySelector(this.$options.el).dataset; const store = new PerformanceBarStore(); return { diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js index 60d9ba62570..3a496fa2ed8 100644 --- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -11,8 +11,10 @@ export default class PerformanceBarService { static registerInterceptor(peekUrl, callback) { const interceptor = response => { - const [fireCallback, requestId, requestUrl] = - PerformanceBarService.callbackParams(response, peekUrl); + const [fireCallback, requestId, requestUrl] = PerformanceBarService.callbackParams( + response, + peekUrl, + ); if (fireCallback) { callback(requestId, requestUrl); @@ -30,10 +32,7 @@ export default class PerformanceBarService { static removeInterceptor(interceptor) { axios.interceptors.response.eject(interceptor); - Vue.http.interceptors = _.without( - Vue.http.interceptors, - vueResourceInterceptor, - ); + Vue.http.interceptors = _.without(Vue.http.interceptors, vueResourceInterceptor); } static callbackParams(response, peekUrl) { diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js index c6b2f55243c..031e774d533 100644 --- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js +++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js @@ -32,8 +32,6 @@ export default class PerformanceBarStore { } canTrackRequest(requestUrl) { - return ( - this.requests.filter(request => request.url === requestUrl).length < 2 - ); + return this.requests.filter(request => request.url === requestUrl).length < 2; } } diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 974629fa2af..99b57f4c9d5 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,78 +1,78 @@ <script> - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { __, s__, sprintf } from '~/locale'; - import csrf from '~/lib/utils/csrf'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { __, s__, sprintf } from '~/locale'; +import csrf from '~/lib/utils/csrf'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + actionUrl: { + type: String, + required: true, }, - props: { - actionUrl: { - type: String, - required: true, - }, - confirmWithPassword: { - type: Boolean, - required: true, - }, - username: { - type: String, - required: true, - }, + confirmWithPassword: { + type: Boolean, + required: true, }, - data() { - return { - enteredPassword: '', - enteredUsername: '', - }; + username: { + type: String, + required: true, }, - computed: { - csrfToken() { - return csrf.token; - }, - inputLabel() { - let confirmationValue; - if (this.confirmWithPassword) { - confirmationValue = __('password'); - } else { - confirmationValue = __('username'); - } + }, + data() { + return { + enteredPassword: '', + enteredUsername: '', + }; + }, + computed: { + csrfToken() { + return csrf.token; + }, + inputLabel() { + let confirmationValue; + if (this.confirmWithPassword) { + confirmationValue = __('password'); + } else { + confirmationValue = __('username'); + } - confirmationValue = `<code>${confirmationValue}</code>`; + confirmationValue = `<code>${confirmationValue}</code>`; - return sprintf( - s__('Profiles|Type your %{confirmationValue} to confirm:'), - { confirmationValue }, - false, - ); - }, - text() { - return sprintf( - s__(`Profiles| + return sprintf( + s__('Profiles|Type your %{confirmationValue} to confirm:'), + { confirmationValue }, + false, + ); + }, + text() { + return sprintf( + s__(`Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), - { - yourAccount: `<strong>${s__('Profiles|your account')}</strong>`, - deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`, - }, - false, - ); - }, + { + yourAccount: `<strong>${s__('Profiles|your account')}</strong>`, + deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`, + }, + false, + ); }, - methods: { - canSubmit() { - if (this.confirmWithPassword) { - return this.enteredPassword !== ''; - } + }, + methods: { + canSubmit() { + if (this.confirmWithPassword) { + return this.enteredPassword !== ''; + } - return this.enteredUsername === this.username; - }, - onSubmit() { - this.$refs.form.submit(); - }, + return this.enteredUsername === this.username; + }, + onSubmit() { + this.$refs.form.submit(); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index af134881f31..befe91c332f 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -4,20 +4,35 @@ import $ from 'jquery'; import 'cropper'; import _ from 'underscore'; -((global) => { +(global => { // Matches everything but the file name const FILENAMEREGEX = /^.*[\\\/]/; class GitLabCrop { - constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg, - exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) { + constructor( + input, + { + filename, + previewImage, + modalCrop, + pickImageEl, + uploadImageBtn, + modalCropImg, + exportWidth = 200, + exportHeight = 200, + cropBoxWidth = 200, + cropBoxHeight = 200, + } = {}, + ) { this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this); this.onModalHide = this.onModalHide.bind(this); this.onModalShow = this.onModalShow.bind(this); this.onPickImageClick = this.onPickImageClick.bind(this); this.fileInput = $(input); this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; - this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `${this.fileInput.attr('id')}-trigger`); + this.fileInput + .attr('name', `${this.fileInput.attr('name')}-trigger`) + .attr('id', `${this.fileInput.attr('id')}-trigger`); this.exportWidth = exportWidth; this.exportHeight = exportHeight; this.cropBoxWidth = cropBoxWidth; @@ -59,7 +74,7 @@ import _ from 'underscore'; btn = this; return _this.onActionBtnClick(btn); }); - return this.croppedImageBlob = null; + return (this.croppedImageBlob = null); } onPickImageClick() { @@ -94,9 +109,9 @@ import _ from 'underscore'; width: cropBoxWidth, height: cropBoxHeight, left: (container.width - cropBoxWidth) / 2, - top: (container.height - cropBoxHeight) / 2 + top: (container.height - cropBoxHeight) / 2, }); - } + }, }); } @@ -116,7 +131,7 @@ import _ from 'underscore'; var data, result; data = $(btn).data(); if (this.modalCropImg.data('cropper') && data.method) { - return result = this.modalCropImg.cropper(data.method, data.option); + return (result = this.modalCropImg.cropper(data.method, data.option)); } } @@ -127,7 +142,7 @@ import _ from 'underscore'; readFile(input) { var _this, reader; _this = this; - reader = new FileReader; + reader = new FileReader(); reader.onload = () => { _this.modalCropImg.attr('src', reader.result); return _this.modalCrop.modal('show'); @@ -145,7 +160,7 @@ import _ from 'underscore'; array.push(binary.charCodeAt(i)); } return new Blob([new Uint8Array(array)], { - type: 'image/png' + type: 'image/png', }); } @@ -157,11 +172,13 @@ import _ from 'underscore'; } setBlob() { - this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { - width: 200, - height: 200 - }).toDataURL('image/png'); - return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); + this.dataURL = this.modalCropImg + .cropper('getCroppedCanvas', { + width: 200, + height: 200, + }) + .toDataURL('image/png'); + return (this.croppedImageBlob = this.dataURLtoBlob(this.dataURL)); } getBlob() { diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index e49c67ffb5c..8704a655b28 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -26,11 +26,7 @@ export default class Profile { } bindEvents() { - $('.js-preferences-form').on( - 'change.preference', - 'input[type=radio]', - this.submitForm, - ); + $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); $('#user_notification_email').on('change', this.submitForm); $('#user_notified_of_own_activity').on('change', this.submitForm); this.form.on('submit', this.onSubmitForm); diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index b601b19e7be..48343c8ba0a 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -46,8 +46,12 @@ export default class ProtectedBranchCreate { onSelect() { // Enable submit button const $branchInput = this.$form.find('input[name="protected_branch[name]"]'); - const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); - const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); + const $allowedToMergeInput = this.$form.find( + 'input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]', + ); + const $allowedToPushInput = this.$form.find( + 'input[name="protected_branch[push_access_levels_attributes][0][access_level]"]', + ); const completedForm = !( $branchInput.val() && $allowedToMergeInput.length && diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 54560d08ad7..5bc08f60d16 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -29,8 +29,12 @@ export default class ProtectedBranchEdit { } onSelect() { - const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); - const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + const $allowedToMergeInput = this.$wrap.find( + `input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`, + ); + const $allowedToPushInput = this.$wrap.find( + `input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`, + ); // Do not update if one dropdown has not selected any option if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; @@ -38,25 +42,36 @@ export default class ProtectedBranchEdit { this.$allowedToMergeDropdown.disable(); this.$allowedToPushDropdown.disable(); - axios.patch(this.$wrap.data('url'), { - protected_branch: { - merge_access_levels_attributes: [{ - id: this.$allowedToMergeDropdown.data('accessLevelId'), - access_level: $allowedToMergeInput.val(), - }], - push_access_levels_attributes: [{ - id: this.$allowedToPushDropdown.data('accessLevelId'), - access_level: $allowedToPushInput.val(), - }], - }, - }).then(() => { - this.$allowedToMergeDropdown.enable(); - this.$allowedToPushDropdown.enable(); - }).catch(() => { - this.$allowedToMergeDropdown.enable(); - this.$allowedToPushDropdown.enable(); - - flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list')); - }); + axios + .patch(this.$wrap.data('url'), { + protected_branch: { + merge_access_levels_attributes: [ + { + id: this.$allowedToMergeDropdown.data('accessLevelId'), + access_level: $allowedToMergeInput.val(), + }, + ], + push_access_levels_attributes: [ + { + id: this.$allowedToPushDropdown.data('accessLevelId'), + access_level: $allowedToPushInput.val(), + }, + ], + }, + }) + .then(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + }) + .catch(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + + flash( + 'Failed to update branch!', + 'alert', + document.querySelector('.js-protected-branches-list'), + ); + }); } } diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 2f8116df0d2..fddf2674cbb 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -40,7 +40,9 @@ export default class ProtectedTagCreate { const $tagInput = this.$form.find('input[name="protected_tag[name]"]'); const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes'); - this.$form.find('input[type="submit"]').prop('disabled', !($tagInput.val() && $allowedToCreateInput.length)); + this.$form + .find('input[type="submit"]') + .prop('disabled', !($tagInput.val() && $allowedToCreateInput.length)); } static getProtectedTags(term, callback) { diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index 8687b2a4044..c52497e62f2 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -21,26 +21,33 @@ export default class ProtectedTagEdit { } onSelect() { - const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`); + const $allowedToCreateInput = this.$wrap.find( + `input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`, + ); // Do not update if one dropdown has not selected any option if (!$allowedToCreateInput.length) return; this.$allowedToCreateDropdownButton.disable(); - axios.patch(this.$wrap.data('url'), { - protected_tag: { - create_access_levels_attributes: [{ - id: this.$allowedToCreateDropdownButton.data('accessLevelId'), - access_level: $allowedToCreateInput.val(), - }], - }, - }).then(() => { - this.$allowedToCreateDropdownButton.enable(); - }).catch(() => { - this.$allowedToCreateDropdownButton.enable(); - - flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); - }); + axios + .patch(this.$wrap.data('url'), { + protected_tag: { + create_access_levels_attributes: [ + { + id: this.$allowedToCreateDropdownButton.data('accessLevelId'), + access_level: $allowedToCreateInput.val(), + }, + ], + }, + }) + .then(() => { + this.$allowedToCreateDropdownButton.enable(); + }) + .catch(() => { + this.$allowedToCreateDropdownButton.enable(); + + flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); + }); } } diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 7e2287ac4db..9dd1c87a87d 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -1,42 +1,35 @@ <script> - import { mapGetters, mapActions } from 'vuex'; - import Flash from '../../flash'; - import store from '../stores'; - import collapsibleContainer from './collapsible_container.vue'; - import { errorMessages, errorMessagesTypes } from '../constants'; +import { mapGetters, mapActions } from 'vuex'; +import Flash from '../../flash'; +import store from '../stores'; +import collapsibleContainer from './collapsible_container.vue'; +import { errorMessages, errorMessagesTypes } from '../constants'; - export default { - name: 'RegistryListApp', - components: { - collapsibleContainer, +export default { + name: 'RegistryListApp', + components: { + collapsibleContainer, + }, + props: { + endpoint: { + type: String, + required: true, }, - props: { - endpoint: { - type: String, - required: true, - }, - }, - store, - computed: { - ...mapGetters([ - 'isLoading', - 'repos', - ]), - }, - created() { - this.setMainEndpoint(this.endpoint); - }, - mounted() { - this.fetchRepos() - .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); - }, - methods: { - ...mapActions([ - 'setMainEndpoint', - 'fetchRepos', - ]), - }, - }; + }, + store, + computed: { + ...mapGetters(['isLoading', 'repos']), + }, + created() { + this.setMainEndpoint(this.endpoint); + }, + mounted() { + this.fetchRepos().catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); + }, + methods: { + ...mapActions(['setMainEndpoint', 'fetchRepos']), + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index d9bf41924d1..501b2625ae5 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,62 +1,59 @@ <script> - import { mapActions } from 'vuex'; - import Flash from '../../flash'; - import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import tableRegistry from './table_registry.vue'; - import { errorMessages, errorMessagesTypes } from '../constants'; - import { __ } from '../../locale'; +import { mapActions } from 'vuex'; +import Flash from '../../flash'; +import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import tableRegistry from './table_registry.vue'; +import { errorMessages, errorMessagesTypes } from '../constants'; +import { __ } from '../../locale'; - export default { - name: 'CollapsibeContainerRegisty', - components: { - clipboardButton, - tableRegistry, +export default { + name: 'CollapsibeContainerRegisty', + components: { + clipboardButton, + tableRegistry, + }, + directives: { + tooltip, + }, + props: { + repo: { + type: Object, + required: true, }, - directives: { - tooltip, - }, - props: { - repo: { - type: Object, - required: true, - }, - }, - data() { - return { - isOpen: false, - }; - }, - methods: { - ...mapActions([ - 'fetchRepos', - 'fetchList', - 'deleteRepo', - ]), + }, + data() { + return { + isOpen: false, + }; + }, + methods: { + ...mapActions(['fetchRepos', 'fetchList', 'deleteRepo']), - toggleRepo() { - this.isOpen = !this.isOpen; + toggleRepo() { + this.isOpen = !this.isOpen; - if (this.isOpen) { - this.fetchList({ repo: this.repo }) - .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); - } - }, + if (this.isOpen) { + this.fetchList({ repo: this.repo }).catch(() => + this.showError(errorMessagesTypes.FETCH_REGISTRY), + ); + } + }, - handleDeleteRepository() { - this.deleteRepo(this.repo) - .then(() => { - Flash(__('This container registry has been scheduled for deletion.'), 'notice'); - this.fetchRepos(); - }) - .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); - }, + handleDeleteRepository() { + this.deleteRepo(this.repo) + .then(() => { + Flash(__('This container registry has been scheduled for deletion.'), 'notice'); + this.fetchRepos(); + }) + .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); + }, - showError(message) { - Flash(errorMessages[message]); - }, + showError(message) { + Flash(errorMessages[message]); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index fafb35bd69a..bb6c977fc63 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -1,66 +1,62 @@ <script> - import { mapActions } from 'vuex'; - import { n__ } from '../../locale'; - import Flash from '../../flash'; - import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; - import tablePagination from '../../vue_shared/components/table_pagination.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import timeagoMixin from '../../vue_shared/mixins/timeago'; - import { errorMessages, errorMessagesTypes } from '../constants'; - import { numberToHumanSize } from '../../lib/utils/number_utils'; +import { mapActions } from 'vuex'; +import { n__ } from '../../locale'; +import Flash from '../../flash'; +import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import tablePagination from '../../vue_shared/components/table_pagination.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import timeagoMixin from '../../vue_shared/mixins/timeago'; +import { errorMessages, errorMessagesTypes } from '../constants'; +import { numberToHumanSize } from '../../lib/utils/number_utils'; - export default { - components: { - clipboardButton, - tablePagination, +export default { + components: { + clipboardButton, + tablePagination, + }, + directives: { + tooltip, + }, + mixins: [timeagoMixin], + props: { + repo: { + type: Object, + required: true, }, - directives: { - tooltip, + }, + computed: { + shouldRenderPagination() { + return this.repo.pagination.total > this.repo.pagination.perPage; }, - mixins: [ - timeagoMixin, - ], - props: { - repo: { - type: Object, - required: true, - }, - }, - computed: { - shouldRenderPagination() { - return this.repo.pagination.total > this.repo.pagination.perPage; - }, - }, - methods: { - ...mapActions([ - 'fetchList', - 'deleteRegistry', - ]), + }, + methods: { + ...mapActions(['fetchList', 'deleteRegistry']), - layers(item) { - return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; - }, + layers(item) { + return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; + }, - formatSize(size) { - return numberToHumanSize(size); - }, + formatSize(size) { + return numberToHumanSize(size); + }, - handleDeleteRegistry(registry) { - this.deleteRegistry(registry) - .then(() => this.fetchList({ repo: this.repo })) - .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); - }, + handleDeleteRegistry(registry) { + this.deleteRegistry(registry) + .then(() => this.fetchList({ repo: this.repo })) + .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); + }, - onPageChange(pageNumber) { - this.fetchList({ repo: this.repo, page: pageNumber }) - .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); - }, + onPageChange(pageNumber) { + this.fetchList({ repo: this.repo, page: pageNumber }).catch(() => + this.showError(errorMessagesTypes.FETCH_REGISTRY), + ); + }, - showError(message) { - Flash(errorMessages[message]); - }, + showError(message) { + Flash(errorMessages[message]); }, - }; + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js index e15cd94a915..025afefe7f0 100644 --- a/app/assets/javascripts/registry/index.js +++ b/app/assets/javascripts/registry/index.js @@ -4,22 +4,23 @@ import Translate from '../vue_shared/translate'; Vue.use(Translate); -export default () => new Vue({ - el: '#js-vue-registry-images', - components: { - registryApp, - }, - data() { - const { dataset } = document.querySelector(this.$options.el); - return { - endpoint: dataset.endpoint, - }; - }, - render(createElement) { - return createElement('registry-app', { - props: { - endpoint: this.endpoint, - }, - }); - }, -}); +export default () => + new Vue({ + el: '#js-vue-registry-images', + components: { + registryApp, + }, + data() { + const { dataset } = document.querySelector(this.$options.el); + return { + endpoint: dataset.endpoint, + }; + }, + render(createElement) { + return createElement('registry-app', { + props: { + endpoint: this.endpoint, + }, + }); + }, + }); diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js index 208c3c39866..69c051cd2d6 100644 --- a/app/assets/javascripts/registry/stores/mutations.js +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -2,7 +2,6 @@ import * as types from './mutation_types'; import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; export default { - [types.SET_MAIN_ENDPOINT](state, endpoint) { Object.assign(state, { endpoint }); }, diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index b373d83a44b..bd204503cc7 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -1,79 +1,72 @@ <script> - import { mapActions, mapGetters, mapState } from 'vuex'; - import { s__ } from '~/locale'; - import { componentNames } from './issue_body'; - import ReportSection from './report_section.vue'; - import SummaryRow from './summary_row.vue'; - import IssuesList from './issues_list.vue'; - import Modal from './modal.vue'; - import createStore from '../store'; - import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import { s__ } from '~/locale'; +import { componentNames } from './issue_body'; +import ReportSection from './report_section.vue'; +import SummaryRow from './summary_row.vue'; +import IssuesList from './issues_list.vue'; +import Modal from './modal.vue'; +import createStore from '../store'; +import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; - export default { - name: 'GroupedTestReportsApp', - store: createStore(), - components: { - ReportSection, - SummaryRow, - IssuesList, - Modal, +export default { + name: 'GroupedTestReportsApp', + store: createStore(), + components: { + ReportSection, + SummaryRow, + IssuesList, + Modal, + }, + props: { + endpoint: { + type: String, + required: true, }, - props: { - endpoint: { - type: String, - required: true, - }, - }, - componentNames, - computed: { - ...mapState([ - 'reports', - 'isLoading', - 'hasError', - 'summary', - ]), - ...mapState({ - modalTitle: state => state.modal.title || '', - modalData: state => state.modal.data || {}, - }), - ...mapGetters([ - 'summaryStatus', - ]), - groupedSummaryText() { - if (this.isLoading) { - return s__('Reports|Test summary results are being parsed'); - } + }, + componentNames, + computed: { + ...mapState(['reports', 'isLoading', 'hasError', 'summary']), + ...mapState({ + modalTitle: state => state.modal.title || '', + modalData: state => state.modal.data || {}, + }), + ...mapGetters(['summaryStatus']), + groupedSummaryText() { + if (this.isLoading) { + return s__('Reports|Test summary results are being parsed'); + } - if (this.hasError) { - return s__('Reports|Test summary failed loading results'); - } + if (this.hasError) { + return s__('Reports|Test summary failed loading results'); + } - return summaryTextBuilder(s__('Reports|Test summary'), this.summary); - }, + return summaryTextBuilder(s__('Reports|Test summary'), this.summary); }, - created() { - this.setEndpoint(this.endpoint); + }, + created() { + this.setEndpoint(this.endpoint); - this.fetchReports(); + this.fetchReports(); + }, + methods: { + ...mapActions(['setEndpoint', 'fetchReports']), + reportText(report) { + const summary = report.summary || {}; + return reportTextBuilder(report.name, summary); + }, + getReportIcon(report) { + return statusIcon(report.status); }, - methods: { - ...mapActions(['setEndpoint', 'fetchReports']), - reportText(report) { - const summary = report.summary || {}; - return reportTextBuilder(report.name, summary); - }, - getReportIcon(report) { - return statusIcon(report.status); - }, - shouldRenderIssuesList(report) { - return ( - report.existing_failures.length > 0 || - report.new_failures.length > 0 || - report.resolved_failures.length > 0 - ); - }, + shouldRenderIssuesList(report) { + return ( + report.existing_failures.length > 0 || + report.new_failures.length > 0 || + report.resolved_failures.length > 0 + ); }, - }; + }, +}; </script> <template> <report-section diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index 85811698a37..6e143c4f98c 100644 --- a/app/assets/javascripts/reports/components/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -1,10 +1,6 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; -import { - STATUS_FAILED, - STATUS_NEUTRAL, - STATUS_SUCCESS, -} from '../constants'; +import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '../constants'; export default { name: 'IssueStatusIcon', diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index df42201b5de..3b425ee2fed 100644 --- a/app/assets/javascripts/reports/components/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -1,10 +1,6 @@ <script> import IssuesBlock from '~/reports/components/report_issues.vue'; -import { - STATUS_SUCCESS, - STATUS_FAILED, - STATUS_NEUTRAL, -} from '~/reports/constants'; +import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants'; /** * Renders block of issues diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue index acc5c6d85e2..5f9e4072b2d 100644 --- a/app/assets/javascripts/reports/components/modal.vue +++ b/app/assets/javascripts/reports/components/modal.vue @@ -1,27 +1,27 @@ <script> - import Modal from '~/vue_shared/components/gl_modal.vue'; - import LoadingButton from '~/vue_shared/components/loading_button.vue'; - import CodeBlock from '~/vue_shared/components/code_block.vue'; - import { fieldTypes } from '../constants'; +import Modal from '~/vue_shared/components/gl_modal.vue'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import CodeBlock from '~/vue_shared/components/code_block.vue'; +import { fieldTypes } from '../constants'; - export default { - components: { - Modal, - LoadingButton, - CodeBlock, +export default { + components: { + Modal, + LoadingButton, + CodeBlock, + }, + props: { + title: { + type: String, + required: true, }, - props: { - title: { - type: String, - required: true, - }, - modalData: { - type: Object, - required: true, - }, + modalData: { + type: Object, + required: true, }, - fieldTypes, - }; + }, + fieldTypes, +}; </script> <template> <modal diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue index cd443a49b52..1a87822fcc3 100644 --- a/app/assets/javascripts/reports/components/test_issue_body.vue +++ b/app/assets/javascripts/reports/components/test_issue_body.vue @@ -1,28 +1,28 @@ <script> - import { mapActions } from 'vuex'; +import { mapActions } from 'vuex'; - export default { - name: 'TestIssueBody', - props: { - issue: { - type: Object, - required: true, - }, - // failed || success - status: { - type: String, - required: true, - }, - isNew: { - type: Boolean, - required: false, - default: false, - }, +export default { + name: 'TestIssueBody', + props: { + issue: { + type: Object, + required: true, }, - methods: { - ...mapActions(['openModal']), + // failed || success + status: { + type: String, + required: true, }, - }; + isNew: { + type: Boolean, + required: false, + default: false, + }, + }, + methods: { + ...mapActions(['openModal']), + }, +}; </script> <template> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js index acabcc1d193..db8ab5ccb80 100644 --- a/app/assets/javascripts/reports/store/actions.js +++ b/app/assets/javascripts/reports/store/actions.js @@ -43,9 +43,11 @@ export const fetchReports = ({ state, dispatch }) => { }, data: state.endpoint, method: 'getReports', - successCallback: ({ data, status }) => dispatch('receiveReportsSuccess', { - data, status, - }), + successCallback: ({ data, status }) => + dispatch('receiveReportsSuccess', { + data, + status, + }), errorCallback: () => dispatch('receiveReportsError'), }); diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js index 9d8f7dc3b74..467c692b438 100644 --- a/app/assets/javascripts/reports/store/index.js +++ b/app/assets/javascripts/reports/store/index.js @@ -7,9 +7,10 @@ import state from './state'; Vue.use(Vuex); -export default () => new Vuex.Store({ - actions, - mutations, - getters, - state: state(), -}); +export default () => + new Vuex.Store({ + actions, + mutations, + getters, + state: state(), + }); diff --git a/app/assets/javascripts/reports/store/mutation_types.js b/app/assets/javascripts/reports/store/mutation_types.js index 82bda31df5d..599d4862dfe 100644 --- a/app/assets/javascripts/reports/store/mutation_types.js +++ b/app/assets/javascripts/reports/store/mutation_types.js @@ -4,4 +4,3 @@ export const REQUEST_REPORTS = 'REQUEST_REPORTS'; export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS'; export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR'; export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA'; - diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js index b88bff97075..2a37f5b74fa 100644 --- a/app/assets/javascripts/reports/store/mutations.js +++ b/app/assets/javascripts/reports/store/mutations.js @@ -19,7 +19,6 @@ export default { state.status = response.status; state.reports = response.suites; - }, [types.RECEIVE_REPORTS_ERROR](state) { state.isLoading = false; @@ -36,7 +35,7 @@ export default { [types.SET_ISSUE_MODAL_DATA](state, payload) { state.modal.title = payload.issue.name; - Object.keys(payload.issue).forEach((key) => { + Object.keys(payload.issue).forEach(key => { if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) { state.modal.data[key] = { ...state.modal.data[key], diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js index 4cab2e27a16..5484900276c 100644 --- a/app/assets/javascripts/reports/store/state.js +++ b/app/assets/javascripts/reports/store/state.js @@ -57,5 +57,4 @@ export default () => ({ }, }, }, - }); diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 123c92aff64..cfa7029b388 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -69,7 +69,8 @@ export default { this.loading = false; } - this.mediator.saveAssignees(this.field) + this.mediator + .saveAssignees(this.field) .then(setLoadingFalse.bind(this)) .catch(() => { setLoadingFalse(); diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 2b8d6207dea..439e8a69df0 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -56,11 +56,7 @@ export default { .update('issue', { confidential }) .then(() => window.location.reload()) .catch(() => { - Flash( - __( - 'Something went wrong trying to change the confidentiality of this issue', - ), - ); + Flash(__('Something went wrong trying to change the confidentiality of this issue')); }); }, }, diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index cdff4105335..48a2b9194aa 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -34,11 +34,7 @@ export default { required: true, type: Object, validator(mediatorObject) { - return ( - mediatorObject.service && - mediatorObject.service.update && - mediatorObject.store - ); + return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; }, }, }, @@ -67,8 +63,7 @@ export default { methods: { toggleForm() { - this.mediator.store.isLockDialogOpen = !this.mediator.store - .isLockDialogOpen; + this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; }, updateLockedAttribute(locked) { @@ -79,9 +74,14 @@ export default { .then(() => window.location.reload()) .catch(() => Flash( - sprintf(__('Something went wrong trying to change the locked state of this %{issuableDisplayName}'), { - issuableDisplayName: this.issuableDisplayName, - }), + sprintf( + __( + 'Something went wrong trying to change the locked state of this %{issuableDisplayName}', + ), + { + issuableDisplayName: this.issuableDisplayName, + }, + ), ), ); }, diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 286a16f7bbf..11b5dbe5f8e 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -1,78 +1,78 @@ <script> - import { __, n__, sprintf } from '~/locale'; - import tooltip from '~/vue_shared/directives/tooltip'; - import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import { __, n__, sprintf } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + userAvatarImage, + }, + props: { + loading: { + type: Boolean, + required: false, + default: false, }, - components: { - userAvatarImage, + participants: { + type: Array, + required: false, + default: () => [], }, - props: { - loading: { - type: Boolean, - required: false, - default: false, - }, - participants: { - type: Array, - required: false, - default: () => [], - }, - numberOfLessParticipants: { - type: Number, - required: false, - default: 7, - }, + numberOfLessParticipants: { + type: Number, + required: false, + default: 7, }, - data() { - return { - isShowingMoreParticipants: false, - }; + }, + data() { + return { + isShowingMoreParticipants: false, + }; + }, + computed: { + lessParticipants() { + return this.participants.slice(0, this.numberOfLessParticipants); }, - computed: { - lessParticipants() { - return this.participants.slice(0, this.numberOfLessParticipants); - }, - visibleParticipants() { - return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; - }, - hasMoreParticipants() { - return this.participants.length > this.numberOfLessParticipants; - }, - toggleLabel() { - let label = ''; - if (this.isShowingMoreParticipants) { - label = __('- show less'); - } else { - label = sprintf(__('+ %{moreCount} more'), { - moreCount: this.participants.length - this.numberOfLessParticipants, - }); - } + visibleParticipants() { + return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; + }, + hasMoreParticipants() { + return this.participants.length > this.numberOfLessParticipants; + }, + toggleLabel() { + let label = ''; + if (this.isShowingMoreParticipants) { + label = __('- show less'); + } else { + label = sprintf(__('+ %{moreCount} more'), { + moreCount: this.participants.length - this.numberOfLessParticipants, + }); + } - return label; - }, - participantLabel() { - return sprintf( - n__('%{count} participant', '%{count} participants', this.participants.length), - { count: this.loading ? '' : this.participantCount }, - ); - }, - participantCount() { - return this.participants.length; - }, + return label; + }, + participantLabel() { + return sprintf( + n__('%{count} participant', '%{count} participants', this.participants.length), + { count: this.loading ? '' : this.participantCount }, + ); + }, + participantCount() { + return this.participants.length; + }, + }, + methods: { + toggleMoreParticipants() { + this.isShowingMoreParticipants = !this.isShowingMoreParticipants; }, - methods: { - toggleMoreParticipants() { - this.isShowingMoreParticipants = !this.isShowingMoreParticipants; - }, - onClickCollapsedIcon() { - this.$emit('toggleSidebar'); - }, + onClickCollapsedIcon() { + this.$emit('toggleSidebar'); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue index 5c1ead1a8ac..4ac515e552a 100644 --- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue @@ -1,23 +1,23 @@ <script> - import Store from '../../stores/sidebar_store'; - import participants from './participants.vue'; +import Store from '../../stores/sidebar_store'; +import participants from './participants.vue'; - export default { - components: { - participants, +export default { + components: { + participants, + }, + props: { + mediator: { + type: Object, + required: true, }, - props: { - mediator: { - type: Object, - required: true, - }, - }, - data() { - return { - store: new Store(), - }; - }, - }; + }, + data() { + return { + store: new Store(), + }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue index 385717e7c1e..95a2c8cce6e 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -21,10 +21,9 @@ export default { }, methods: { onToggleSubscription() { - this.mediator.toggleSubscription() - .catch(() => { - Flash(__('Error occurred when toggling the notification subscription')); - }); + this.mediator.toggleSubscription().catch(() => { + Flash(__('Error occurred when toggling the notification subscription')); + }); }, }, }; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 1d030c4f67f..259858e4b46 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -1,111 +1,111 @@ <script> - import { __, sprintf } from '~/locale'; - import { abbreviateTime } from '~/lib/utils/pretty_time'; - import icon from '~/vue_shared/components/icon.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; +import { __, sprintf } from '~/locale'; +import { abbreviateTime } from '~/lib/utils/datetime_utility'; +import icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; - export default { - name: 'TimeTrackingCollapsedState', - components: { - icon, +export default { + name: 'TimeTrackingCollapsedState', + components: { + icon, + }, + directives: { + tooltip, + }, + props: { + showComparisonState: { + type: Boolean, + required: true, }, - directives: { - tooltip, + showSpentOnlyState: { + type: Boolean, + required: true, }, - props: { - showComparisonState: { - type: Boolean, - required: true, - }, - showSpentOnlyState: { - type: Boolean, - required: true, - }, - showEstimateOnlyState: { - type: Boolean, - required: true, - }, - showNoTimeTrackingState: { - type: Boolean, - required: true, - }, - timeSpentHumanReadable: { - type: String, - required: false, - default: '', - }, - timeEstimateHumanReadable: { - type: String, - required: false, - default: '', - }, + showEstimateOnlyState: { + type: Boolean, + required: true, }, - computed: { - timeSpent() { - return this.abbreviateTime(this.timeSpentHumanReadable); - }, - timeEstimate() { - return this.abbreviateTime(this.timeEstimateHumanReadable); - }, - divClass() { - if (this.showComparisonState) { - return 'compare'; - } else if (this.showEstimateOnlyState) { - return 'estimate-only'; - } else if (this.showSpentOnlyState) { - return 'spend-only'; - } else if (this.showNoTimeTrackingState) { - return 'no-tracking'; - } + showNoTimeTrackingState: { + type: Boolean, + required: true, + }, + timeSpentHumanReadable: { + type: String, + required: false, + default: '', + }, + timeEstimateHumanReadable: { + type: String, + required: false, + default: '', + }, + }, + computed: { + timeSpent() { + return this.abbreviateTime(this.timeSpentHumanReadable); + }, + timeEstimate() { + return this.abbreviateTime(this.timeEstimateHumanReadable); + }, + divClass() { + if (this.showComparisonState) { + return 'compare'; + } else if (this.showEstimateOnlyState) { + return 'estimate-only'; + } else if (this.showSpentOnlyState) { + return 'spend-only'; + } else if (this.showNoTimeTrackingState) { + return 'no-tracking'; + } + return ''; + }, + spanClass() { + if (this.showComparisonState) { return ''; - }, - spanClass() { - if (this.showComparisonState) { - return ''; - } else if (this.showEstimateOnlyState || this.showSpentOnlyState) { - return 'bold'; - } else if (this.showNoTimeTrackingState) { - return 'no-value'; - } + } else if (this.showEstimateOnlyState || this.showSpentOnlyState) { + return 'bold'; + } else if (this.showNoTimeTrackingState) { + return 'no-value'; + } - return ''; - }, - text() { - if (this.showComparisonState) { - return `${this.timeSpent} / ${this.timeEstimate}`; - } else if (this.showEstimateOnlyState) { - return `-- / ${this.timeEstimate}`; - } else if (this.showSpentOnlyState) { - return `${this.timeSpent} / --`; - } else if (this.showNoTimeTrackingState) { - return 'None'; - } + return ''; + }, + text() { + if (this.showComparisonState) { + return `${this.timeSpent} / ${this.timeEstimate}`; + } else if (this.showEstimateOnlyState) { + return `-- / ${this.timeEstimate}`; + } else if (this.showSpentOnlyState) { + return `${this.timeSpent} / --`; + } else if (this.showNoTimeTrackingState) { + return 'None'; + } - return ''; - }, - timeTrackedTooltipText() { - let title; - if (this.showComparisonState) { - title = __('Time remaining'); - } else if (this.showEstimateOnlyState) { - title = __('Estimated'); - } else if (this.showSpentOnlyState) { - title = __('Time spent'); - } + return ''; + }, + timeTrackedTooltipText() { + let title; + if (this.showComparisonState) { + title = __('Time remaining'); + } else if (this.showEstimateOnlyState) { + title = __('Estimated'); + } else if (this.showSpentOnlyState) { + title = __('Time spent'); + } - return sprintf('%{title}: %{text}', ({ title, text: this.text })); - }, - tooltipText() { - return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText; - }, + return sprintf('%{title}: %{text}', { title, text: this.text }); + }, + tooltipText() { + return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText; }, - methods: { - abbreviateTime(timeStr) { - return abbreviateTime(timeStr); - }, + }, + methods: { + abbreviateTime(timeStr) { + return abbreviateTime(timeStr); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index dc599e1b9fc..e74912d628f 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -1,5 +1,5 @@ <script> -import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time'; +import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import tooltip from '../../../vue_shared/directives/tooltip'; export default { diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue index 19ec0f05a26..91909cd49b8 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue @@ -15,16 +15,22 @@ export default { }, estimateText() { return sprintf( - s__('estimateCommand|%{slash_command} will update the estimated time with the latest command.'), { + s__( + 'estimateCommand|%{slash_command} will update the estimated time with the latest command.', + ), + { slash_command: '<code>/estimate</code>', - }, false, + }, + false, ); }, spendText() { return sprintf( - s__('spendCommand|%{slash_command} will update the sum of the time spent.'), { + s__('spendCommand|%{slash_command} will update the sum of the time spent.'), + { slash_command: '<code>/spend</code>', - }, false, + }, + false, ); }, }, diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue index 8660b0546cf..8e8b9f19b6e 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue @@ -26,7 +26,7 @@ export default { methods: { listenForQuickActions() { $(document).on('ajax:success', '.gfm-form', this.quickActionListened); - eventHub.$on('timeTrackingUpdated', (data) => { + eventHub.$on('timeTrackingUpdated', data => { this.quickActionListened(null, data); }); }, @@ -34,9 +34,7 @@ export default { const subscribedCommands = ['spend_time', 'time_estimate']; let changedCommands; if (data !== undefined) { - changedCommands = data.commands_changes - ? Object.keys(data.commands_changes) - : []; + changedCommands = data.commands_changes ? Object.keys(data.commands_changes) : []; } else { changedCommands = []; } diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index a6b3a674952..bc59774f0a8 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -41,9 +41,9 @@ export default { }, computed: { buttonClasses() { - return this.collapsed ? - 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' : - 'btn btn-default btn-todo issuable-header-btn float-right'; + return this.collapsed + ? 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' + : 'btn btn-default btn-todo issuable-header-btn float-right'; }, buttonLabel() { return this.isTodo ? MARK_TEXT : TODO_TEXT; diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index b267422cd97..225ebb61195 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -37,7 +37,8 @@ class SidebarMoveIssue { // Keep the dropdown open after selecting an option shouldPropagate: false, data: (searchTerm, callback) => { - this.mediator.fetchAutocompleteProjects(searchTerm) + this.mediator + .fetchAutocompleteProjects(searchTerm) .then(callback) .catch(() => new window.Flash('An error occurred while fetching projects autocomplete.')); }, @@ -48,7 +49,7 @@ class SidebarMoveIssue { </a> </li> `, - clicked: (options) => { + clicked: options => { const project = options.selectedObj; const selectedProjectId = options.isMarking ? project.id : 0; this.mediator.setMoveToProjectId(selectedProjectId); @@ -68,17 +69,12 @@ class SidebarMoveIssue { onConfirmClicked() { if (isValidProjectId(this.mediator.store.moveToProjectId)) { - this.$confirmButton - .disable() - .addClass('is-loading'); + this.$confirmButton.disable().addClass('is-loading'); - this.mediator.moveIssue() - .catch(() => { - window.Flash('An error occurred while moving the issue.'); - this.$confirmButton - .enable() - .removeClass('is-loading'); - }); + this.mediator.moveIssue().catch(() => { + window.Flash('An error occurred while moving the issue.'); + this.$confirmButton.enable().removeClass('is-loading'); + }); } } } diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js index 87da65a1b1f..1ebdbec7bc9 100644 --- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js @@ -15,15 +15,16 @@ export default class SidebarMilestone { components: { timeTracker, }, - render: createElement => createElement('timeTracker', { - props: { - timeEstimate: parseInt(timeEstimate, 10), - timeSpent: parseInt(timeSpent, 10), - humanTimeEstimate, - humanTimeSpent, - rootPath: '/', - }, - }), + render: createElement => + createElement('timeTracker', { + props: { + timeEstimate: parseInt(timeEstimate, 10), + timeSpent: parseInt(timeSpent, 10), + humanTimeEstimate, + humanTimeSpent, + rootPath: '/', + }, + }), }); } } diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 655bf9198b7..6f8214b18ee 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -22,14 +22,15 @@ function mountAssigneesComponent(mediator) { components: { SidebarAssignees, }, - render: createElement => createElement('sidebar-assignees', { - props: { - mediator, - field: el.dataset.field, - signedIn: el.hasAttribute('data-signed-in'), - issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', - }, - }), + render: createElement => + createElement('sidebar-assignees', { + props: { + mediator, + field: el.dataset.field, + signedIn: el.hasAttribute('data-signed-in'), + issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', + }, + }), }); } @@ -83,11 +84,12 @@ function mountParticipantsComponent(mediator) { components: { sidebarParticipants, }, - render: createElement => createElement('sidebar-participants', { - props: { - mediator, - }, - }), + render: createElement => + createElement('sidebar-participants', { + props: { + mediator, + }, + }), }); } @@ -102,11 +104,12 @@ function mountSubscriptionsComponent(mediator) { components: { sidebarSubscriptions, }, - render: createElement => createElement('sidebar-subscriptions', { - props: { - mediator, - }, - }), + render: createElement => + createElement('sidebar-subscriptions', { + props: { + mediator, + }, + }), }); } diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js index 37c97225bfd..cbe20f761ff 100644 --- a/app/assets/javascripts/sidebar/services/sidebar_service.js +++ b/app/assets/javascripts/sidebar/services/sidebar_service.js @@ -22,11 +22,15 @@ export default class SidebarService { } update(key, data) { - return Vue.http.put(this.endpoint, { - [key]: data, - }, { - emulateJSON: true, - }); + return Vue.http.put( + this.endpoint, + { + [key]: data, + }, + { + emulateJSON: true, + }, + ); } getProjectsAutocomplete(searchTerm) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 6c87287a4c4..5e7a35e9396 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -2,6 +2,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue'; +import { __ } from '~/locale'; import timeagoMixin from '../../vue_shared/mixins/timeago'; import tooltip from '../../vue_shared/directives/tooltip'; import LoadingButton from '../../vue_shared/components/loading_button.vue'; @@ -31,6 +32,11 @@ export default { required: true, }, }, + deployedTextMap: { + running: __('Deploying to'), + success: __('Deployed to'), + failed: __('Failed to deploy to'), + }, data() { const features = window.gon.features || {}; return { @@ -54,10 +60,13 @@ export default { hasMetrics() { return !!this.deployment.metrics_url; }, + deployedText() { + return this.$options.deployedTextMap[this.deployment.status]; + }, }, methods: { stopEnvironment() { - const msg = 'Are you sure you want to stop this environment?'; + const msg = __('Are you sure you want to stop this environment?'); const isConfirmed = confirm(msg); // eslint-disable-line if (isConfirmed) { @@ -87,10 +96,10 @@ export default { <div class="ci-widget media"> <div class="media-body"> <div class="deploy-body"> - <div class="deployment-info"> + <div class="js-deployment-info deployment-info"> <template v-if="hasDeploymentMeta"> <span> - Deployed to + {{ deployedText }} </span> <tooltip-on-truncate :title="deployment.name" diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js deleted file mode 100644 index a23496c6bf5..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * This file is the centerpiece of an attempt to reduce potential conflicts - * between the CE and EE versions of the MR widget. EE additions to the MR widget should - * be contained in the ee/vue_merge_request_widget directory, and should **extend** - * rather than mutate CE MR Widget code. - * - * This file should be the only source of conflicts between EE and CE. EE-only components should - * imported directly where they are needed, and import paths for EE extensions of CE components - * should overwrite import paths **without** changing the order of dependencies listed here. - */ - -export { default as Vue } from 'vue'; -export { default as SmartInterval } from '~/smart_interval'; -export { default as WidgetHeader } from './components/mr_widget_header.vue'; -export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; -export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; -export { default as Deployment } from './components/deployment.vue'; -export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; -export { default as MergedState } from './components/states/mr_widget_merged.vue'; -export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; -export { default as ClosedState } from './components/states/mr_widget_closed.vue'; -export { default as MergingState } from './components/states/mr_widget_merging.vue'; -export { default as WorkInProgressState } from './components/states/work_in_progress.vue'; -export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; -export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; -export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue'; -export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue'; -export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue'; -export { default as ReadyToMergeState } from './components/states/ready_to_merge.vue'; -export { default as ShaMismatchState } from './components/states/sha_mismatch.vue'; -export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue'; -export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue'; -export { default as PipelineFailedState } from './components/states/pipeline_failed.vue'; -export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; -export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; -export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue'; -export { default as CheckingState } from './components/states/mr_widget_checking.vue'; -export { default as MRWidgetStore } from './stores/mr_widget_store'; -export { default as MRWidgetService } from './services/mr_widget_service'; -export { default as eventHub } from './event_hub'; -export { default as getStateKey } from './stores/get_state_key'; -export { default as stateMaps } from './stores/state_maps'; -export { default as SquashBeforeMerge } from './components/states/squash_before_merge.vue'; -export { default as notify } from '../lib/utils/notify'; -export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue'; - -export { default as mrWidgetOptions } from './mr_widget_options.vue'; diff --git a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js new file mode 100644 index 00000000000..8780aa4bd1c --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js @@ -0,0 +1,3 @@ +import MRWidgetOptions from './mr_widget_options.vue'; + +export default MRWidgetOptions; diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index cc6e620f365..60cebbfc2b2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -1,4 +1,5 @@ -import { Vue, mrWidgetOptions } from './dependencies'; +import Vue from 'vue'; +import MrWidgetOptions from './ee_switch_mr_widget_options'; import Translate from '../vue_shared/translate'; Vue.use(Translate); @@ -6,7 +7,7 @@ Vue.use(Translate); export default () => { gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; - const vm = new Vue(mrWidgetOptions); + const vm = new Vue(MrWidgetOptions); window.gl.mrWidget = { checkStatus: vm.checkStatus, diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 8180f13a7cb..a5c69d2bc7a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -1,40 +1,40 @@ <script> +import _ from 'underscore'; +import { __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; import createFlash from '../flash'; -import { - WidgetHeader, - WidgetMergeHelp, - WidgetPipeline, - Deployment, - WidgetRelatedLinks, - MergedState, - ClosedState, - MergingState, - RebaseState, - WorkInProgressState, - ArchivedState, - ConflictsState, - NothingToMergeState, - MissingBranchState, - NotAllowedState, - ReadyToMergeState, - ShaMismatchState, - UnresolvedDiscussionsState, - PipelineBlockedState, - PipelineFailedState, - FailedToMerge, - MergeWhenPipelineSucceedsState, - AutoMergeFailed, - CheckingState, - MRWidgetStore, - MRWidgetService, - eventHub, - stateMaps, - SquashBeforeMerge, - notify, - SourceBranchRemovalStatus, -} from './dependencies'; +import WidgetHeader from './components/mr_widget_header.vue'; +import WidgetMergeHelp from './components/mr_widget_merge_help.vue'; +import WidgetPipeline from './components/mr_widget_pipeline.vue'; +import Deployment from './components/deployment.vue'; +import WidgetRelatedLinks from './components/mr_widget_related_links.vue'; +import MergedState from './components/states/mr_widget_merged.vue'; +import ClosedState from './components/states/mr_widget_closed.vue'; +import MergingState from './components/states/mr_widget_merging.vue'; +import RebaseState from './components/states/mr_widget_rebase.vue'; +import WorkInProgressState from './components/states/work_in_progress.vue'; +import ArchivedState from './components/states/mr_widget_archived.vue'; +import ConflictsState from './components/states/mr_widget_conflicts.vue'; +import NothingToMergeState from './components/states/nothing_to_merge.vue'; +import MissingBranchState from './components/states/mr_widget_missing_branch.vue'; +import NotAllowedState from './components/states/mr_widget_not_allowed.vue'; +import ReadyToMergeState from './components/states/ready_to_merge.vue'; +import ShaMismatchState from './components/states/sha_mismatch.vue'; +import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue'; +import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue'; +import PipelineFailedState from './components/states/pipeline_failed.vue'; +import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue'; +import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; +import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue'; +import CheckingState from './components/states/mr_widget_checking.vue'; +import MRWidgetStore from './stores/ee_switch_mr_widget_store'; +import MRWidgetService from './services/ee_switch_mr_widget_service'; +import eventHub from './event_hub'; +import stateMaps from './stores/ee_switch_state_maps'; +import SquashBeforeMerge from './components/states/squash_before_merge.vue'; +import notify from '~/lib/utils/notify'; +import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue'; import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; import { setFaviconOverlay } from '../lib/utils/common_utils'; @@ -82,6 +82,7 @@ export default { const service = this.createService(store); return { mr: store, + state: store.state, service, }; }, @@ -105,6 +106,17 @@ export default { (!this.mr.isNothingToMergeState && !this.mr.isMergedState) ); }, + shouldRenderMergedPipeline() { + return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline); + }, + }, + watch: { + state(newVal, oldVal) { + if (newVal !== oldVal && this.shouldRenderMergedPipeline) { + // init polling + this.initPostMergeDeploymentsPolling(); + } + } }, created() { this.initPolling(); @@ -114,11 +126,19 @@ export default { mounted() { this.setFaviconHelper(); this.initDeploymentsPolling(); + + if (this.shouldRenderMergedPipeline) { + this.initPostMergeDeploymentsPolling(); + } }, beforeDestroy() { eventHub.$off('mr.discussion.updated', this.checkStatus); this.pollingInterval.destroy(); this.deploymentsInterval.destroy(); + + if (this.postMergeDeploymentsInterval) { + this.postMergeDeploymentsInterval.destroy(); + } }, methods: { createService(store) { @@ -148,7 +168,13 @@ export default { cb.call(null, data); } }) - .catch(() => createFlash('Something went wrong. Please try again.')); + .catch(() => createFlash(__('Something went wrong. Please try again.'))); + }, + setFaviconHelper() { + if (this.mr.ciStatusFaviconPath) { + return setFaviconOverlay(this.mr.ciStatusFaviconPath); + } + return Promise.resolve(); }, initPolling() { this.pollingInterval = new SmartInterval({ @@ -160,8 +186,14 @@ export default { }); }, initDeploymentsPolling() { - this.deploymentsInterval = new SmartInterval({ - callback: this.fetchDeployments, + this.deploymentsInterval = this.deploymentsPoll(this.fetchPreMergeDeployments); + }, + initPostMergeDeploymentsPolling() { + this.postMergeDeploymentsInterval = this.deploymentsPoll(this.fetchPostMergeDeployments); + }, + deploymentsPoll(callback) { + return new SmartInterval({ + callback, startingInterval: 30000, maxInterval: 120000, hiddenInterval: 240000, @@ -169,26 +201,29 @@ export default { immediateExecution: true, }); }, - setFaviconHelper() { - if (this.mr.ciStatusFaviconPath) { - return setFaviconOverlay(this.mr.ciStatusFaviconPath); - } - return Promise.resolve(); + fetchDeployments(target) { + return this.service.fetchDeployments(target); }, - fetchDeployments() { - return this.service - .fetchDeployments() - .then(res => res.data) - .then(data => { + fetchPreMergeDeployments() { + return this.fetchDeployments() + .then(({ data }) => { if (data.length) { this.mr.deployments = data; } }) - .catch(() => { - createFlash( - 'Something went wrong while fetching the environments for this merge request. Please try again.', - ); - }); + .catch(() => this.throwDeploymentsError()); + }, + fetchPostMergeDeployments(){ + return this.fetchDeployments('merge_commit') + .then(({ data }) => { + if (data.length) { + this.mr.postMergeDeployments = data; + } + }) + .catch(() => this.throwDeploymentsError()); + }, + throwDeploymentsError() { + createFlash(__('Something went wrong while fetching the environments for this merge request. Please try again.')); }, fetchActionsContent() { this.service @@ -201,7 +236,7 @@ export default { Project.initRefSwitcher(); } }) - .catch(() => createFlash('Something went wrong. Please try again.')); + .catch(() => createFlash(__('Something went wrong. Please try again.'))); }, handleNotification(data) { if (data.ci_status === this.mr.ciStatus) return; @@ -269,7 +304,8 @@ export default { /> <deployment v-for="deployment in mr.deployments" - :key="deployment.id" + :key="`pre-merge-deploy-${deployment.id}`" + class="js-pre-merge-deploy" :deployment="deployment" /> <div class="mr-section-container"> @@ -310,5 +346,22 @@ export default { <mr-widget-merge-help /> </div> </div> + + <template v-if="shouldRenderMergedPipeline"> + <mr-widget-pipeline + class="js-post-merge-pipeline prepend-top-default" + :pipeline="mr.mergePipeline" + :ci-status="mr.ciStatus" + :has-ci="mr.hasCI" + :source-branch="mr.targetBranch" + :source-branch-link="mr.targetBranch" + /> + <deployment + v-for="postMergeDeployment in mr.postMergeDeployments" + :key="`post-merge-deploy-${postMergeDeployment.id}`" + :deployment="postMergeDeployment" + class="js-post-deployment" + /> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js new file mode 100644 index 00000000000..ea2aabb78fe --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js @@ -0,0 +1,3 @@ +import MRWidgetService from './mr_widget_service'; + +export default MRWidgetService; diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index fecbfec2214..bf5b85b2ae6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -21,8 +21,12 @@ export default class MRWidgetService { return axios.delete(this.endpoints.sourceBranchPath); } - fetchDeployments() { - return axios.get(this.endpoints.ciEnvironmentsStatusPath); + fetchDeployments(targetParam) { + return axios.get(this.endpoints.ciEnvironmentsStatusPath, { + params: { + environment_target: targetParam + } + }); } poll() { diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js new file mode 100644 index 00000000000..ebef30e3eab --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js @@ -0,0 +1,3 @@ +import getStateKey from './get_state_key'; + +export default getStateKey; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js new file mode 100644 index 00000000000..92a07c53f2d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js @@ -0,0 +1,3 @@ +import MergeRequestStore from './mr_widget_store'; + +export default MergeRequestStore; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js new file mode 100644 index 00000000000..50cf9503ea7 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js @@ -0,0 +1,3 @@ +import stateMaps from './state_maps'; + +export default stateMaps; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 672e5280b5e..a0c008e7314 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -1,5 +1,5 @@ import Timeago from 'timeago.js'; -import { getStateKey } from '../dependencies'; +import getStateKey from './ee_switch_get_state_key'; import { stateKey } from './state_maps'; import { formatDate } from '../../lib/utils/datetime_utility'; @@ -32,7 +32,9 @@ export default class MergeRequestStore { this.commitsCount = data.commits_count; this.divergedCommitsCount = data.diverged_commits_count; this.pipeline = data.pipeline || {}; + this.mergePipeline = data.merge_pipeline || {}; this.deployments = this.deployments || data.deployments || []; + this.postMergeDeployments = this.postMergeDeployments || []; this.initRebase(data); if (data.issues_links) { diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 36a345130c0..2d89a156117 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -34,10 +34,21 @@ export default { required: false, default: false, }, + displayTextKey: { + type: String, + required: false, + default: 'name', + }, + shouldTruncateStart: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { mouseOver: false, + truncateStart: 0, }; }, computed: { @@ -60,6 +71,15 @@ export default { 'is-open': this.file.opened, }; }, + outputText() { + const text = this.file[this.displayTextKey]; + + if (this.truncateStart === 0) { + return text; + } + + return `...${text.substring(this.truncateStart, text.length)}`; + }, }, watch: { 'file.active': function fileActiveWatch(active) { @@ -72,6 +92,15 @@ export default { if (this.hasPathAtCurrentRoute()) { this.scrollIntoView(true); } + + if (this.shouldTruncateStart) { + const { scrollWidth, offsetWidth } = this.$refs.textOutput; + const textOverflow = scrollWidth - offsetWidth; + + if (textOverflow > 0) { + this.truncateStart = Math.ceil(textOverflow / 5) + 3; + } + } }, methods: { toggleTreeOpen(path) { @@ -139,6 +168,7 @@ export default { class="file-row-name-container" > <span + ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated" > @@ -156,7 +186,7 @@ export default { :size="16" class="append-right-5" /> - {{ file.name }} + {{ outputText }} </span> <component :is="extraComponent" @@ -175,6 +205,8 @@ export default { :hide-extra-on-tree="hideExtraOnTree" :extra-component="extraComponent" :show-changed-icon="showChangedIcon" + :display-text-key="displayTextKey" + :should-truncate-start="shouldTruncateStart" @toggleTreeOpen="toggleTreeOpen" @clickFile="clickedFile" /> diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue new file mode 100644 index 00000000000..9327a2a4a6c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue @@ -0,0 +1,49 @@ +<script> +import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility'; + +/** + * Counts down to a given end date. + */ +export default { + props: { + endDateString: { + type: String, + required: true, + validator(value) { + return !Number.isNaN(new Date(value).getTime()); + }, + }, + }, + + data() { + return { + remainingTime: formatTime(0), + countdownUpdateIntervalId: null, + }; + }, + + mounted() { + const updateRemainingTime = () => { + const remainingMilliseconds = calculateRemainingMilliseconds(this.endDateString); + this.remainingTime = formatTime(remainingMilliseconds); + }; + + updateRemainingTime(); + this.countdownUpdateIntervalId = window.setInterval(updateRemainingTime, 1000); + }, + + beforeDestroy() { + window.clearInterval(this.countdownUpdateIntervalId); + }, +}; +</script> + +<template> + <time + v-gl-tooltip + :datetime="endDateString" + :title="endDateString" + > + {{ remainingTime }} + </time> +</template> diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index 782d8e3abf6..26c99aecae4 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -1,6 +1,6 @@ <script> import Pikaday from 'pikaday'; -import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix'; +import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility'; export default { name: 'DatePicker', diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue index 7f1eb6bcec4..11fac3bb12c 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue @@ -1,6 +1,11 @@ <script> + import tooltip from '~/vue_shared/directives/tooltip'; + export default { name: 'CollapsedCalendarIcon', + directives: { + tooltip, + }, props: { containerClass: { type: String, @@ -17,6 +22,11 @@ required: false, default: true, }, + tooltipText: { + type: String, + required: false, + default: '', + }, }, methods: { click() { @@ -28,7 +38,13 @@ <template> <div + v-tooltip :class="containerClass" + :title="tooltipText" + data-container="body" + data-placement="left" + data-html="true" + data-boundary="viewport" @click="click" > <i diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue index dac438a702d..6e7194ccc9e 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue @@ -1,25 +1,23 @@ <script> - import { dateInWords } from '../../../lib/utils/datetime_utility'; - import toggleSidebar from './toggle_sidebar.vue'; + import { __ } from '~/locale'; + import timeagoMixin from '~/vue_shared/mixins/timeago'; + import { dateInWords, timeFor } from '~/lib/utils/datetime_utility'; import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; export default { name: 'SidebarCollapsedGroupedDatePicker', components: { - toggleSidebar, collapsedCalendarIcon, }, + mixins: [ + timeagoMixin, + ], props: { collapsed: { type: Boolean, required: false, default: true, }, - showToggleSidebar: { - type: Boolean, - required: false, - default: false, - }, minDate: { type: Date, required: false, @@ -51,7 +49,7 @@ }, iconClass() { const disabledClass = this.disableClickableIcons ? 'disabled' : ''; - return `block sidebar-collapsed-icon calendar-icon ${disabledClass}`; + return `sidebar-collapsed-icon calendar-icon ${disabledClass}`; }, }, methods: { @@ -63,7 +61,21 @@ const dateWords = dateInWords(date, true); const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords; - return date ? parsedDateWords : 'None'; + return date ? parsedDateWords : __('None'); + }, + tooltipText(dateType = 'min') { + const defaultText = dateType === 'min' ? __('Start date') : __('Due date'); + const date = this[`${dateType}Date`]; + const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date); + const dateText = date ? [ + this.dateText(dateType), + `(${timeAgo})`, + ].join(' ') : ''; + + if (date) { + return [defaultText, dateText].join('<br />'); + } + return __('Start and due date'); }, }, }; @@ -71,18 +83,10 @@ <template> <div class="block sidebar-grouped-item"> - <div - v-if="showToggleSidebar" - class="issuable-sidebar-header" - > - <toggle-sidebar - :collapsed="collapsed" - @toggle="toggleSidebar" - /> - </div> <collapsed-calendar-icon v-if="showMinDateBlock" :container-class="iconClass" + :tooltip-text="tooltipText('min')" @click="toggleSidebar" > <span class="sidebar-collapsed-value"> @@ -99,7 +103,7 @@ <collapsed-calendar-icon v-if="maxDate" :container-class="iconClass" - :show-icon="!minDate" + :tooltip-text="tooltipText('max')" @click="toggleSidebar" > <span class="sidebar-collapsed-value"> diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index 4f2412ce520..549d27e96d9 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -9,6 +9,14 @@ export default { componentUpdated(el) { $(el).tooltip('_fixTitle'); + + // update visible tooltips + const tooltipInstance = $(el).data('bs.tooltip'); + const tip = tooltipInstance.getTipElement(); + tooltipInstance.setElementContent( + $(tip.querySelectorAll('.tooltip-inner')), + tooltipInstance.getTitle(), + ); }, unbind(el) { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 3c9505a21d6..fa753b13e5f 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -334,6 +334,14 @@ img.emoji { } } +.outline-0 { + outline: 0; + + &:focus { + outline: 0; + } +} + /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } .prepend-top-2 { margin-top: 2px; } @@ -369,3 +377,5 @@ img.emoji { .flex-align-self-center { align-self: center; } .flex-grow { flex-grow: 1; } .flex-no-shrink { flex-shrink: 0; } +.mw-460 { max-width: 460px; } +.ws-initial { white-space: initial; } diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index bf6f66d30ff..f47dfe1b563 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -37,6 +37,7 @@ button { padding-top: 0; + background-color: transparent; } &.active a, diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 6d891e21556..e261bd7c0ca 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -34,7 +34,7 @@ margin-bottom: 0; } - *:first-child:not(.katex-display) { + *:first-child { margin-top: 0; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 8d884ad6891..52c91266ff4 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1027,8 +1027,12 @@ overflow-x: auto; } -.tree-list-search .form-control { - padding-left: 30px; +.tree-list-search { + flex: 0 0 34px; + + .form-control { + padding-left: 30px; + } } .tree-list-icon { @@ -1063,3 +1067,9 @@ } } } + +.tree-list-view-toggle { + svg { + top: 0; + } +} diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb index b7e4f9b81f1..3cdf4ddf8bb 100644 --- a/app/controllers/concerns/boards_responses.rb +++ b/app/controllers/concerns/boards_responses.rb @@ -50,7 +50,10 @@ module BoardsResponses end def authorize_create_issue - authorize_action_for!(project, :admin_issue) + list = List.find(issue_params[:list_id]) + action = list.backlog? ? :create_issue : :admin_issue + + authorize_action_for!(project, action) end def authorize_admin_list diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 6e17bc212e4..3802aa5f40f 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -4,12 +4,13 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController include MilestoneActions before_action :projects + before_action :groups, only: :index before_action :milestone, only: [:show, :merge_requests, :participants, :labels] def index respond_to do |format| format.html do - @milestone_states = GlobalMilestone.states_count(@projects) + @milestone_states = Milestone.states_count(@projects.select(:id), @groups.select(:id)) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do @@ -42,4 +43,8 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController @milestone = DashboardMilestone.build(@projects, params[:title]) render_404 unless @milestone end + + def groups + @groups ||= GroupsFinder.new(current_user, state_all: true).execute + end end diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 8d259b4052e..cdc6f53df8e 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -5,6 +5,7 @@ class Groups::BoardsController < Groups::ApplicationController before_action :assign_endpoint_vars before_action :boards, only: :index + before_action :redirect_to_recent_board, only: :index def index respond_with_boards @@ -13,6 +14,9 @@ class Groups::BoardsController < Groups::ApplicationController def show @board = boards.find(params[:id]) + # add/update the board in the recent visited table + Boards::Visits::CreateService.new(@board.group, current_user).execute(@board) if request.format.html? + respond_with_board end @@ -31,4 +35,18 @@ class Groups::BoardsController < Groups::ApplicationController def serialize_as_json(resource) resource.as_json(only: [:id]) end + + def includes_board?(board_id) + boards.any? { |board| board.id == board_id } + end + + def redirect_to_recent_board + return if request.format.json? + + recently_visited = Boards::Visits::LatestService.new(group, current_user).execute + + if recently_visited && includes_board?(recently_visited.board_id) + redirect_to(group_board_path(id: recently_visited.board_id), status: :found) + end + end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index a7cee426cf1..b42116b0f36 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -10,7 +10,7 @@ class Groups::MilestonesController < Groups::ApplicationController def index respond_to do |format| format.html do - @milestone_states = GlobalMilestone.states_count(group_projects, group) + @milestone_states = Milestone.states_count(group_projects, [group]) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 77b818347c7..8189b5d182a 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -8,6 +8,7 @@ class Projects::BoardsController < Projects::ApplicationController before_action :authorize_read_board!, only: [:index, :show] before_action :boards, only: :index before_action :assign_endpoint_vars + before_action :redirect_to_recent_board, only: :index def index respond_with_boards @@ -16,6 +17,9 @@ class Projects::BoardsController < Projects::ApplicationController def show @board = boards.find(params[:id]) + # add/update the board in the recent visited table + Boards::Visits::CreateService.new(@board.project, current_user).execute(@board) if request.format.html? + respond_with_board end @@ -33,10 +37,24 @@ class Projects::BoardsController < Projects::ApplicationController end def authorize_read_board! - return access_denied! unless can?(current_user, :read_board, project) + access_denied! unless can?(current_user, :read_board, project) end def serialize_as_json(resource) resource.as_json(only: [:id]) end + + def includes_board?(board_id) + boards.any? { |board| board.id == board_id } + end + + def redirect_to_recent_board + return if request.format.json? + + recently_visited = Boards::Visits::LatestService.new(project, current_user).execute + + if recently_visited && includes_board?(recently_visited.board_id) + redirect_to(namespace_project_board_path(id: recently_visited.board_id), status: :found) + end + end end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index be708835e30..c0aa39d87c6 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -8,6 +8,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403 rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404 rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422 + rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503 # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) @@ -62,6 +63,10 @@ class Projects::GitHttpController < Projects::GitHttpClientController render plain: exception.message, status: :unprocessable_entity end + def render_503(exception) + render plain: exception.message, status: :service_unavailable + end + def access @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 94cd4f1fe9a..27b83da4f54 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -203,9 +203,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def ci_environments_status - environments = @merge_request.environments_for(current_user).map do |environment| - EnvironmentStatus.new(environment, @merge_request) - end + environments = if ci_environments_status_on_merge_result? + EnvironmentStatus.after_merge_request(@merge_request, current_user) + else + EnvironmentStatus.for_merge_request(@merge_request, current_user) + end render json: EnvironmentStatusSerializer.new(current_user: current_user).represent(environments) end @@ -243,6 +245,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo private + def ci_environments_status_on_merge_result? + params[:environment_target] == 'merge_commit' + end + def target_branch_missing? @merge_request.has_no_commits? && !@merge_request.target_branch_exists? end diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb index 78d5faf2326..53176978416 100644 --- a/app/controllers/projects/mirrors_controller.rb +++ b/app/controllers/projects/mirrors_controller.rb @@ -44,6 +44,22 @@ class Projects::MirrorsController < Projects::ApplicationController redirect_to_repository_settings(project, anchor: 'js-push-remote-settings') end + def ssh_host_keys + lookup = SshHostKey.new(project: project, url: params[:ssh_url], compare_host_keys: params[:compare_host_keys]) + + if lookup.error.present? + # Failed to read keys + render json: { message: lookup.error }, status: :bad_request + elsif lookup.known_hosts.nil? + # Still working, come back later + render body: nil, status: :no_content + else + render json: lookup + end + rescue ArgumentError => err + render json: { message: err.message }, status: :bad_request + end + private def remote_mirror diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 8abfe0c4c17..eb3d2498830 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -14,7 +14,7 @@ # project_id: integer # milestone_title: string # author_id: integer -# assignee_id: integer +# assignee_id: integer or 'None' or 'Any' # search: string # label_name: string # sort: string @@ -34,6 +34,11 @@ class IssuableFinder requires_cross_project_access unless: -> { project? } + # This is used as a common filter for None / Any + FILTER_NONE = 'none'.freeze + FILTER_ANY = 'any'.freeze + + # This is accepted as a deprecated filter and is also used in unassigning users NONE = '0'.freeze attr_accessor :current_user, :params @@ -236,16 +241,20 @@ class IssuableFinder # rubocop: enable CodeReuse/ActiveRecord def assignee_id? - params[:assignee_id].present? && params[:assignee_id].to_s != NONE + params[:assignee_id].present? end def assignee_username? - params[:assignee_username].present? && params[:assignee_username].to_s != NONE + params[:assignee_username].present? end - def no_assignee? + def filter_by_no_assignee? # Assignee_id takes precedence over assignee_username - params[:assignee_id].to_s == NONE || params[:assignee_username].to_s == NONE + [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE + end + + def filter_by_any_assignee? + params[:assignee_id].to_s.downcase == FILTER_ANY end # rubocop: disable CodeReuse/ActiveRecord @@ -399,15 +408,17 @@ class IssuableFinder # rubocop: disable CodeReuse/ActiveRecord def by_assignee(items) - if assignee - items = items.where(assignee_id: assignee.id) - elsif no_assignee? - items = items.where(assignee_id: nil) + if filter_by_no_assignee? + items.where(assignee_id: nil) + elsif filter_by_any_assignee? + items.where('assignee_id IS NOT NULL') + elsif assignee + items.where(assignee_id: assignee.id) elsif assignee_id? || assignee_username? # assignee not found - items = items.none + items.none + else + items end - - items end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index abdc47b9866..cee57a83df4 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -137,10 +137,12 @@ class IssuesFinder < IssuableFinder # rubocop: disable CodeReuse/ActiveRecord def by_assignee(items) - if assignee - items.assigned_to(assignee) - elsif no_assignee? + if filter_by_no_assignee? items.unassigned + elsif filter_by_any_assignee? + items.assigned + elsif assignee + items.assigned_to(assignee) elsif assignee_id? || assignee_username? # assignee not found items.none else diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index f573fd399a5..0c313e9e6d3 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,6 +1,15 @@ # frozen_string_literal: true module GroupsHelper + def group_overview_nav_link_paths + %w[ + groups#show + groups#activity + groups#subgroups + analytics#show + ] + end + def group_nav_link_paths %w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index] end diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb new file mode 100644 index 00000000000..92abbb67222 --- /dev/null +++ b/app/models/board_group_recent_visit.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Tracks which boards in a specific group a user has visited +class BoardGroupRecentVisit < ActiveRecord::Base + belongs_to :user + belongs_to :group + belongs_to :board + + validates :user, presence: true + validates :group, presence: true + validates :board, presence: true + + scope :by_user_group, -> (user, group) { where(user: user, group: group).order(:updated_at) } + + def self.visited!(user, board) + visit = find_or_create_by(user: user, group: board.group, board: board) + visit.touch if visit.updated_at < Time.now + rescue ActiveRecord::RecordNotUnique + retry + end + + def self.latest(user, group) + by_user_group(user, group).last + end +end diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb new file mode 100644 index 00000000000..7cffff906d8 --- /dev/null +++ b/app/models/board_project_recent_visit.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Tracks which boards in a specific project a user has visited +class BoardProjectRecentVisit < ActiveRecord::Base + belongs_to :user + belongs_to :project + belongs_to :board + + validates :user, presence: true + validates :project, presence: true + validates :board, presence: true + + scope :by_user_project, -> (user, project) { where(user: user, project: project).order(:updated_at) } + + def self.visited!(user, board) + visit = find_or_create_by(user: user, project: board.project, board: board) + visit.touch if visit.updated_at < Time.now + rescue ActiveRecord::RecordNotUnique + retry + end + + def self.latest(user, project) + by_user_project(user, project).last + end +end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 43bf852c7ec..b311f5e0617 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ActiveRecord::Base - VERSION = '0.1.34'.freeze + VERSION = '0.1.35'.freeze self.table_name = 'clusters_applications_runners' diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 95efecfc41d..222e4217e67 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -20,6 +20,12 @@ module Clusters has_many :cluster_projects, class_name: 'Clusters::Project' has_many :projects, through: :cluster_projects, class_name: '::Project' + has_many :cluster_groups, class_name: 'Clusters::Group' + has_many :groups, through: :cluster_groups, class_name: '::Group' + + has_one :cluster_group, -> { order(id: :desc) }, class_name: 'Clusters::Group' + has_one :group, through: :cluster_group, class_name: '::Group' + # we force autosave to happen when we save `Cluster` model has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true @@ -38,8 +44,12 @@ module Clusters accepts_nested_attributes_for :platform_kubernetes, update_only: true validates :name, cluster_name: true + validates :cluster_type, presence: true validate :restrict_modification, on: :update + validate :no_groups, unless: :group_type? + validate :no_projects, unless: :project_type? + delegate :status, to: :provider, allow_nil: true delegate :status_reason, to: :provider, allow_nil: true delegate :on_creation?, to: :provider, allow_nil: true @@ -50,6 +60,12 @@ module Clusters delegate :available?, to: :application_ingress, prefix: true, allow_nil: true delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true + enum cluster_type: { + instance_type: 1, + group_type: 2, + project_type: 3 + } + enum platform_type: { kubernetes: 1 } @@ -122,5 +138,17 @@ module Clusters true end + + def no_groups + if groups.any? + errors.add(:cluster, 'cannot have groups assigned') + end + end + + def no_projects + if projects.any? + errors.add(:cluster, 'cannot have projects assigned') + end + end end end diff --git a/app/models/clusters/group.rb b/app/models/clusters/group.rb new file mode 100644 index 00000000000..2b08a9e47f0 --- /dev/null +++ b/app/models/clusters/group.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Clusters + class Group < ActiveRecord::Base + self.table_name = 'cluster_groups' + + belongs_to :cluster, class_name: 'Clusters::Cluster' + belongs_to :group, class_name: '::Group' + end +end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index e8e943872de..f0f791742f4 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -107,7 +107,7 @@ module Clusters end def kubeclient - @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) + @kubeclient ||= build_kube_client! end private @@ -136,7 +136,7 @@ module Clusters Gitlab::NamespaceSanitizer.sanitize(slug) end - def build_kube_client!(api_groups: ['api'], api_version: 'v1') + def build_kube_client! raise "Incomplete settings" unless api_url && actual_namespace unless (username && password) || token @@ -145,8 +145,6 @@ module Clusters Gitlab::Kubernetes::KubeClient.new( api_url, - api_groups, - api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, http_proxy_uri: ENV['http_proxy'] diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 06507345fe8..7d36f9982ab 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -50,7 +50,8 @@ class CommitStatus < ActiveRecord::Base runner_system_failure: 4, missing_dependency_failure: 5, runner_unsupported: 6, - stale_schedule: 7 + stale_schedule: 7, + job_execution_timeout: 8 } ## @@ -109,7 +110,7 @@ class CommitStatus < ActiveRecord::Base before_transition any => :failed do |commit_status, transition| failure_reason = transition.args.first - commit_status.failure_reason = failure_reason + commit_status.failure_reason = CommitStatus.failure_reasons[failure_reason] end after_transition do |commit_status, transition| diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index 91052013592..e57a3383544 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -42,6 +42,7 @@ module DeploymentPlatform { name: 'kubernetes-template', projects: [self], + cluster_type: :project_type, provider_type: :user, platform_type: :kubernetes, platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 62dc0f2cbeb..ee5b96e7454 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -127,6 +127,10 @@ class Deployment < ActiveRecord::Base metrics&.merge(deployment_time: created_at.to_i) || {} end + def status + 'success' + end + private def prometheus_adapter diff --git a/app/models/environment.rb b/app/models/environment.rb index 0816c395185..1c31c01eb9f 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -149,7 +149,7 @@ class Environment < ActiveRecord::Base end def has_metrics? - prometheus_adapter&.can_query? && available? && last_deployment.present? + prometheus_adapter&.can_query? && available? end def metrics diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 5ff3acc0e58..a84871f7253 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -3,21 +3,33 @@ class EnvironmentStatus include Gitlab::Utils::StrongMemoize - attr_reader :environment, :merge_request + attr_reader :environment, :merge_request, :sha delegate :id, to: :environment delegate :name, to: :environment delegate :project, to: :environment delegate :deployed_at, to: :deployment, allow_nil: true + delegate :status, to: :deployment - def initialize(environment, merge_request) + def self.for_merge_request(mr, user) + build_environments_status(mr, user, mr.head_pipeline) + end + + def self.after_merge_request(mr, user) + return [] unless mr.merged? + + build_environments_status(mr, user, mr.merge_pipeline) + end + + def initialize(environment, merge_request, sha) @environment = environment @merge_request = merge_request + @sha = sha end def deployment strong_memoize(:deployment) do - environment.first_deployment_for(merge_request.diff_head_sha) + environment.first_deployment_for(sha) end end @@ -26,10 +38,9 @@ class EnvironmentStatus end def changes - sha = merge_request.diff_head_sha return [] if project.route_map_for(sha).nil? - changed_files.map { |file| build_change(file, sha) }.compact + changed_files.map { |file| build_change(file) }.compact end def changed_files @@ -41,7 +52,7 @@ class EnvironmentStatus PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze - def build_change(file, sha) + def build_change(file) public_path = project.public_path_for_source_path(file.new_path, sha) return if public_path.nil? @@ -53,4 +64,22 @@ class EnvironmentStatus external_url: environment.external_url_for(file.new_path, sha) } end + + def self.build_environments_status(mr, user, pipeline) + return [] unless pipeline.present? + + find_environments(user, pipeline).map do |environment| + EnvironmentStatus.new(environment, mr, pipeline.sha) + end + end + private_class_method :build_environments_status + + def self.find_environments(user, pipeline) + env_ids = Deployment.where(deployable: pipeline.builds).select(:environment_id) + + Environment.available.where(id: env_ids).select do |environment| + Ability.allowed?(user, :read_environment, environment) + end + end + private_class_method :find_environments end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index a6cebabe089..085ffd16c6a 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -34,50 +34,6 @@ class GlobalMilestone new(title, child_milestones) end - def self.states_count(projects, group = nil) - legacy_group_milestones_count = legacy_group_milestone_states_count(projects) - group_milestones_count = group_milestones_states_count(group) - - legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count| - legacy_group_milestones_count + group_milestones_count - end - end - - def self.group_milestones_states_count(group) - return STATE_COUNT_HASH unless group - - params = { group_ids: [group.id], state: 'all' } - - relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder - grouped_by_state = relation.reorder(nil).group(:state).count - - { - opened: grouped_by_state['active'] || 0, - closed: grouped_by_state['closed'] || 0, - all: grouped_by_state.values.sum - } - end - - # Counts the legacy group milestones which must be grouped by title - def self.legacy_group_milestone_states_count(projects) - return STATE_COUNT_HASH unless projects - - params = { project_ids: projects.map(&:id), state: 'all' } - - relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder - project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count - - opened = count_by_state(project_milestones_by_state_and_title, 'active') - closed = count_by_state(project_milestones_by_state_and_title, 'closed') - all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count - - { - opened: opened, - closed: closed, - all: all - } - end - def self.count_by_state(milestones_by_state_and_title, state) milestones_by_state_and_title.count do |(milestone_state, _), _| milestone_state == state diff --git a/app/models/group.rb b/app/models/group.rb index 612c546ca57..c67479110c9 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -41,6 +41,9 @@ class Group < Namespace has_many :boards has_many :badges, class_name: 'GroupBadge' + has_many :cluster_groups, class_name: 'Clusters::Group' + has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster' + has_many :todos accepts_nested_attributes_for :variables, allow_destroy: true diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6559f94a696..7eef08aa6a3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -204,6 +204,12 @@ class MergeRequest < ActiveRecord::Base head_pipeline&.sha == diff_head_sha ? head_pipeline : nil end + def merge_pipeline + return unless merged? + + target_project.pipeline_for(target_branch, merge_commit_sha) + end + # Pattern used to extract `!123` merge request references from text # # This pattern supports cross-project references. diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 892a680f221..9f2c4efaa96 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -170,6 +170,22 @@ class Milestone < ActiveRecord::Base sorted.with_order_id_desc end + def self.states_count(projects, groups = nil) + return STATE_COUNT_HASH unless projects || groups + + counts = Milestone + .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id)) + .reorder(nil) + .group(:state) + .count + + { + opened: counts['active'] || 0, + closed: counts['closed'] || 0, + all: counts.values.sum + } + end + ## # Returns the String necessary to reference this Milestone in Markdown. Group # milestones only support name references, and do not support cross-project diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index f119555f16b..798944d0c06 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -144,7 +144,7 @@ class KubernetesService < DeploymentService end def kubeclient - @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) + @kubeclient ||= build_kube_client! end def deprecated? @@ -182,13 +182,11 @@ class KubernetesService < DeploymentService slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kube_client!(api_groups: ['api'], api_version: 'v1') + def build_kube_client! raise "Incomplete settings" unless api_url && actual_namespace && token Gitlab::Kubernetes::KubeClient.new( api_url, - api_groups, - api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, http_proxy_uri: ENV['http_proxy'] diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb new file mode 100644 index 00000000000..b6844dbe870 --- /dev/null +++ b/app/models/ssh_host_key.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +# Detected SSH host keys are transiently stored in Redis +class SshHostKey + class Fingerprint < Gitlab::SSHPublicKey + attr_reader :index + + def initialize(key, index: nil) + super(key) + + @index = index + end + + def as_json(*) + { bits: bits, fingerprint: fingerprint, type: type, index: index } + end + end + + include ReactiveCaching + + self.reactive_cache_key = ->(key) { [key.class.to_s, key.id] } + + # Do not refresh the data in the background - it is not expected to change. + # This is achieved by making the lifetime shorter than the refresh interval. + self.reactive_cache_refresh_interval = 15.minutes + self.reactive_cache_lifetime = 10.minutes + + def self.find_by(opts = {}) + return nil unless opts.key?(:id) + + project_id, url = opts[:id].split(':', 2) + project = Project.find_by(id: project_id) + + project.presence && new(project: project, url: url) + end + + def self.fingerprint_host_keys(data) + return [] unless data.is_a?(String) + + data + .each_line + .each_with_index + .map { |line, index| Fingerprint.new(line, index: index) } + .select(&:valid?) + end + + attr_reader :project, :url, :compare_host_keys + + def initialize(project:, url:, compare_host_keys: nil) + @project = project + @url = normalize_url(url) + @compare_host_keys = compare_host_keys + end + + def id + [project.id, url].join(':') + end + + def as_json(*) + { + host_keys_changed: host_keys_changed?, + fingerprints: fingerprints, + known_hosts: known_hosts + } + end + + def known_hosts + with_reactive_cache { |data| data[:known_hosts] } + end + + def fingerprints + @fingerprints ||= self.class.fingerprint_host_keys(known_hosts) + end + + # Returns true if the known_hosts data differs from the version passed in at + # initialization as `compare_host_keys`. Comments, ordering, etc, is ignored + def host_keys_changed? + cleanup(known_hosts) != cleanup(compare_host_keys) + end + + def error + with_reactive_cache { |data| data[:error] } + end + + def calculate_reactive_cache + known_hosts, errors, status = + Open3.popen3({}, *%W[ssh-keyscan -T 5 -p #{url.port} -f-]) do |stdin, stdout, stderr, wait_thr| + stdin.puts(url.host) + stdin.close + + [ + cleanup(stdout.read), + cleanup(stderr.read), + wait_thr.value + ] + end + + # ssh-keyscan returns an exit code 0 in several error conditions, such as an + # unknown hostname, so check both STDERR and the exit code + if status.success? && !errors.present? + { known_hosts: known_hosts } + else + Rails.logger.debug("Failed to detect SSH host keys for #{id}: #{errors}") + + { error: 'Failed to detect SSH host keys' } + end + end + + private + + # Remove comments and duplicate entries + def cleanup(data) + data + .to_s + .each_line + .reject { |line| line.start_with?('#') || line.chomp.empty? } + .uniq + .sort + .join + end + + def normalize_url(url) + full_url = ::Addressable::URI.parse(url) + raise ArgumentError.new("Invalid URL") unless full_url&.scheme == 'ssh' + + Addressable::URI.parse("ssh://#{full_url.host}:#{full_url.inferred_port}") + rescue Addressable::URI::InvalidURIError + raise ArgumentError.new("Invalid URL") + end +end diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index 29eaad759bb..a866e76df5a 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -9,7 +9,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated runner_system_failure: 'There has been a runner system failure, please try again', missing_dependency_failure: 'There has been a missing dependency failure', runner_unsupported: 'Your runner is outdated, please upgrade your runner', - stale_schedule: 'Delayed job could not be executed by some reason, please try again' + stale_schedule: 'Delayed job could not be executed by some reason, please try again', + job_execution_timeout: 'The script exceeded the maximum execution time set for the job' }.freeze private_constant :CALLOUT_FAILURE_MESSAGES diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 066a5b1885c..9ddce0d2c80 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -5,6 +5,7 @@ class BuildDetailsEntity < JobEntity expose :tag_list, as: :tags expose :has_trace?, as: :has_trace expose :stage + expose :stuck?, as: :stuck expose :user, using: UserEntity expose :runner, using: RunnerEntity expose :pipeline, using: PipelineEntity diff --git a/app/serializers/environment_status_entity.rb b/app/serializers/environment_status_entity.rb index 3dfa4f204c9..f87cc894d2f 100644 --- a/app/serializers/environment_status_entity.rb +++ b/app/serializers/environment_status_entity.rb @@ -5,6 +5,7 @@ class EnvironmentStatusEntity < Grape::Entity expose :id expose :name + expose :status expose :url do |es| project_environment_path(es.project, es.environment) diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 9ec24f799ef..f33a1654d5e 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -55,6 +55,7 @@ class MergeRequestWidgetEntity < IssuableEntity expose :merge_commit_message expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline + expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)} # Booleans expose :merge_ongoing?, as: :merge_ongoing diff --git a/app/services/boards/visits/create_service.rb b/app/services/boards/visits/create_service.rb new file mode 100644 index 00000000000..e2adf755511 --- /dev/null +++ b/app/services/boards/visits/create_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Boards + module Visits + class CreateService < Boards::BaseService + def execute(board) + return unless current_user && Gitlab::Database.read_write? + + if parent.is_a?(Group) + BoardGroupRecentVisit.visited!(current_user, board) + else + BoardProjectRecentVisit.visited!(current_user, board) + end + end + end + end +end diff --git a/app/services/boards/visits/latest_service.rb b/app/services/boards/visits/latest_service.rb new file mode 100644 index 00000000000..9e4c77a6317 --- /dev/null +++ b/app/services/boards/visits/latest_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Boards + module Visits + class LatestService < Boards::BaseService + def execute + return nil unless current_user + + if parent.is_a?(Group) + BoardGroupRecentVisit.latest(current_user, parent) + else + BoardProjectRecentVisit.latest(current_user, parent) + end + end + end + end +end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index c6e955800af..cd843b8ffa8 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -9,9 +9,9 @@ module Clusters end def execute(project:, access_token: nil) - raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?(project) + raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?(project) - cluster_params = params.merge(user: current_user, projects: [project]) + cluster_params = params.merge(user: current_user, cluster_type: :project_type, projects: [project]) cluster_params[:provider_gcp_attributes].try do |provider| provider[:access_token] = access_token end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 3ae0a4a19d0..6ee63db8eb9 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -60,18 +60,15 @@ module Clusters 'https://' + gke_cluster.endpoint, Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), gke_cluster.master_auth.username, - gke_cluster.master_auth.password, - api_groups: ['api', 'apis/rbac.authorization.k8s.io'] + gke_cluster.master_auth.password ) end - def build_kube_client!(api_url, ca_pem, username, password, api_groups: ['api'], api_version: 'v1') + def build_kube_client!(api_url, ca_pem, username, password) raise "Incomplete settings" unless api_url && username && password Gitlab::Kubernetes::KubeClient.new( api_url, - api_groups, - api_version, auth_options: { username: username, password: password }, ssl_options: kubeclient_ssl_options(ca_pem), http_proxy_uri: ENV['http_proxy'] diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb index 596c0105ea0..7504773a002 100644 --- a/app/services/resource_events/merge_into_notes_service.rb +++ b/app/services/resource_events/merge_into_notes_service.rb @@ -34,7 +34,7 @@ module ResourceEvents def label_events_by_discussion_id return [] unless resource.respond_to?(:resource_label_events) - events = resource.resource_label_events.includes(:label, :user) + events = resource.resource_label_events.includes(:label, user: :status) events = since_fetch_at(events) events.group_by { |event| event.discussion_id } diff --git a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml index 0a5717f75e1..b854e15d36f 100644 --- a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml +++ b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml @@ -11,4 +11,4 @@ %p = _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.') - if current_user.admin? - = link_to _('Enable usage ping'), admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' + = link_to _('Enable usage ping'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'btn btn-primary' diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml index 8bd5708d490..2cdaa85bdaa 100644 --- a/app/views/layouts/_flash.html.haml +++ b/app/views/layouts/_flash.html.haml @@ -6,5 +6,5 @@ -# Don't show a flash message if the message is nil - if value %div{ class: "flash-#{key}" } - %div{ class: "#{container_class} #{extra_flash_class}" } + %div{ class: "#{(container_class unless fluid_layout)} #{(extra_flash_class unless @no_container)} #{@content_class}" } %span= value diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1420b0a4973..1b2a4cd6780 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -6,12 +6,12 @@ .mobile-overlay .alert-wrapper = render "layouts/broadcast" - = render 'layouts/header/read_only_banner' + = render "layouts/header/read_only_banner" = yield :flash_message = render "shared/ping_consent" - unless @hide_breadcrumbs = render "layouts/nav/breadcrumbs" - = render "layouts/flash" + = render "layouts/flash", extra_flash_class: 'limit-container-width' .d-flex %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } .content{ id: "content-body" } diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 4aa22138498..163556f4509 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -12,7 +12,7 @@ = @group.name %ul.sidebar-top-level-items.qa-group-sidebar - if group_sidebar_link?(:overview) - = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do + = nav_link(path: group_overview_nav_link_paths, html_options: { class: 'home' }) do = link_to group_path(@group) do .nav-icon-container = sprite_icon('home') @@ -36,6 +36,16 @@ %span = _('Activity') + = render_if_exists 'groups/sidebar/security_dashboard' + + - if group_sidebar_link?(:contribution_analytics) + = nav_link(path: 'analytics#show') do + = link_to group_analytics_path(@group), title: 'Contribution Analytics', data: {placement: 'right'} do + %span + Contribution Analytics + + = render_if_exists "layouts/nav/ee/epic_link", group: @group + - if group_sidebar_link?(:issues) = nav_link(path: issues_sub_menu_items) do = link_to issues_group_path(@group) do @@ -132,4 +142,6 @@ %span = _('CI / CD') + = render_if_exists "groups/ee/settings_nav" + = render 'shared/sidebar_toggle_button' diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index 406dccb74fb..e37fd7624be 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,2 +1,2 @@ %span.str-truncated - = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), class: 'tree-commit-link' + = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), title: commit.redacted_full_title_html, class: 'tree-commit-link' diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index e26f5260e5b..c6c5cadc3f5 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -39,14 +39,13 @@ {{ list.issuesSize }} = render_if_exists "shared/boards/components/list_weight" - - if can?(current_user, :admin_list, current_board_parent) - %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button", - "@click" => "showNewIssueForm", - "v-if" => 'list.type !== "closed"', - "aria-label" => _("New issue"), - "title" => _("New issue"), - data: { placement: "top", container: "body" } } - = icon("plus", class: "js-no-trigger-collapse") + %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button", + "@click" => "showNewIssueForm", + "v-if" => "isNewIssueShown", + "aria-label" => _("New issue"), + "title" => _("New issue"), + data: { placement: "top", container: "body" } } + = icon("plus", class: "js-no-trigger-collapse") %board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', ":list" => "list", diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index cb45928d9a5..1d876cc4a5d 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -61,7 +61,10 @@ %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link{ type: 'button' } - = _('No Assignee') + = _('None') + %li.filter-dropdown-item{ data: { value: 'any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') %li.divider.droplab-item-ignore - if current_user = render 'shared/issuable/user_dropdown_item', @@ -81,7 +84,7 @@ %li.filter-dropdown-item{ data: { value: 'upcoming' } } %button.btn.btn-link{ type: 'button' } = _('Upcoming') - %li.filter-dropdown-item{ 'data-value' => 'started' } + %li.filter-dropdown-item{ data: { value: 'started' } } %button.btn.btn-link{ type: 'button' } = _('Started') %li.divider.droplab-item-ignore diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index 09ddf732ada..f6c7ca70ebd 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -9,11 +9,11 @@ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) - %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } + %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = icon('caret-down') .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), display: 'static' } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) = icon("caret-down") diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 09a594cdb4e..72a1733a2a8 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -29,15 +29,14 @@ class PostReceive def process_project_changes(post_received) changes = [] refs = Set.new + @user = post_received.identify - post_received.changes_refs do |oldrev, newrev, ref| - @user ||= post_received.identify(newrev) - - unless @user - log("Triggered hook for non-existing user \"#{post_received.identifier}\"") - return false # rubocop:disable Cop/AvoidReturnFromBlocks - end + unless @user + log("Triggered hook for non-existing user \"#{post_received.identifier}\"") + return false + end + post_received.changes_refs do |oldrev, newrev, ref| if Gitlab::Git.tag_ref?(ref) GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute elsif Gitlab::Git.branch_ref?(ref) diff --git a/bin/secpick b/bin/secpick index 5e30c8e72c5..2b263d452c9 100755 --- a/bin/secpick +++ b/bin/secpick @@ -15,7 +15,7 @@ parser = OptionParser.new do |opts| options[:version] = version&.tr('.', '-') end - opts.on('-b', '--branch security-fix-branch', 'Original branch name') do |branch| + opts.on('-b', '--branch security-fix-branch', 'Original branch name (optional, defaults to current)') do |branch| options[:branch] = branch end @@ -32,15 +32,21 @@ end parser.parse! +options[:branch] ||= `git rev-parse --abbrev-ref HEAD` + abort("Missing options. Use #{$0} --help to see the list of options available".red) if options.values.include?(nil) abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/ -branch = "#{options[:branch]}-#{options[:version]}" +ee = File.exist?('./CHANGELOG-EE.md') +original_branch = options[:branch].strip +branch = "#{original_branch}-#{options[:version]}" branch.prepend("#{BRANCH_PREFIX}-") unless branch.start_with?("#{BRANCH_PREFIX}-") branch = branch.freeze -stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".freeze +stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".tap do |name| + name << "-ee" if ee +end.freeze -command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}" +command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch} && git checkout #{original_branch}" _stdin, stdout, stderr = Open3.popen3(command) @@ -3,6 +3,11 @@ cd $(dirname $0)/.. app_root=$(pwd) +# Switch to experimental PUMA configuration +if [ -n "${EXPERIMENTAL_PUMA}" ]; then + exec bin/web_puma "$@" +fi + unicorn_pidfile="$app_root/tmp/pids/unicorn.pid" unicorn_config="$app_root/config/unicorn.rb" unicorn_cmd="bundle exec unicorn_rails -c $unicorn_config -E $RAILS_ENV" diff --git a/bin/web_puma b/bin/web_puma new file mode 100755 index 00000000000..178fe84800d --- /dev/null +++ b/bin/web_puma @@ -0,0 +1,63 @@ +#!/bin/sh + +set -e + +cd $(dirname $0)/.. +app_root=$(pwd) + +puma_pidfile="$app_root/tmp/pids/puma.pid" +puma_config="$app_root/config/puma.rb" + +spawn_puma() +{ + exec bundle exec puma --config "${puma_config}" "$@" +} + +get_puma_pid() +{ + pid=$(cat "${puma_pidfile}") + if [ -z "$pid" ] ; then + echo "Could not find a PID in $puma_pidfile" + exit 1 + fi + echo "${pid}" +} + +start() +{ + spawn_puma -d +} + +start_foreground() +{ + spawn_puma +} + +stop() +{ + get_puma_pid + kill -QUIT "$(get_puma_pid)" +} + +reload() +{ + kill -USR2 "$(get_puma_pid)" +} + +case "$1" in + start) + start + ;; + start_foreground) + start_foreground + ;; + stop) + stop + ;; + reload) + reload + ;; + *) + echo "Usage: RAILS_ENV=your_env $0 {start|stop|reload}" + ;; +esac diff --git a/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml b/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml new file mode 100644 index 00000000000..f5ed6ccf6df --- /dev/null +++ b/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Add license data to projects endpoint +merge_request: 21606 +author: J.D. Bean (@jdbean) +type: added diff --git a/changelogs/unreleased/34758-create-group-clusters.yml b/changelogs/unreleased/34758-create-group-clusters.yml new file mode 100644 index 00000000000..50efde3cac3 --- /dev/null +++ b/changelogs/unreleased/34758-create-group-clusters.yml @@ -0,0 +1,5 @@ +--- +title: Adds model and migrations to enable group level clusters +merge_request: 22307 +author: +type: other diff --git a/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml b/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml new file mode 100644 index 00000000000..8376fac7abf --- /dev/null +++ b/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml @@ -0,0 +1,5 @@ +--- +title: Fix prometheus graphs in firefox +merge_request: 22400 +author: +type: fixed diff --git a/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml b/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml new file mode 100644 index 00000000000..d58d8da3a0e --- /dev/null +++ b/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml @@ -0,0 +1,5 @@ +--- +title: Adds trace of each access check when git push times out +merge_request: 22265 +author: +type: added diff --git a/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml b/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml new file mode 100644 index 00000000000..6a305099dde --- /dev/null +++ b/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml @@ -0,0 +1,5 @@ +--- +title: No longer require a deploy to start Prometheus monitoring +merge_request: 22401 +author: +type: changed diff --git a/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml b/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml new file mode 100644 index 00000000000..13e3bb66430 --- /dev/null +++ b/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml @@ -0,0 +1,5 @@ +--- +title: Fix inaccessible dropdown for code-less projects +merge_request: 22137 +author: +type: other diff --git a/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml b/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml new file mode 100644 index 00000000000..a042debc28f --- /dev/null +++ b/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml @@ -0,0 +1,5 @@ +--- +title: If user was not found, service hooks won't run on post receive background job +merge_request: 22519 +author: +type: fixed diff --git a/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml b/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml new file mode 100644 index 00000000000..8521335c2ea --- /dev/null +++ b/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml @@ -0,0 +1,5 @@ +--- +title: Link button in markdown editor recognize URLs +merge_request: 1983 +author: Johann Hubert Sonntagbauer +type: changed diff --git a/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml b/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml new file mode 100644 index 00000000000..0efd97d91b8 --- /dev/null +++ b/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml @@ -0,0 +1,5 @@ +--- +title: Renders stuck block when runners are stuck +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml b/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml new file mode 100644 index 00000000000..adf153f33ce --- /dev/null +++ b/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml @@ -0,0 +1,5 @@ +--- +title: Add None/Any option for assignee_id in search bar +merge_request: 22599 +author: Heinrich Lee Yu +type: added diff --git a/changelogs/unreleased/52384-api-filter-assignee-none-any.yml b/changelogs/unreleased/52384-api-filter-assignee-none-any.yml new file mode 100644 index 00000000000..9acec04d946 --- /dev/null +++ b/changelogs/unreleased/52384-api-filter-assignee-none-any.yml @@ -0,0 +1,5 @@ +--- +title: Add None/Any option for assignee_id in Issues and Merge Requests API +merge_request: 22598 +author: Heinrich Lee Yu +type: added diff --git a/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml b/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml new file mode 100644 index 00000000000..5701e44eb32 --- /dev/null +++ b/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml @@ -0,0 +1,5 @@ +--- +title: Always show new issue button in boards' Open list +merge_request: 22557 +author: Heinrich Lee Yu +type: fixed diff --git a/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml b/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml new file mode 100644 index 00000000000..7586d7995b7 --- /dev/null +++ b/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: Cache pipeline status per SHA. +merge_request: 22589 +author: +type: fixed diff --git a/changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml b/changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml new file mode 100644 index 00000000000..ca78f9a392e --- /dev/null +++ b/changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml @@ -0,0 +1,5 @@ +--- +title: Use gitlab_environment for ldap rake task +merge_request: 22582 +author: +type: fixed diff --git a/changelogs/unreleased/53013-duplicate-escape.yml b/changelogs/unreleased/53013-duplicate-escape.yml new file mode 100644 index 00000000000..c5ec2322fb5 --- /dev/null +++ b/changelogs/unreleased/53013-duplicate-escape.yml @@ -0,0 +1,5 @@ +--- +title: Remove duplicate escape in job sidebar +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/53023-endless-scroll-loader-is-visible-on-user-profile-overview-page.yml b/changelogs/unreleased/53023-endless-scroll-loader-is-visible-on-user-profile-overview-page.yml new file mode 100644 index 00000000000..0377e10fe9e --- /dev/null +++ b/changelogs/unreleased/53023-endless-scroll-loader-is-visible-on-user-profile-overview-page.yml @@ -0,0 +1,4 @@ +title: Adds container to pager to enable scoping +merge_request: 22529 +? author +type: other diff --git a/changelogs/unreleased/53055-combine-date-util-functions.yml b/changelogs/unreleased/53055-combine-date-util-functions.yml new file mode 100644 index 00000000000..56d4406f1bf --- /dev/null +++ b/changelogs/unreleased/53055-combine-date-util-functions.yml @@ -0,0 +1,5 @@ +--- +title: Combine all datetime library functions into 'datetime_utility.js' +merge_request: 22570 +author: +type: other diff --git a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml b/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml new file mode 100644 index 00000000000..605d3679159 --- /dev/null +++ b/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml @@ -0,0 +1,5 @@ +--- +title: "fix link to enable usage ping from convdev index" +merge_request: 22545 +author: Anand Capur +type: fixed diff --git a/changelogs/unreleased/53133-jobs-list.yml b/changelogs/unreleased/53133-jobs-list.yml new file mode 100644 index 00000000000..2e13edc0e76 --- /dev/null +++ b/changelogs/unreleased/53133-jobs-list.yml @@ -0,0 +1,5 @@ +--- +title: Fix stage dropdown not rendering in different languages +merge_request: 22604 +author: +type: other diff --git a/changelogs/unreleased/53155-structured-logs-params-array.yml b/changelogs/unreleased/53155-structured-logs-params-array.yml new file mode 100644 index 00000000000..4d4f68a5c84 --- /dev/null +++ b/changelogs/unreleased/53155-structured-logs-params-array.yml @@ -0,0 +1,5 @@ +--- +title: Use key-value pair arrays for API query parameter logging instead of hashes +merge_request: 22623 +author: +type: other diff --git a/changelogs/unreleased/ac-post-merge-pipeline.yml b/changelogs/unreleased/ac-post-merge-pipeline.yml new file mode 100644 index 00000000000..08322c9cb8a --- /dev/null +++ b/changelogs/unreleased/ac-post-merge-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Show post-merge pipeline in merge request page +merge_request: 22292 +author: +type: added diff --git a/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml b/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml new file mode 100644 index 00000000000..c8488cbf200 --- /dev/null +++ b/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml @@ -0,0 +1,5 @@ +--- +title: Add failure reason for execution timeout +merge_request: 22224 +author: +type: changed diff --git a/changelogs/unreleased/an-multithreading.yml b/changelogs/unreleased/an-multithreading.yml new file mode 100644 index 00000000000..fca847e6ea4 --- /dev/null +++ b/changelogs/unreleased/an-multithreading.yml @@ -0,0 +1,5 @@ +--- +title: Experimental support for running Puma multithreaded web-server +merge_request: 22372 +author: +type: performance diff --git a/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml b/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml new file mode 100644 index 00000000000..30b9ae032d4 --- /dev/null +++ b/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml @@ -0,0 +1,5 @@ +--- +title: Support backward compatibility when introduce new failure reason +merge_request: 22566 +author: +type: changed diff --git a/changelogs/unreleased/bvl-preload-user-status-for-events.yml b/changelogs/unreleased/bvl-preload-user-status-for-events.yml new file mode 100644 index 00000000000..e13b19b19c1 --- /dev/null +++ b/changelogs/unreleased/bvl-preload-user-status-for-events.yml @@ -0,0 +1,5 @@ +--- +title: Show user status for label events in system notes +merge_request: 22609 +author: +type: fixed diff --git a/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml b/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml new file mode 100644 index 00000000000..836b9aa21c5 --- /dev/null +++ b/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml @@ -0,0 +1,5 @@ +--- +title: Automatically navigate to last board visited +merge_request: 22430 +author: +type: changed diff --git a/changelogs/unreleased/gt-add-transparent-background-to-markdown-header-tabs.yml b/changelogs/unreleased/gt-add-transparent-background-to-markdown-header-tabs.yml new file mode 100644 index 00000000000..2ba52e07324 --- /dev/null +++ b/changelogs/unreleased/gt-add-transparent-background-to-markdown-header-tabs.yml @@ -0,0 +1,5 @@ +--- +title: Add transparent background to markdown header tabs +merge_request: 22565 +author: George Tsiolis +type: fixed diff --git a/changelogs/unreleased/jramsay-42673-commit-tooltip.yml b/changelogs/unreleased/jramsay-42673-commit-tooltip.yml new file mode 100644 index 00000000000..083cd1a54a0 --- /dev/null +++ b/changelogs/unreleased/jramsay-42673-commit-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Add commit message to commit tree anchor title +merge_request: 22585 +author: +type: fixed diff --git a/changelogs/unreleased/mr-file-list.yml b/changelogs/unreleased/mr-file-list.yml new file mode 100644 index 00000000000..0a2a5e0c1cc --- /dev/null +++ b/changelogs/unreleased/mr-file-list.yml @@ -0,0 +1,5 @@ +--- +title: Switch between tree list & file list in diffs file browser +merge_request: 22191 +author: +type: added diff --git a/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml b/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml new file mode 100644 index 00000000000..d0c8ed8001d --- /dev/null +++ b/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Prometheus to 2.4.3 and Alertmanager to 0.15.2 +merge_request: 22600 +author: +type: other diff --git a/changelogs/unreleased/rz_fix_milestone_count.yml b/changelogs/unreleased/rz_fix_milestone_count.yml new file mode 100644 index 00000000000..1013b88e0bc --- /dev/null +++ b/changelogs/unreleased/rz_fix_milestone_count.yml @@ -0,0 +1,5 @@ +--- +title: Fixing count on Milestones +merge_request: 21446 +author: +type: fixed diff --git a/changelogs/unreleased/update-runner-chart-to-0-1-35.yml b/changelogs/unreleased/update-runner-chart-to-0-1-35.yml new file mode 100644 index 00000000000..3b8029c8d96 --- /dev/null +++ b/changelogs/unreleased/update-runner-chart-to-0-1-35.yml @@ -0,0 +1,5 @@ +--- +title: Update used version of Runner Helm Chart to 0.1.35 +merge_request: 22541 +author: +type: other diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb index 146c4b1e024..8052880cc3d 100644 --- a/config/initializers/7_prometheus_metrics.rb +++ b/config/initializers/7_prometheus_metrics.rb @@ -26,9 +26,25 @@ Sidekiq.configure_server do |config| end if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled? - unless Sidekiq.server? - Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start + Gitlab::Cluster::LifecycleEvents.on_worker_start do + defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change + + unless Sidekiq.server? + Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start + end + + Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start end +end - Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start +Gitlab::Cluster::LifecycleEvents.on_master_restart do + # The following is necessary to ensure stale Prometheus metrics don't + # accumulate over time. It needs to be done in this hook as opposed to + # inside an init script to ensure metrics files aren't deleted after new + # unicorn workers start after a SIGUSR2 is received. + prometheus_multiproc_dir = ENV['prometheus_multiproc_dir'] + if prometheus_multiproc_dir + old_metrics = Dir[File.join(prometheus_multiproc_dir, '*.db')] + FileUtils.rm_rf(old_metrics) + end end diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb index eccf82ab8dc..c8d261d415e 100644 --- a/config/initializers/8_metrics.rb +++ b/config/initializers/8_metrics.rb @@ -158,7 +158,9 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? GC::Profiler.enable - Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start + Gitlab::Cluster::LifecycleEvents.on_worker_start do + Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start + end module TrackNewRedisConnections def connect(*args) diff --git a/config/initializers/active_record_lifecycle.rb b/config/initializers/active_record_lifecycle.rb new file mode 100644 index 00000000000..7fa37121efc --- /dev/null +++ b/config/initializers/active_record_lifecycle.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Don't handle sidekiq configuration as it +# has its own special active record configuration here +if defined?(ActiveRecord::Base) && !Sidekiq.server? + Gitlab::Cluster::LifecycleEvents.on_worker_start do + ActiveSupport.on_load(:active_record) do + ActiveRecord::Base.establish_connection + + Rails.logger.debug("ActiveRecord connection established") + end + end +end + +if defined?(ActiveRecord::Base) + Gitlab::Cluster::LifecycleEvents.on_before_fork do + # the following is highly recommended for Rails + "preload_app true" + # as there's no need for the master process to hold a connection + ActiveRecord::Base.connection.disconnect! + + Rails.logger.debug("ActiveRecord connection disconnected") + end +end diff --git a/config/initializers/macos.rb b/config/initializers/macos.rb new file mode 100644 index 00000000000..f410af6ed47 --- /dev/null +++ b/config/initializers/macos.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +if /darwin/ =~ RUBY_PLATFORM + Gitlab::Cluster::LifecycleEvents.on_before_fork do + require 'fiddle' + + # Dynamically load Foundation.framework, ~implicitly~ initialising + # the Objective-C runtime before any forking happens in Unicorn + # + # From https://bugs.ruby-lang.org/issues/14009 + Fiddle.dlopen '/System/Library/Frameworks/Foundation.framework/Foundation' + end +end diff --git a/config/initializers/rbtrace.rb b/config/initializers/rbtrace.rb new file mode 100644 index 00000000000..6a1b71bf4bd --- /dev/null +++ b/config/initializers/rbtrace.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +if ENV['ENABLE_RBTRACE'] + Gitlab::Cluster::LifecycleEvents.on_worker_start do + # Unicorn clears out signals before it forks, so rbtrace won't work + # unless it is enabled after the fork. + require 'rbtrace' + end +end diff --git a/config/initializers/routing_draw.rb b/config/initializers/routing_draw.rb index 25003cf0239..f0f74954eef 100644 --- a/config/initializers/routing_draw.rb +++ b/config/initializers/routing_draw.rb @@ -1,7 +1,3 @@ # Adds draw method into Rails routing -# It allows us to keep routing splitted into files -class ActionDispatch::Routing::Mapper - def draw(routes_name) - instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb"))) - end -end +# It allows us to keep routing split into files +ActionDispatch::Routing::Mapper.prepend Gitlab::Patch::DrawRoute diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index bc6b7aed6aa..565efc858d1 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -14,8 +14,6 @@ Sidekiq.default_worker_options = { retry: 3 } enable_json_logs = Gitlab.config.sidekiq.log_format == 'json' Sidekiq.configure_server do |config| - require 'rbtrace' if ENV['ENABLE_RBTRACE'] - config.redis = queues_config_hash config.server_middleware do |chain| diff --git a/config/puma.example.development.rb b/config/puma.example.development.rb new file mode 100644 index 00000000000..490c940077a --- /dev/null +++ b/config/puma.example.development.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# ----------------------------------------------------------------------- +# This file is used by the GDK to generate a default config/puma.rb file +# Note that `/home/git` will be substituted for the actual GDK root +# directory when this file is generated +# ----------------------------------------------------------------------- + +# Load "path" as a rackup file. +# +# The default is "config.ru". +# +rackup 'config.ru' +pidfile '/home/git/gitlab/tmp/pids/puma.pid' +state_path '/home/git/gitlab/tmp/pids/puma.state' + +stdout_redirect '/home/git/gitlab/log/puma.stdout.log', + '/home/git/gitlab/log/puma.stderr.log', + true + +# Configure "min" to be the minimum number of threads to use to answer +# requests and "max" the maximum. +# +# The default is "0, 16". +# +threads 1, 4 + +# By default, workers accept all requests and queue them to pass to handlers. +# When false, workers accept the number of simultaneous requests configured. +# +# Queueing requests generally improves performance, but can cause deadlocks if +# the app is waiting on a request to itself. See https://github.com/puma/puma/issues/612 +# +# When set to false this may require a reverse proxy to handle slow clients and +# queue requests before they reach puma. This is due to disabling HTTP keepalive +queue_requests false + +# Bind the server to "url". "tcp://", "unix://" and "ssl://" are the only +# accepted protocols. +bind 'unix:///home/git/gitlab.socket' + +workers 2 + +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" +require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer" + +on_restart do + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart +end + +before_fork do + # Signal to the puma killer + Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options unless ENV['DISABLE_PUMA_WORKER_KILLER'] + + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options +on_worker_boot do + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start +end + +# Preload the application before starting the workers; this conflicts with +# phased restart feature. (off by default) + +preload_app! + +tag 'gitlab-puma-worker' + +# Verifies that all workers have checked in to the master process within +# the given timeout. If not the worker process will be restarted. Default +# value is 60 seconds. +# +worker_timeout 60 diff --git a/config/routes.rb b/config/routes.rb index c081ca9672a..37c7f98ec98 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,6 +34,8 @@ Rails.application.routes.draw do match '*all', via: [:get, :post], to: proc { [404, {}, ['']] } end + draw :oauth + use_doorkeeper_openid_connect # Autocomplete @@ -78,6 +80,7 @@ Rails.application.routes.draw do get 'ide' => 'ide#index' get 'ide/*vueroute' => 'ide#index', format: false + draw :operations draw :instance_statistics end diff --git a/config/routes/admin.rb b/config/routes/admin.rb index fb29c4748c1..af333bdc748 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -71,6 +71,7 @@ namespace :admin do resource :logs, only: [:show] resource :health_check, controller: 'health_check', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] + resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } @@ -104,6 +105,7 @@ namespace :admin do resource :application_settings, only: [:show, :update] do resources :services, only: [:index, :edit, :update] + get :usage_data put :reset_registration_token put :reset_health_check_token diff --git a/config/routes/group.rb b/config/routes/group.rb index 602bbe837cf..2328b50b760 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + resources :groups, only: [:index, :new, :create] do post :preview_markdown end @@ -63,7 +65,6 @@ constraints(::Constraints::GroupUrlConstrainer.new) do end end - # On CE only index and show actions are needed resources :boards, only: [:index, :show] resources :runners, only: [:index, :edit, :update, :destroy, :show] do diff --git a/config/routes/project.rb b/config/routes/project.rb index 9cbd5b644f6..85872a4122a 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -178,6 +178,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resource :mirror, only: [:show, :update] do member do + get :ssh_host_keys, constraints: { format: :json } post :update_now end end diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index e06cce3e97a..4637eb8bc6e 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -81,22 +81,16 @@ preload_app true # fast LAN. check_client_connection false +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" + before_exec do |server| - # The following is necessary to ensure stale Prometheus metrics don't - # accumulate over time. It needs to be done in this hook as opposed to - # inside an init script to ensure metrics files aren't deleted after new - # unicorn workers start after a SIGUSR2 is received. - if ENV['prometheus_multiproc_dir'] - old_metrics = Dir[File.join(ENV['prometheus_multiproc_dir'], '*.db')] - FileUtils.rm_rf(old_metrics) - end + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart end before_fork do |server, worker| - # the following is highly recommended for Rails + "preload_app true" - # as there's no need for the master process to hold a connection - defined?(ActiveRecord::Base) && - ActiveRecord::Base.connection.disconnect! + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork # The following is only recommended for memory/DB-constrained # installations. It is not needed if your system can house @@ -124,25 +118,10 @@ before_fork do |server, worker| end after_fork do |server, worker| - # Unicorn clears out signals before it forks, so rbtrace won't work - # unless it is enabled after the fork. - require 'rbtrace' if ENV['ENABLE_RBTRACE'] + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start # per-process listener ports for debugging/admin/migrations # addr = "127.0.0.1:#{9293 + worker.nr}" # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true) - - # the following is *required* for Rails + "preload_app true", - defined?(ActiveRecord::Base) && - ActiveRecord::Base.establish_connection - - # reset prometheus client, this will cause any opened metrics files to be closed - defined?(::Prometheus::Client.reinitialize_on_pid_change) && - Prometheus::Client.reinitialize_on_pid_change - - # if preload_app is true, then you may also want to check and - # restart any other shared sockets/descriptors such as Memcached, - # and Redis. TokyoCabinet file handles are safe to reuse - # between any number of forked children (assuming your kernel - # correctly implements pread()/pwrite() system calls) end diff --git a/config/unicorn.rb.example.development b/config/unicorn.rb.example.development index f31df66015a..f7541bb9d55 100644 --- a/config/unicorn.rb.example.development +++ b/config/unicorn.rb.example.development @@ -1,32 +1,61 @@ +# frozen_string_literal: true + +# ------------------------------------------------------------------------- +# This file is used by the GDK to generate a default config/unicorn.rb file +# Note that `/home/git` will be substituted for the actual GDK root +# directory when this file is generated +# ------------------------------------------------------------------------- + worker_processes 2 timeout 60 +listen '/home/git/gitlab.socket' + preload_app true check_client_connection false +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" + +before_exec do |server| + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart +end + before_fork do |server, worker| - # the following is highly recommended for Rails + "preload_app true" - # as there's no need for the master process to hold a connection - defined?(ActiveRecord::Base) && - ActiveRecord::Base.connection.disconnect! - - if /darwin/ =~ RUBY_PLATFORM - require 'fiddle' - - # Dynamically load Foundation.framework, ~implicitly~ initialising - # the Objective-C runtime before any forking happens in Unicorn - # - # From https://bugs.ruby-lang.org/issues/14009 - Fiddle.dlopen '/System/Library/Frameworks/Foundation.framework/Foundation' + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork + + # The following is only recommended for memory/DB-constrained + # installations. It is not needed if your system can house + # twice as many worker_processes as you have configured. + # + # This allows a new master process to incrementally + # phase out the old master process with SIGTTOU to avoid a + # thundering herd (especially in the "preload_app false" case) + # when doing a transparent upgrade. The last worker spawned + # will then kill off the old master process with a SIGQUIT. + old_pid = "#{server.config[:pid]}.oldbin" + if old_pid != server.pid + begin + sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU + Process.kill(sig, File.read(old_pid).to_i) + rescue Errno::ENOENT, Errno::ESRCH + end end + # + # Throttle the master from forking too quickly by sleeping. Due + # to the implementation of standard Unix signal handlers, this + # helps (but does not completely) prevent identical, repeated signals + # from being lost when the receiving process is busy. + # sleep 1 end after_fork do |server, worker| - # Unicorn clears out signals before it forks, so rbtrace won't work - # unless it is enabled after the fork. - require 'rbtrace' if ENV['ENABLE_RBTRACE'] + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start - # the following is *required* for Rails + "preload_app true", - defined?(ActiveRecord::Base) && - ActiveRecord::Base.establish_connection + # per-process listener ports for debugging/admin/migrations + # addr = "127.0.0.1:#{9293 + worker.nr}" + # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true) end + diff --git a/danger/specs/Dangerfile b/danger/specs/Dangerfile index 97188df8785..a526bb8adaa 100644 --- a/danger/specs/Dangerfile +++ b/danger/specs/Dangerfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + NO_SPECS_LABELS = %w[backstage Documentation QA].freeze NO_NEW_SPEC_MESSAGE = <<~MSG.freeze You've made some app changes, but didn't add any tests. @@ -9,8 +11,8 @@ def presented_no_changelog_labels NO_SPECS_LABELS.map { |label| "~#{label}" }.join(', ') end -has_app_changes = !git.modified_files.grep(%r{\A(ee/)?(app|lib|db/(geo/)?(post_)?migrate)/}).empty? -has_spec_changes = !git.modified_files.grep(%r{\A(ee/)?spec/}).empty? +has_app_changes = !helper.all_changed_files.grep(%r{\A(ee/)?(app|lib|db/(geo/)?(post_)?migrate)/}).empty? +has_spec_changes = !helper.all_changed_files.grep(%r{\A(ee/)?spec/}).empty? new_specs_needed = (gitlab.mr_labels & NO_SPECS_LABELS).empty? if has_app_changes && !has_spec_changes && new_specs_needed diff --git a/db/migrate/20181010235606_create_board_project_recent_visits.rb b/db/migrate/20181010235606_create_board_project_recent_visits.rb new file mode 100644 index 00000000000..426f41e202a --- /dev/null +++ b/db/migrate/20181010235606_create_board_project_recent_visits.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class CreateBoardProjectRecentVisits < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :board_project_recent_visits, id: :bigserial do |t| + t.timestamps_with_timezone null: false + + t.references :user, index: true, foreign_key: { on_delete: :cascade } + t.references :project, index: true, foreign_key: { on_delete: :cascade } + t.references :board, index: true, foreign_key: { on_delete: :cascade } + end + + add_index :board_project_recent_visits, [:user_id, :project_id, :board_id], unique: true, name: 'index_board_project_recent_visits_on_user_project_and_board' + end +end diff --git a/db/migrate/20181014203236_create_cluster_groups.rb b/db/migrate/20181014203236_create_cluster_groups.rb new file mode 100644 index 00000000000..69382d5c851 --- /dev/null +++ b/db/migrate/20181014203236_create_cluster_groups.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateClusterGroups < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :cluster_groups do |t| + t.references :cluster, null: false, foreign_key: { on_delete: :cascade } + t.references :group, null: false, index: true + + t.index [:cluster_id, :group_id], unique: true + t.foreign_key :namespaces, column: :group_id, on_delete: :cascade + end + end +end diff --git a/db/migrate/20181016152238_create_board_group_recent_visits.rb b/db/migrate/20181016152238_create_board_group_recent_visits.rb new file mode 100644 index 00000000000..1e55dc8658e --- /dev/null +++ b/db/migrate/20181016152238_create_board_group_recent_visits.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class CreateBoardGroupRecentVisits < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :board_group_recent_visits, id: :bigserial do |t| + t.timestamps_with_timezone null: false + + t.references :user, index: true, foreign_key: { on_delete: :cascade } + t.references :board, index: true, foreign_key: { on_delete: :cascade } + t.references :group, references: :namespace, column: :group_id, index: true + t.foreign_key :namespaces, column: :group_id, on_delete: :cascade + end + + add_index :board_group_recent_visits, [:user_id, :group_id, :board_id], unique: true, name: 'index_board_group_recent_visits_on_user_group_and_board' + end +end diff --git a/db/migrate/20181017001059_add_cluster_type_to_clusters.rb b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb new file mode 100644 index 00000000000..191e7eb4fb3 --- /dev/null +++ b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddClusterTypeToClusters < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + PROJECT_CLUSTER_TYPE = 3 + + disable_ddl_transaction! + + def up + add_column_with_default(:clusters, :cluster_type, :smallint, default: PROJECT_CLUSTER_TYPE) + end + + def down + remove_column(:clusters, :cluster_type) + end +end diff --git a/db/schema.rb b/db/schema.rb index ddfccbba678..a36399cb37e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181013005024) do +ActiveRecord::Schema.define(version: 20181017001059) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -204,6 +204,32 @@ ActiveRecord::Schema.define(version: 20181013005024) do add_index "badges", ["group_id"], name: "index_badges_on_group_id", using: :btree add_index "badges", ["project_id"], name: "index_badges_on_project_id", using: :btree + create_table "board_group_recent_visits", id: :bigserial, force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "user_id" + t.integer "board_id" + t.integer "group_id" + end + + add_index "board_group_recent_visits", ["board_id"], name: "index_board_group_recent_visits_on_board_id", using: :btree + add_index "board_group_recent_visits", ["group_id"], name: "index_board_group_recent_visits_on_group_id", using: :btree + add_index "board_group_recent_visits", ["user_id", "group_id", "board_id"], name: "index_board_group_recent_visits_on_user_group_and_board", unique: true, using: :btree + add_index "board_group_recent_visits", ["user_id"], name: "index_board_group_recent_visits_on_user_id", using: :btree + + create_table "board_project_recent_visits", id: :bigserial, force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "user_id" + t.integer "project_id" + t.integer "board_id" + end + + add_index "board_project_recent_visits", ["board_id"], name: "index_board_project_recent_visits_on_board_id", using: :btree + add_index "board_project_recent_visits", ["project_id"], name: "index_board_project_recent_visits_on_project_id", using: :btree + add_index "board_project_recent_visits", ["user_id", "project_id", "board_id"], name: "index_board_project_recent_visits_on_user_project_and_board", unique: true, using: :btree + add_index "board_project_recent_visits", ["user_id"], name: "index_board_project_recent_visits_on_user_id", using: :btree + create_table "boards", force: :cascade do |t| t.integer "project_id" t.datetime "created_at", null: false @@ -573,6 +599,14 @@ ActiveRecord::Schema.define(version: 20181013005024) do add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree + create_table "cluster_groups", force: :cascade do |t| + t.integer "cluster_id", null: false + t.integer "group_id", null: false + end + + add_index "cluster_groups", ["cluster_id", "group_id"], name: "index_cluster_groups_on_cluster_id_and_group_id", unique: true, using: :btree + add_index "cluster_groups", ["group_id"], name: "index_cluster_groups_on_group_id", using: :btree + create_table "cluster_platforms_kubernetes", force: :cascade do |t| t.integer "cluster_id", null: false t.datetime_with_timezone "created_at", null: false @@ -628,6 +662,7 @@ ActiveRecord::Schema.define(version: 20181013005024) do t.boolean "enabled", default: true t.string "name", null: false t.string "environment_scope", default: "*", null: false + t.integer "cluster_type", limit: 2, default: 3, null: false end add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree @@ -2306,6 +2341,12 @@ ActiveRecord::Schema.define(version: 20181013005024) do add_foreign_key "application_settings", "users", column: "usage_stats_set_by_user_id", name: "fk_964370041d", on_delete: :nullify add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "badges", "projects", on_delete: :cascade + add_foreign_key "board_group_recent_visits", "boards", on_delete: :cascade + add_foreign_key "board_group_recent_visits", "namespaces", column: "group_id", on_delete: :cascade + add_foreign_key "board_group_recent_visits", "users", on_delete: :cascade + add_foreign_key "board_project_recent_visits", "boards", on_delete: :cascade + add_foreign_key "board_project_recent_visits", "projects", on_delete: :cascade + add_foreign_key "board_project_recent_visits", "users", on_delete: :cascade add_foreign_key "boards", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade add_foreign_key "chat_teams", "namespaces", on_delete: :cascade @@ -2340,6 +2381,8 @@ ActiveRecord::Schema.define(version: 20181013005024) do add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade + add_foreign_key "cluster_groups", "clusters", on_delete: :cascade + add_foreign_key "cluster_groups", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade add_foreign_key "cluster_projects", "clusters", on_delete: :cascade add_foreign_key "cluster_projects", "projects", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index 03371226041..20fcd2e1724 100644 --- a/doc/README.md +++ b/doc/README.md @@ -165,6 +165,7 @@ configuration. Then customize everything from buildpacks to CI/CD. - [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications) - [Protected variables](ci/variables/README.md#protected-variables) - [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab) +- [Executable Runbooks](user/project/clusters/runbooks/index.md) ### Monitor diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 96803746637..481eb692674 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -61,7 +61,7 @@ on an Linux NFS server, do the following: 2. Restart the NFS server process. For example, on CentOS run `service nfs restart`. -## AWS Elastic File System +## Avoid using AWS's Elastic File System (EFS) GitLab strongly recommends against using AWS Elastic File System (EFS). Our support team will not be able to assist on performance issues related to @@ -78,6 +78,23 @@ stored on a local volume. For more details on another person's experience with EFS, see [Amazon's Elastic File System: Burst Credits](https://rawkode.com/2017/04/16/amazons-elastic-file-system-burst-credits/) +## Avoid using PostgreSQL with NFS + +GitLab strongly recommends against running your PostgreSQL database +across NFS. The GitLab support team will not be able to assist on performance issues related to +this configuration. + +Additionally, this configuration is specifically warned against in the +[Postgres Documentation](https://www.postgresql.org/docs/current/static/creating-cluster.html#CREATING-CLUSTER-NFS): + +>PostgreSQL does nothing special for NFS file systems, meaning it assumes NFS behaves exactly like +>locally-connected drives. If the client or server NFS implementation does not provide standard file +>system semantics, this can cause reliability problems. Specifically, delayed (asynchronous) writes +>to the NFS server can cause data corruption problems. + +For supported database architecture, please see our documentation on +[Configuring a Database for GitLab HA](https://docs.gitlab.com/ee/administration/high_availability/database.html). + ## NFS Client mount options Below is an example of an NFS mount point defined in `/etc/fstab` we use on diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index b5d1ff698c6..dcee57def74 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -15,7 +15,7 @@ Omnibus GitLab packages. > **Notes:** > - Redis requires authentication for High Availability. See -> [Redis Security](http://redis.io/topics/security) documentation for more +> [Redis Security](https://redis.io/topics/security) documentation for more > information. We recommend using a combination of a Redis password and tight > firewall rules to secure your Redis service. > - You are highly encouraged to read the [Redis Sentinel][sentinel] documentation @@ -82,7 +82,7 @@ When a **Master** fails to respond, it's the application's responsibility for a new **Master**). To get a better understanding on how to correctly set up Sentinel, please read -the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as +the [Redis Sentinel documentation](https://redis.io/topics/sentinel) first, as failing to configure it correctly can lead to data loss or can bring your whole cluster down, invalidating the failover effort. @@ -885,8 +885,8 @@ Read more on High Availability: [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [gh-531]: https://github.com/redis/redis-rb/issues/531 [gh-534]: https://github.com/redis/redis-rb/issues/534 -[redis]: http://redis.io/ -[sentinel]: http://redis.io/topics/sentinel +[redis]: https://redis.io/ +[sentinel]: https://redis.io/topics/sentinel [omnifile]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb [source]: ../../install/installation.md [ce]: https://about.gitlab.com/downloads diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md index 5823c575251..2101d36d2b6 100644 --- a/doc/administration/high_availability/redis_source.md +++ b/doc/administration/high_availability/redis_source.md @@ -107,7 +107,7 @@ starting with `sentinel` prefix. Assuming that the Redis Sentinel is installed on the same instance as Redis master with IP `10.0.0.1` (some settings might overlap with the master): -1. [Install Redis Sentinel](http://redis.io/topics/sentinel) +1. [Install Redis Sentinel](https://redis.io/topics/sentinel) 1. Edit `/etc/redis/sentinel.conf`: ```conf @@ -363,7 +363,7 @@ production: port: 26379 # point to sentinel, not to redis port ``` -When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel). +When in doubt, please read [Redis Sentinel documentation](https://redis.io/topics/sentinel). [gh-531]: https://github.com/redis/redis-rb/issues/531 [downloads]: https://about.gitlab.com/downloads diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 0e41a38b7e2..038e043281c 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -84,7 +84,7 @@ Introduced in GitLab 10.0, this file lives in It helps you see requests made directly to the API. For example: ```json -{"time":"2017-10-10T12:30:11.579Z","severity":"INFO","duration":16.84,"db":1.57,"view":15.27,"status":200,"method":"POST","path":"/api/v4/internal/allowed","params":{"action":"git-upload-pack","changes":"_any","gl_repository":null,"project":"root/foobar.git","protocol":"ssh","env":"{}","key_id":"[FILTERED]","secret_token":"[FILTERED]"},"host":"127.0.0.1","ip":"127.0.0.1","ua":"Ruby"} +{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30} ``` This entry above shows an access to an internal endpoint to check whether an diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 2952a98626a..d8345f2d6bd 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -242,6 +242,33 @@ verification requirement. Navigate to `Admin area ➔ Settings` and uncheck **Require users to prove ownership of custom domains** in the Pages section. This setting is enabled by default. +### Access control + +Access control was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/33422) +in GitLab 11.5. It can be configured per-project, and allows access to a Pages +site to be controlled based on a user's membership to that project. + +Access control works by registering the Pages daemon as an OAuth application +with GitLab. Whenever a request to access a private Pages site is made by an +unauthenticated user, the Pages daemon redirects the user to GitLab. If +authentication is successful, the user is redirected back to Pages with a token, +which is persisted in a cookie. The cookies are signed with a secret key, so +tampering can be detected. + +Each request to view a resource in a private site is authenticated by Pages +using that token. For each request it receives, it makes a request to the GitLab +API to check that the user is authorized to read that site. + +Pages access control is currently disabled by default. To enable it, you must: + +1. Enable it in `/etc/gitlab/gitlab.rb` + + ```ruby + gitlab_pages['access_control'] = true + ``` + +1. [Reconfigure GitLab][reconfigure] + ## Activate verbose logging for daemon Verbose logging was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2533) in diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index 295905a7625..ddff54be575 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -391,6 +391,44 @@ the first one with a backslash (\). For example `pages.example.io` would be: server_name ~^.*\.pages\.example\.io$; ``` +## Access control + +Access control was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/33422) +in GitLab 11.5. It can be configured per-project, and allows access to a Pages +site to be controlled based on a user's membership to that project. + +Access control works by registering the Pages daemon as an OAuth application +with GitLab. Whenever a request to access a private Pages site is made by an +unauthenticated user, the Pages daemon redirects the user to GitLab. If +authentication is successful, the user is redirected back to Pages with a token, +which is persisted in a cookie. The cookies are signed with a secret key, so +tampering can be detected. + +Each request to view a resource in a private site is authenticated by Pages +using that token. For each request it receives, it makes a request to the GitLab +API to check that the user is authorized to read that site. + +Pages access control is currently disabled by default. To enable it, you must: + +1. Modify your `config/gitlab.yml` file: + ```yaml + pages: + access_control: true + ``` +1. [Restart GitLab][restart] +1. Create a new [system OAuth application](../../integration/oauth_provider.md#adding-an-application-through-the-profile) + This should be called `GitLab Pages` and have a `Redirect URL` of + `https://projects.example.io/auth`. It does not need to be a "trusted" + application, but it does need the "api" scope. +1. Start the Pages daemon with the following additional arguments: + + ```shell + -auth-client-secret <OAuth code generated by GitLab> \ + -auth-redirect-uri http://projects.example.io/auth \ + -auth-secret <40 random hex characters> \ + -auth-server <URL of the GitLab instance> + ``` + ## Change storage path Follow the steps below to change the default path where GitLab Pages' contents diff --git a/doc/api/issues.md b/doc/api/issues.md index cc1d6834a20..57e861bc62e 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -40,7 +40,7 @@ GET /issues?my_reaction_emoji=star | `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone. `Any+Milestone` lists all issues that have an assigned milestone | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | @@ -154,7 +154,7 @@ GET /groups/:id/issues?my_reaction_emoji=star | `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | @@ -268,7 +268,7 @@ GET /projects/:id/issues?my_reaction_emoji=star | `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 862ee398a84..0291b7e00c2 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -42,7 +42,7 @@ Parameters: | `updated_before` | datetime | no | Return merge requests updated on or before the given time | | `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. | | `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` | -| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` | +| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `source_branch` | string | no | Return merge requests with the given source branch | | `target_branch` | string | no | Return merge requests with the given target branch | @@ -166,7 +166,7 @@ Parameters: | `updated_before` | datetime | no | Return merge requests updated on or before the given time | | `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `source_branch` | string | no | Return merge requests with the given source branch | | `target_branch` | string | no | Return merge requests with the given target branch | @@ -279,7 +279,7 @@ Parameters: | `updated_before` | datetime | no | Return merge requests updated on or before the given time | | `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> | | `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `source_branch` | string | no | Return merge requests with the given source branch | | `target_branch` | string | no | Return merge requests with the given target branch | diff --git a/doc/api/projects.md b/doc/api/projects.md index 947e7db9c52..961241f31e1 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -451,6 +451,7 @@ GET /projects/:id | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `statistics` | boolean | no | Include project statistics | +| `license` | boolean | no | Include project license data | | `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | ```json @@ -508,6 +509,14 @@ GET /projects/:id }, "archived": false, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE", + "license": { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "nickname": "GNU LGPLv3", + "html_url": "http://choosealicense.com/licenses/lgpl-3.0/", + "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt" + }, "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, @@ -572,6 +581,14 @@ If the project is a fork, and you provide a valid token to authenticate, the "http_url_to_repo":"https://gitlab.com/gitlab-org/gitlab-ce.git", "web_url":"https://gitlab.com/gitlab-org/gitlab-ce", "avatar_url":"https://assets.gitlab-static.net/uploads/-/system/project/avatar/13083/logo-extra-whitespace.png", + "license_url": "https://gitlab.com/gitlab-org/gitlab-ce/blob/master/LICENSE", + "license": { + "key": "mit", + "name": "MIT License", + "nickname": null, + "html_url": "http://choosealicense.com/licenses/mit/", + "source_url": "https://opensource.org/licenses/MIT", + }, "star_count":3812, "forks_count":3561, "last_activity_at":"2018-01-02T11:40:26.570Z", @@ -905,6 +922,14 @@ Example response: "import_status": "none", "archived": true, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE", + "license": { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "nickname": "GNU LGPLv3", + "html_url": "http://choosealicense.com/licenses/lgpl-3.0/", + "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt" + }, "shared_runners_enabled": true, "forks_count": 0, "star_count": 1, @@ -983,6 +1008,14 @@ Example response: "import_status": "none", "archived": true, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE", + "license": { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "nickname": "GNU LGPLv3", + "html_url": "http://choosealicense.com/licenses/lgpl-3.0/", + "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt" + }, "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, @@ -1101,6 +1134,14 @@ Example response: }, "archived": true, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE", + "license": { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "nickname": "GNU LGPLv3", + "html_url": "http://choosealicense.com/licenses/lgpl-3.0/", + "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt" + }, "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, @@ -1197,6 +1238,14 @@ Example response: }, "archived": false, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE", + "license": { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "nickname": "GNU LGPLv3", + "html_url": "http://choosealicense.com/licenses/lgpl-3.0/", + "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt" + }, "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, diff --git a/doc/api/tags.md b/doc/api/tags.md index f2a3f9f28d2..826900ca518 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -134,7 +134,7 @@ Parameters: "description": "Amazing release. Wow" }, "name": "v1.0.0", - "target: "2695effb5807a22ff3d138d593fd856244e155e7", + "target": "2695effb5807a22ff3d138d593fd856244e155e7", "message": null } ``` diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md index b2c73caae2e..c0346d78141 100644 --- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md @@ -398,10 +398,10 @@ other reasons][ci-reasons] to keep using GitLab CI/CD. The benefits to our teams - [Using Docker images documentation][using-docker] - [Example project: Hello GitLab CI/CD on GitLab][hello-gitlab] -[phoenix-site]: http://phoenixframework.org/ "Phoenix Framework" +[phoenix-site]: https://phoenixframework.org/ "Phoenix Framework" [phoenix-learning-guide]: https://hexdocs.pm/phoenix/learning.html "Phoenix Learning Guide" -[phoenix-install]: http://www.phoenixframework.org/docs/installation "Phoenix Installation" -[phoenix-mysql]: http://www.phoenixframework.org/docs/using-mysql "Phoenix with MySQL" +[phoenix-install]: https://hexdocs.pm/phoenix/installation.html "Phoenix Installation" +[phoenix-mysql]: https://hexdocs.pm/phoenix/ecto.html#using-mysql "Phoenix with MySQL" [elixir-site]: http://elixir-lang.org/ "Elixir" [elixir-mix]: http://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html "Introduction to mix" [elixir-docs]: http://elixir-lang.org/getting-started/introduction.html "Elixir Documentation" diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 83e0fa34ad6..2a179bfbbf0 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -312,7 +312,7 @@ We're always looking for contributions that can mitigate these If you think that registration token for a Project was revealed, you should reset them. It's recommended because such token can be used to register another -Runner to thi Project. It may be next used to obtain the values of secret +Runner to the Project. It may be next used to obtain the values of secret variables or clone the project code, that normally may be unavailable for the attacker. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 424e1af7ba3..981aa101dd3 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -474,6 +474,7 @@ docker build: changes: - Dockerfile - docker/scripts/* + - dockerfiles/**/* ``` In the scenario above, if you are pushing multiple commits to GitLab to an @@ -482,6 +483,7 @@ one of the commits contains changes to either: - The `Dockerfile` file. - Any of the files inside `docker/scripts/` directory. +- Any of the files and subfolders inside `dockerfiles` directory. CAUTION: **Warning:** There are some caveats when using this feature with new branches and tags. See @@ -2031,3 +2033,5 @@ CI with various languages. [ce-12909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12909 [schedules]: ../../user/project/pipelines/schedules.md [variables-expressions]: ../variables/README.md#variables-expressions +[ee]: https://about.gitlab.com/gitlab-ee/ +[gitlab-versions]: https://about.gitlab.com/products/
\ No newline at end of file diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index f9e6efa2c30..b6f053ff0e9 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -171,7 +171,7 @@ There are a few gotchas with it: class Base def execute return unless enabled? - + # ... # ... end @@ -185,12 +185,12 @@ There are a few gotchas with it: class Base def execute return unless enabled? - + do_something end - + private - + def do_something # ... # ... @@ -204,14 +204,14 @@ There are a few gotchas with it: ```ruby module EE::Base extend ::Gitlab::Utils::Override - + override :do_something def do_something # Follow the above pattern to call super and extend it end end ``` - + This would require updating CE first, or make sure this is back ported to CE. When prepending, place them in the `ee/` specific sub-directory, and @@ -332,6 +332,21 @@ full implementation details. [ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12373 [ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2199 +### Code in `config/routes` + +When we add `draw :admin` in `config/routes.rb`, the application will try to +load the file located in `config/routes/admin.rb`, and also try to load the +file located in `ee/config/routes/admin.rb`. + +In EE, it should at least load one file, at most two files. If it cannot find +any files, an error will be raised. In CE, since we don't know if there will +be an EE route, it will not raise any errors even if it cannot find anything. + +This means if we want to extend a particular CE route file, just add the same +file located in `ee/config/routes`. If we want to add an EE only route, we +could still put `draw :ee_only` in both CE and EE, and add +`ee/config/routes/ee_only.rb` in EE, similar to `render_if_exists`. + ### Code in `app/controllers/` In controllers, the most common type of conflict is with `before_action` that diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md index 0f1f079bdb4..350593cc813 100644 --- a/doc/development/feature_flags.md +++ b/doc/development/feature_flags.md @@ -112,3 +112,8 @@ feature flag. You can stub a feature flag as follows: ```ruby stub_feature_flags(my_feature_flag: false) ``` + +## Enabling a feature flag + +Check how to [roll out changes using feature flags](rolling_out_changes_using_feature_flags.md). + diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index 6a67aa7f610..f58d79fccf1 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -20,6 +20,7 @@ are very appreciative of the work done by translators and proofreaders! - French - Davy Defaud - [GitLab](https://gitlab.com/DevDef), [Crowdin](https://crowdin.com/profile/DevDef) - German + - Michael Hahnle - [GitLab](https://gitlab.com/mhah), [Crowdin](https://crowdin.com/profile/mhah) - Indonesian - Ahmad Naufal Mukhtar - [GitLab](https://gitlab.com/anaufalm), [Crowdin](https://crowdin.com/profile/anaufalm) - Italian diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index 25c6371f3d7..4292a17bfa5 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -1,15 +1,13 @@ # Review apps -We currently have review apps available as a manual job in EE pipelines. Here is -[the first implementation](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6259). - -That said, [the Quality team is working](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6665) -on making Review Apps automatically deployed by each pipeline, both in CE and EE. +Review Apps are automatically deployed by each pipeline, both in +[CE](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22010) and +[EE](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6665). ## How does it work? -1. On every EE [pipeline][gitlab-pipeline] during the `test` stage, you can - start the [`review` job][review-job] +1. On every [pipeline][gitlab-pipeline] during the `test` stage, the + [`review` job][review-job] is automatically started. 1. The `review` job [triggers a pipeline][cng-pipeline] in the [`CNG-mirror`][cng-mirror] [^1] project 1. The `CNG-mirror` pipeline creates the Docker images of each component (e.g. `gitlab-rails-ee`, @@ -39,6 +37,9 @@ on making Review Apps automatically deployed by each pipeline, both in CE and EE review app manually, and is also started by GitLab once a branch is deleted - [TBD] Review apps are cleaned up regularly using a pipeline schedule that runs the [`scripts/review_apps/automated_cleanup.rb`][automated_cleanup.rb] script +- If you're unable to log in using the `root` username and password the + deployment may have failed. Stop the review app via the `stop_review` + manual job and then retry the `review` job to redeploy the review app. [^1]: We use the `CNG-mirror` project so that the `CNG`, (**C**loud **N**ative **G**itLab), project's registry is not overloaded with a lot of transient Docker images. diff --git a/doc/install/installation.md b/doc/install/installation.md index 1210ac58499..37c826ce9e0 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -132,9 +132,9 @@ Remove the old Ruby 1.8 if present: Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz - echo 'ec82b0d53bd0adad9b19e6b45e44d54e9ec3f10c ruby-2.4.4.tar.gz' | shasum -c - && tar xzf ruby-2.4.4.tar.gz - cd ruby-2.4.4 + curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.gz + echo '4d650f302f1ec00256450b112bb023644b6ab6dd ruby-2.4.5.tar.gz' | shasum -c - && tar xzf ruby-2.4.5.tar.gz + cd ruby-2.4.5 ./configure --disable-install-rdoc make diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md index 9546f43eea8..73301394e9f 100644 --- a/doc/topics/authentication/index.md +++ b/doc/topics/authentication/index.md @@ -43,7 +43,7 @@ This page gathers all the resources for the topic **Authentication** within GitL ## Third-party resources - [Kanboard Plugin GitLab Authentication](https://github.com/kanboard/plugin-gitlab-auth) -- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+OAuth+Plugin) +- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+OAuth+Plugin) - [Set up Gitlab CE with Active Directory authentication](https://www.caseylabs.com/setup-gitlab-ce-with-active-directory-authentication/) - [How to customize GitLab to support OpenID authentication](http://eric.van-der-vlist.com/blog/2013/11/23/how-to-customize-gitlab-to-support-openid-authentication/) - [Openshift - Configuring Authentication and User Agent](https://docs.openshift.org/latest/install_config/configuring_authentication.html#GitLab) diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index c60d25eda1b..96e788666a1 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -2,14 +2,15 @@ > [Introduced][ce-37115] in GitLab 10.0. Generally available on GitLab 11.0. -Auto DevOps automatically detects, builds, tests, deploys, and monitors your -applications. +Auto DevOps provides pre-defined CI/CD configuration which allows you to automatically detect, build, test, +deploy, and monitor your applications. Leveraging CI/CD best practices and tools, Auto DevOps aims +to simplify the setup and execution of a mature & modern software development lifecycle. ## Overview NOTE: **Enabled by default:** -Starting with GitLab 11.3, the Auto DevOps pipeline will be enabled by default for all -projects. If it's not explicitly enabled for the project, Auto DevOps will be automatically +Starting with GitLab 11.3, the Auto DevOps pipeline is enabled by default for all +projects. If it has not been explicitly enabled for the project, Auto DevOps will be automatically disabled on the first pipeline failure. Your project will continue to use an alternative [CI/CD configuration file](../../ci/yaml/README.md) if one is found. A GitLab administrator can [change this setting](../../user/admin_area/settings/continuous_integration.html#auto-devops) @@ -17,33 +18,38 @@ in the admin area. With Auto DevOps, the software development process becomes easier to set up as every project can have a complete workflow from verification to monitoring -without needing to configure anything. Just push your code and GitLab takes +with minimal configuration. Just push your code and GitLab takes care of everything else. This makes it easier to start new projects and brings consistency to how applications are set up throughout a company. ## Quick start If you are using GitLab.com, see the [quick start guide](quick_start_guide.md) -for using Auto DevOps with GitLab.com and a Kubernetes cluster on Google Kubernetes -Engine. +for how to use Auto DevOps with GitLab.com and a Kubernetes cluster on Google Kubernetes +Engine (GKE). + +If you are using a self-hosted instance of GitLab, you will need to configure the +[Google OAuth2 OmniAuth Provider](../../integration/google.md) before +you can configure a cluster on GKE. Once this is set up, you can follow the steps on the +[quick start guide](quick_start_guide.md) to get started. ## Comparison to application platforms and PaaS -Auto DevOps provides functionality described by others as an application -platform or as a Platform as a Service (PaaS). It takes inspiration from the +Auto DevOps provides functionality that is often included in an application +platform or a Platform as a Service (PaaS). It takes inspiration from the innovative work done by [Heroku](https://www.heroku.com/) and goes beyond it -in a couple of ways: +in multiple ways: -1. Auto DevOps works with any Kubernetes cluster, you're not limited to running - on GitLab's infrastructure (note that many features also work without Kubernetes). +1. Auto DevOps works with any Kubernetes cluster; you're not limited to running + on GitLab's infrastructure. (Note that many features also work without Kubernetes.) 1. There is no additional cost (no markup on the infrastructure costs), and you can use a self-hosted Kubernetes cluster or Containers as a Service on any - public cloud (for example [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)). + public cloud (for example, [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)). 1. Auto DevOps has more features including security testing, performance testing, and code quality testing. -1. It offers an incremental graduation path. If you need advanced customizations +1. Auto DevOps offers an incremental graduation path. If you need advanced customizations, you can start modifying the templates without having to start over on a - completely different platform. + completely different platform. Review the [customizing](#customizing) section for more information. ## Features @@ -197,23 +203,38 @@ and verifying that your app is deployed as a review app in the Kubernetes cluster with the `review/*` environment scope. Similarly, you can check the other environments. -## Enabling Auto DevOps +## Enabling/Disabling Auto DevOps -If you haven't done already, read the [requirements](#requirements) to make -full use of Auto DevOps. If this is your fist time, we recommend you follow the +When first using Auto Devops, review the [requirements](#requirements) to ensure all necessary components to make +full use of Auto DevOps are available. If this is your fist time, we recommend you follow the [quick start guide](quick_start_guide.md). -To enable Auto DevOps to your project: +GitLab.com users can enable/disable Auto DevOps at the project-level only. Self-managed users +can enable/disable Auto DevOps at either the project-level or instance-level. + +### Enabling/disabling Auto DevOps at the instance-level (Administrators only) + +1. Go to **Admin area > Settings > Continuous Integration and Deployment**. +1. Toggle the checkbox labeled **Default to Auto DevOps pipeline for all projects**. +1. If enabling, optionally set up the Auto DevOps [base domain](#auto-devops-base-domain) which will be used for Auto Deploy and Auto Review Apps. +1. Click **Save changes** for the changes to take effect. + +NOTE: **Note:** +Even when disabled at the instance level, project maintainers are still able to enable +Auto DevOps at the project level. + +### Enabling/disabling Auto DevOps at the project-level -1. Check that your project doesn't have a `.gitlab-ci.yml`, or remove it otherwise -1. Go to your project's **Settings > CI/CD > Auto DevOps** -1. Select "Enable Auto DevOps" -1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain) - that will be used by Kubernetes to [deploy your application](#auto-deploy) - and choose the [deployment strategy](#deployment-strategy) -1. Hit **Save changes** for the changes to take effect +If enabling, check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it. -Once saved, an Auto DevOps pipeline will be triggered on the default branch. +1. Go to your project's **Settings > CI/CD > Auto DevOps**. +1. Toggle the **Default to Auto DevOps pipeline** checkbox (checked to enable, unchecked to disable) +1. When enabling, it's optional but recommended to add in the [base domain](#auto-devops-base-domain) + that will be used by Auto DevOps to [deploy your application](#auto-deploy) + and choose the [deployment strategy](#deployment-strategy). +1. Click **Save changes** for the changes to take effect. + +When the feature has been enabled, an Auto DevOps pipeline is triggered on the default branch. NOTE: **Note:** For GitLab versions 10.0 - 10.2, when enabling Auto DevOps, a pipeline needs to be @@ -222,12 +243,6 @@ manually triggered either by pushing a new commit to the repository or by visiti a new pipeline for your default branch, generally `master`. NOTE: **Note:** -If you are a GitLab Administrator, you can -[enable/disable Auto DevOps instance-wide](../../user/admin_area/settings/continuous_integration.md#auto-devops), -and all projects that haven't explicitly set an option will have Auto DevOps -enabled/disabled by default. - -NOTE: **Note:** There is also a feature flag to enable Auto DevOps to a percentage of projects which can be enabled from the console with `Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`. @@ -299,8 +314,7 @@ static analysis and other code checks on the current code. The report is created, and is uploaded as an artifact which you can later download and check out. -In GitLab Starter, differences between the source and -target branches are also +Any differences between the source and target branches are also [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html). ### Auto SAST **[ULTIMATE]** @@ -313,9 +327,12 @@ analysis on the current code and checks for potential security issues. Once the report is created, it's uploaded as an artifact which you can later download and check out. -In GitLab Ultimate, any security warnings are also +Any security warnings are also [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/sast.html). +NOTE: **Note:** +The Auto SAST stage will be skipped on licenses other than Ultimate. + ### Auto Dependency Scanning **[ULTIMATE]** > Introduced in [GitLab Ultimate][ee] 10.7. @@ -329,6 +346,9 @@ check out. Any security warnings are also [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dependency_scanning.html). +NOTE: **Note:** +The Auto Dependency Scanning stage will be skipped on licenses other than Ultimate. + ### Auto License Management **[ULTIMATE]** > Introduced in [GitLab Ultimate][ee] 11.0. @@ -342,6 +362,9 @@ check out. Any licenses are also [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/license_management.html). +NOTE: **Note:** +The Auto License Management stage will be skipped on licenses other than Ultimate. + ### Auto Container Scanning > Introduced in GitLab 10.4. @@ -352,9 +375,12 @@ Docker image and checks for potential security issues. Once the report is created, it's uploaded as an artifact which you can later download and check out. -In GitLab Ultimate, any security warnings are also +Any security warnings are also [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/container_scanning.html). +NOTE: **Note:** +The Auto Container Scanning stage will be skipped on licenses other than Ultimate. + ### Auto Review Apps NOTE: **Note:** @@ -374,6 +400,9 @@ branch's code so developers, designers, QA, product managers, and other reviewers can actually see and interact with code changes as part of the review process. Auto Review Apps create a Review App for each branch. +Auto Review Apps will deploy your app to your Kubernetes cluster only. When no cluster +is available, no deployment will occur. + The Review App will have a unique URL based on the project name, the branch name, and a unique number, combined with the Auto DevOps base domain. For example, `user-project-branch-1234.example.com`. A link to the Review App shows @@ -391,9 +420,12 @@ to perform an analysis on the current code and checks for potential security issues. Once the report is created, it's uploaded as an artifact which you can later download and check out. -In GitLab Ultimate, any security warnings are also +Any security warnings are also [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dast.html). +NOTE: **Note:** +The Auto DAST stage will be skipped on licenses other than Ultimate. + ### Auto Browser Performance Testing **[PREMIUM]** > Introduced in [GitLab Premium][ee] 10.4. @@ -406,8 +438,8 @@ Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://h /direction ``` -In GitLab Premium, performance differences between the source -and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html). +Any performance differences between the source and target branches are also +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html). ### Auto Deploy diff --git a/doc/university/README.md b/doc/university/README.md index 5edf92b3b09..f19b1ffd3d9 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -205,7 +205,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project ### 4. External Articles -1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460) +1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460) 1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/) 1. [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/) diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index 6ff27e495fb..6e0f71017c6 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -303,7 +303,7 @@ A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) use ### Jenkins -An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html). +An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins.io/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html). ### Jira diff --git a/doc/update/11.3-to-11.4.md b/doc/update/11.3-to-11.4.md index b50e21f27dd..00dfb19b4b4 100644 --- a/doc/update/11.3-to-11.4.md +++ b/doc/update/11.3-to-11.4.md @@ -39,9 +39,9 @@ Download Ruby and compile it: ```bash mkdir /tmp/ruby && cd /tmp/ruby -curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz -echo 'ec82b0d53bd0adad9b19e6b45e44d54e9ec3f10c ruby-2.4.4.tar.gz' | shasum -c - && tar xzf ruby-2.4.4.tar.gz -cd ruby-2.4.4 +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.gz +echo '4d650f302f1ec00256450b112bb023644b6ab6dd ruby-2.4.5.tar.gz' | shasum -c - && tar xzf ruby-2.4.5.tar.gz +cd ruby-2.4.5 ./configure --disable-install-rdoc make diff --git a/doc/update/11.4-to-11.5.md b/doc/update/11.4-to-11.5.md new file mode 100644 index 00000000000..e64ab2acae2 --- /dev/null +++ b/doc/update/11.4-to-11.5.md @@ -0,0 +1,404 @@ +--- +comments: false +--- + +# From 11.4 to 11.5 + +Make sure you view this update guide from the branch (version) of GitLab you would +like to install (e.g., `11-5-stable`. You can select the branch in the version +dropdown at the top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +NOTE: If you installed GitLab from source, make sure `rsync` is installed. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 11.0 and higher only support Ruby 2.4.x and dropped support for Ruby 2.3.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download Ruby and compile it: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.gz +echo '4d650f302f1ec00256450b112bb023644b6ab6dd ruby-2.4.5.tar.gz' | shasum -c - && tar xzf ruby-2.4.5.tar.gz +cd ruby-2.4.5 + +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets. +This requires a minimum version of node v6.0.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v6.0.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + +GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript +dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 11.4 and higher only supports Go 1.10.x and newer, and dropped support for Go +1.9.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz +echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.10.3.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all --prune +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-5-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-5-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### New Gitaly configuration options required + +In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`. + +```shell +echo ' +[gitaly-ruby] +dir = "/home/git/gitaly/ruby" + +[gitlab-shell] +dir = "/home/git/gitlab-shell" +' | sudo -u git tee -a /home/git/gitaly/config.toml +``` + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update gitlab-pages + +#### Only needed if you use GitLab Pages. + +Install and compile gitlab-pages. GitLab-Pages uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-pages + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) +sudo -u git -H make +``` + +### 11. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 12. Update configuration files + +#### New `unicorn.rb` configuration + +Note: we have made [changes](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22372) to `unicorn.rb` to allow GitLab run with both Unicorn and Puma in future. + +- Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/unicorn.rb.example but with your settings. + - In particular, make sure that `require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"` line exists and the `before_exec`, `before_fork`, and `after_fork` handlers are configured as shown below: + +```ruby +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" + +before_exec do |server| + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart +end + +before_fork do |server, worker| + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +after_fork do |server, worker| + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start +end +``` + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/11-4-stable:config/gitlab.yml.example origin/11-5-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/11-1-stable:lib/support/nginx/gitlab-ssl origin/11-5-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/11-1-stable:lib/support/nginx/gitlab origin/11-5-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/11-1-stable:lib/support/init.d/gitlab.default.example origin/11-5-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 13. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 14. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 15. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (11.4) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 11.3 to 11.4](11.3-to-11.4.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md new file mode 100644 index 00000000000..3b81e439119 --- /dev/null +++ b/doc/user/project/clusters/runbooks/index.md @@ -0,0 +1,49 @@ +# Runbooks + +Runbooks are a collection of documented procedures that explain how to +carry out a particular process, be it starting, stopping, debugging, +or troubleshooting a particular system. + +## Overview + +Historically, runbooks took the form of a decision tree or a detailed +step-by-step guide depending on the condition or system. + +Modern implementations have introduced the concept of an "executable +runbooks", where along with a well define process, operators can execute +code blocks or database queries against a given environment. + +## Nurtch Executable Runbooks + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45912) in GitLab 11.4. + +The JupyterHub app offered via GitLab’s Kubernetes integration now ships +with Nurtch’s Rubix library, providing a simple way to create DevOps +runbooks. A sample runbook is provided, showcasing common operations. + +**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +Watch this [video](https://www.youtube.com/watch?v=Q_OqHIIUPjE) +for an overview of how this is acomplished in GitLab!** + +## Requirements + +To create an executable runbook, you will need: + +1. **Kubernetes** - A Kubernetes cluster is required to deploy the rest of the applications. + The simplest way to get started is to add a cluster using [GitLab's GKE integration](https://docs.gitlab.com/ee/user/project/clusters/#adding-and-creating-a-new-gke-cluster-via-gitlab). +1. **Helm Tiller** - Helm is a package manager for Kubernetes and is required to install + all the other applications. It is installed in its own pod inside the cluster which + can run the helm CLI in a safe environment. +1. **Ingress** - Ingress can provide load balancing, SSL termination, and name-based + virtual hosting. It acts as a web proxy for your applications. +1. **JupyterHub** - JupyterHub is a multi-user service for managing notebooks across + a team. Jupyter Notebooks provide a web-based interactive programming environment + used for data analysis, visualization, and machine learning. + +## Nurtch + +Nurtch is the company behind the [Rubix library](https://github.com/Nurtch/rubix). Rubix is +an open-source python library that makes it easy to perform common DevOps tasks inside Jupyter Notebooks. +Tasks such as plotting Cloudwatch metrics and rolling your ECS/Kubernetes app are simplified +down to a couple of lines of code. Check the [Nurtch Documentation](http://docs.nurtch.com/en/latest) +for more information. diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 464fa5987c1..9e2434c02ec 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -176,6 +176,9 @@ Clicking on the current board name in the upper left corner will reveal a menu from where you can create another Issue Board and rename or delete the existing one. +When you're revisiting an issue board in a project or group with multiple boards, +GitLab will automatically load the last board you visited. + NOTE: **Note:** The Multiple Issue Boards feature is available for **projects in GitLab Starter Edition** and for **groups in GitLab Premium Edition**. diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md index e1eede8bbed..89b9621b8b9 100644 --- a/doc/user/project/pages/getting_started_part_three.md +++ b/doc/user/project/pages/getting_started_part_three.md @@ -212,7 +212,7 @@ security measure, necessary just for big companies, like banks and shoppings sit with financial transactions. Now we have a different picture. [According to Josh Aas](https://letsencrypt.org/2015/10/29/phishing-and-malware.html), Executive Director at [ISRG](https://en.wikipedia.org/wiki/Internet_Security_Research_Group): -> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](http://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._ +> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](https://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._ Therefore, the reason why certificates are so important is that they encrypt the connection between the **client** (you, me, your visitors) diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md index e1d8345f415..783081cec26 100644 --- a/doc/user/project/repository/branches/index.md +++ b/doc/user/project/repository/branches/index.md @@ -30,12 +30,12 @@ to learn more. ## Delete merged branches -> [Introduced][ce-6449] in GitLab 8.14. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6449) in GitLab 8.14. ![Delete merged branches](img/delete_merged_branches.png) This feature allows merged branches to be deleted in bulk. Only branches that -have been merged and [are not protected][protected] will be deleted as part of +have been merged and [are not protected](../../protected_branches.md) will be deleted as part of this operation. It's particularly useful to clean up old branches that were not deleted @@ -44,7 +44,7 @@ automatically when a merge request was merged. ## Branch filter search box -> [Introduced][https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22166] in GitLab 11.5. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22166) in GitLab 11.5. ![Branch filter search box](img/branch_filter_search_box.png) @@ -57,6 +57,3 @@ Sometimes when you have hundreds of branches you may want a more flexible matchi - `^feature` will only match branch names that begin with 'feature'. - `feature$` will only match branch names that end with 'feature'. - -[ce-6449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6449 "Add button to delete all merged branches" -[protected]: ../../protected_branches.md diff --git a/doc/user/search/img/issues_filter_none_any.png b/doc/user/search/img/issues_filter_none_any.png Binary files differnew file mode 100644 index 00000000000..9682fc55315 --- /dev/null +++ b/doc/user/search/img/issues_filter_none_any.png diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 4f1b96b775c..3f9d07dacaa 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -40,6 +40,16 @@ The same process is valid for merge requests. Navigate to your project's **Merge and click **Search or filter results...**. Merge requests can be filtered by author, assignee, milestone, and label. +### Filtering by **None** / **Any** + +Some filter fields like milestone and assignee, allow you to filter by **None** or **Any**. + +![filter by none any](img/issues_filter_none_any.png) + +Selecting **None** returns results that have an empty value for that field. E.g.: no milestone, no assignee. + +Selecting **Any** does the opposite. It returns results that have a non-empty value for that field. + ### Searching for specific terms You can filter issues and merge requests by specific terms included in titles or descriptions. diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 18c30723d73..9f7be27b047 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -160,13 +160,27 @@ module API # (fixed in https://github.com/rails/rails/pull/25976). project.tags.map(&:name).sort end + expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url + + expose :license_url, if: :license do |project| + license = project.repository.license_blob + + if license + Gitlab::Routing.url_helpers.project_blob_url(project, File.join(project.default_branch, license.path)) + end + end + + expose :license, with: 'API::Entities::LicenseBasic', if: :license do |project| + project.repository.license + end + expose :avatar_url do |project, options| project.avatar_url(only_path: false) end + expose :star_count, :forks_count expose :last_activity_at - expose :namespace, using: 'API::Entities::NamespaceBasic' expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes @@ -1208,11 +1222,14 @@ module API expose :deployable, using: Entities::Job end - class License < Grape::Entity + class LicenseBasic < Grape::Entity expose :key, :name, :nickname - expose :popular?, as: :popular expose :url, as: :html_url expose(:source_url) { |license| license.meta['source'] } + end + + class License < LicenseBasic + expose :popular?, as: :popular expose(:description) { |license| license.meta['description'] } expose(:conditions) { |license| license.meta['conditions'] } expose(:permissions) { |license| license.meta['permissions'] } diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb index 23b1cd1ad45..1058f4e8a5e 100644 --- a/lib/api/helpers/custom_validators.rb +++ b/lib/api/helpers/custom_validators.rb @@ -10,8 +10,21 @@ module API raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence) end end + + class IntegerNoneAny < Grape::Validations::Base + def validate_param!(attr_name, params) + value = params[attr_name] + + return if value.is_a?(Integer) || + [IssuableFinder::FILTER_NONE, IssuableFinder::FILTER_ANY].include?(value.to_s.downcase) + + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], + message: "should be an integer, 'None' or 'Any'" + end + end end end end Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence) +Grape::Validations.register_validator(:integer_none_any, ::API::Helpers::CustomValidators::IntegerNoneAny) diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 4dd6b19e353..ae40b5f7557 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -65,6 +65,8 @@ module API result rescue Gitlab::GitAccess::UnauthorizedError => e break response_with_status(code: 401, success: false, message: e.message) + rescue Gitlab::GitAccess::TimeoutError => e + break response_with_status(code: 503, success: false, message: e.message) rescue Gitlab::GitAccess::NotFoundError => e break response_with_status(code: 404, success: false, message: e.message) end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 25d78053c88..405fc30a2ed 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -40,7 +40,8 @@ module API optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time' optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time' optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID' - optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID' + optional :assignee_id, types: [Integer, String], integer_none_any: true, + desc: 'Return issues which are assigned to the user with the given ID' optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 440d94ae186..a617efaaa4c 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -89,7 +89,8 @@ module API optional :updated_before, type: DateTime, desc: 'Return merge requests updated before the specified time' optional :view, type: String, values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request' optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID' - optional :assignee_id, type: Integer, desc: 'Return merge requests which are assigned to the user with the given ID' + optional :assignee_id, types: [Integer, String], integer_none_any: true, + desc: 'Return merge requests which are assigned to the user with the given ID' optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' diff --git a/lib/api/projects.rb b/lib/api/projects.rb index ae2d327e45b..0a914f9012e 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -114,7 +114,8 @@ module API options = options.reverse_merge( with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails, statistics: params[:statistics], - current_user: current_user + current_user: current_user, + license: false ) options[:with] = Entities::BasicProjectDetails if params[:simple] @@ -230,13 +231,17 @@ module API params do use :statistics_params use :with_custom_attributes + + optional :license, type: Boolean, default: false, + desc: 'Include project license data' end get ":id" do options = { with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails, current_user: current_user, user_can_admin_project: can?(current_user, :admin_project, user_project), - statistics: params[:statistics] + statistics: params[:statistics], + license: params[:license] } project, options = with_custom_attributes(user_project, options) diff --git a/lib/api/runner.rb b/lib/api/runner.rb index d8768a54986..2f15f3a7d76 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -142,8 +142,7 @@ module API requires :id, type: Integer, desc: %q(Job's ID) optional :trace, type: String, desc: %q(Job's full trace) optional :state, type: String, desc: %q(Job's status: success, failed) - optional :failure_reason, type: String, values: CommitStatus.failure_reasons.keys, - desc: %q(Job's failure_reason) + optional :failure_reason, type: String, desc: %q(Job's failure_reason) end put '/:id' do job = authenticate_job! diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb index b369b9e7600..dfbb83f7bb9 100644 --- a/lib/gitlab/cache/ci/project_pipeline_status.rb +++ b/lib/gitlab/cache/ci/project_pipeline_status.rb @@ -42,7 +42,7 @@ module Gitlab end def self.cache_key_for_project(project) - "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:projects/#{project.id}/pipeline_status" + "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:projects/#{project.id}/pipeline_status/#{project.commit&.sha}" end def self.update_for_pipeline(pipeline) @@ -84,9 +84,7 @@ module Gitlab def load_from_project return unless commit - self.sha = commit.sha - self.status = commit.status - self.ref = project.default_branch + self.sha, self.status, self.ref = commit.sha, commit.status, project.default_branch end # We only cache the status for the HEAD commit of a project @@ -104,6 +102,8 @@ module Gitlab def load_from_cache Gitlab::Redis::Cache.with do |redis| self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref) + + self.status = nil if self.status.empty? end end diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 49e7f7e1fd7..074afe9c412 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -18,11 +18,24 @@ module Gitlab lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".' }.freeze - attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name + LOG_MESSAGES = { + push_checks: "Checking if you are allowed to push...", + delete_default_branch_check: "Checking if default branch is being deleted...", + protected_branch_checks: "Checking if you are force pushing to a protected branch...", + protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...", + protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch...", + tag_checks: "Checking if you are allowed to change existing tags...", + protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag...", + lfs_objects_exist_check: "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...", + commits_check_file_paths_validation: "Validating commits' file paths...", + commits_check: "Validating commit contents..." + }.freeze + + attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name, :logger def initialize( change, user_access:, project:, skip_authorization: false, - skip_lfs_integrity_check: false, protocol: + skip_lfs_integrity_check: false, protocol:, logger: ) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @branch_name = Gitlab::Git.branch_name(@ref) @@ -32,6 +45,9 @@ module Gitlab @skip_authorization = skip_authorization @skip_lfs_integrity_check = skip_lfs_integrity_check @protocol = protocol + + @logger = logger + @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}") end def exec(skip_commits_check: false) @@ -49,26 +65,32 @@ module Gitlab protected def push_checks - unless can_push? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code] + logger.log_timed(LOG_MESSAGES[__method__]) do + unless can_push? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code] + end end end def branch_checks return unless branch_name - if deletion? && branch_name == project.default_branch - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch] + logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do + if deletion? && branch_name == project.default_branch + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch] + end end protected_branch_checks end def protected_branch_checks - return unless ProtectedBranch.protected?(project, branch_name) + logger.log_timed(LOG_MESSAGES[__method__]) do + return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks - if forced_push? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch] + if forced_push? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch] + end end if deletion? @@ -79,23 +101,27 @@ module Gitlab end def protected_branch_deletion_checks - unless user_access.can_delete_branch?(branch_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch] - end + logger.log_timed(LOG_MESSAGES[__method__]) do + unless user_access.can_delete_branch?(branch_name) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch] + end - unless updated_from_web? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch] + unless updated_from_web? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch] + end end end def protected_branch_push_checks - if matching_merge_request? - unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch] - end - else - unless user_access.can_push_to_branch?(branch_name) - raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message + logger.log_timed(LOG_MESSAGES[__method__]) do + if matching_merge_request? + unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch] + end + else + unless user_access.can_push_to_branch?(branch_name) + raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message + end end end end @@ -103,21 +129,25 @@ module Gitlab def tag_checks return unless tag_name - if tag_exists? && user_access.cannot_do_action?(:admin_project) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags] + logger.log_timed(LOG_MESSAGES[__method__]) do + if tag_exists? && user_access.cannot_do_action?(:admin_project) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags] + end end protected_tag_checks end def protected_tag_checks - return unless ProtectedTag.protected?(project, tag_name) + logger.log_timed(LOG_MESSAGES[__method__]) do + return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks - raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update? - raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion? + raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update? + raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion? - unless user_access.can_create_tag?(tag_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag] + unless user_access.can_create_tag?(tag_name) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag] + end end end @@ -125,14 +155,20 @@ module Gitlab return if deletion? || newrev.nil? return unless should_run_commit_validations? - # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593 - ::Gitlab::GitalyClient.allow_n_plus_1_calls do - commits.each do |commit| - commit_check.validate(commit, validations_for_commit(commit)) + logger.log_timed(LOG_MESSAGES[__method__]) do + # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593 + ::Gitlab::GitalyClient.allow_n_plus_1_calls do + commits.each do |commit| + logger.check_timeout_reached + + commit_check.validate(commit, validations_for_commit(commit)) + end end end - commit_check.validate_file_paths + logger.log_timed(LOG_MESSAGES[:commits_check_file_paths_validation]) do + commit_check.validate_file_paths + end end # Method overwritten in EE to inject custom validations @@ -194,10 +230,12 @@ module Gitlab end def lfs_objects_exist_check - lfs_check = Checks::LfsIntegrity.new(project, newrev) + logger.log_timed(LOG_MESSAGES[__method__]) do + lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left) - if lfs_check.objects_missing? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing] + if lfs_check.objects_missing? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing] + end end end diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb index fa3dc1808df..1652d5a30a4 100644 --- a/lib/gitlab/checks/lfs_integrity.rb +++ b/lib/gitlab/checks/lfs_integrity.rb @@ -3,9 +3,10 @@ module Gitlab module Checks class LfsIntegrity - def initialize(project, newrev) + def initialize(project, newrev, time_left) @project = project @newrev = newrev + @time_left = time_left end # rubocop: disable CodeReuse/ActiveRecord @@ -13,7 +14,7 @@ module Gitlab return false unless @newrev && @project.lfs_enabled? new_lfs_pointers = Gitlab::Git::LfsChanges.new(@project.repository, @newrev) - .new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT) + .new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT, dynamic_timeout: @time_left) return false unless new_lfs_pointers.present? diff --git a/lib/gitlab/checks/timed_logger.rb b/lib/gitlab/checks/timed_logger.rb new file mode 100644 index 00000000000..f365e0a43f6 --- /dev/null +++ b/lib/gitlab/checks/timed_logger.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Gitlab + module Checks + class TimedLogger + TimeoutError = Class.new(StandardError) + + attr_reader :start_time, :header, :log, :timeout + + def initialize(start_time: Time.now, log: [], header: "", timeout:) + @start_time = start_time + @timeout = timeout + @header = header + @log = log + end + + # Adds trace of method being tracked with + # the correspondent time it took to run it. + # We make use of the start default argument + # on unit tests related to this method + # + def log_timed(log_message, start = Time.now) + check_timeout_reached + + timed = true + + yield + + append_message(log_message + time_suffix_message(start: start)) + rescue GRPC::DeadlineExceeded, TimeoutError + args = { cancelled: true } + args[:start] = start if timed + + append_message(log_message + time_suffix_message(args)) + + raise TimeoutError + end + + def check_timeout_reached + return unless time_expired? + + raise TimeoutError + end + + def time_left + (start_time + timeout.seconds) - Time.now + end + + def full_message + header + log.join("\n") + end + + # We always want to append in-place on the log + def append_message(message) + log << message + end + + private + + def time_expired? + time_left <= 0 + end + + def time_suffix_message(cancelled: false, start: nil) + return " (#{elapsed_time(start)}ms)" unless cancelled + + if start + " (cancelled after #{elapsed_time(start)}ms)" + else + " (cancelled)" + end + end + + def elapsed_time(start) + to_ms(Time.now - start) + end + + def to_ms(elapsed) + (elapsed.to_f * 1000).round(2) + end + end + end +end diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb index 50b0d044265..4babc23a495 100644 --- a/lib/gitlab/ci/status/build/failed.rb +++ b/lib/gitlab/ci/status/build/failed.rb @@ -11,7 +11,8 @@ module Gitlab runner_system_failure: 'runner system failure', missing_dependency_failure: 'missing dependency failure', runner_unsupported: 'unsupported runner', - stale_schedule: 'stale schedule' + stale_schedule: 'stale schedule', + job_execution_timeout: 'job execution timeout' }.freeze private_constant :REASONS diff --git a/lib/gitlab/ci/templates/Android.gitlab-ci.yml b/lib/gitlab/ci/templates/Android.gitlab-ci.yml index bf7831b937c..6e138639b71 100644 --- a/lib/gitlab/ci/templates/Android.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Android.gitlab-ci.yml @@ -1,51 +1,45 @@ -# Read more about this script on this blog post https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/, by Greyson Parrelli +# Read more about this script on this blog post https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/, by Jason Lenny image: openjdk:8-jdk variables: ANDROID_COMPILE_SDK: "28" - ANDROID_BUILD_TOOLS: "28.0.3" - ANDROID_SDK_TOOLS: "26.1.1" + ANDROID_BUILD_TOOLS: "28.0.2" + ANDROID_SDK_TOOLS: "4333796" before_script: -- apt-get --quiet update --yes -- apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 -- wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip -- unzip android-sdk.zip -d android-sdk-linux -- echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null -- echo y | android-sdk-linux/tools/bin/sdkmanager platform-tools > /dev/null -- echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" > /dev/null -- echo y | android-sdk-linux/tools/bin/sdkmanager "extras;google;google_play_services" > /dev/null -- echo y | android-sdk-linux/tools/bin/sdkmanager "extras;google;m2repository" > /dev/null -- export ANDROID_HOME=$PWD/android-sdk-linux -- export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ -- yes | android-sdk-linux/tools/bin/sdkmanager --licenses & -- chmod +x ./gradlew + - apt-get --quiet update --yes + - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 + - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip + - unzip -d android-sdk-linux android-sdk.zip + - echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null + - echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null + - echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null + - export ANDROID_HOME=$PWD/android-sdk-linux + - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ + - chmod +x ./gradlew + # temporarily disable checking for EPIPE error and use yes to accept all licenses + - set +o pipefail + - yes | android-sdk-linux/tools/bin/sdkmanager --licenses + - set -o pipefail stages: -- build -- test + - build + - test -build: +lintDebug: stage: build script: - - ./gradlew assembleDebug + - ./gradlew -Pci --console=plain :app:lintDebug -PbuildDir=lint + +assembleDebug: + stage: build + script: + - ./gradlew assembleDebug artifacts: paths: - app/build/outputs/ -unitTests: - stage: test - script: - - ./gradlew test - -functionalTests: +debugTests: stage: test script: - - wget --quiet --output-document=android-wait-for-emulator https://raw.githubusercontent.com/travis-ci/travis-cookbooks/0f497eb71291b52a703143c5cd63a217c8766dc9/community-cookbooks/android-sdk/files/default/android-wait-for-emulator - - chmod +x android-wait-for-emulator - - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter sys-img-x86-google_apis-${ANDROID_COMPILE_SDK} - - echo no | android-sdk-linux/tools/android create avd -n test -t android-${ANDROID_COMPILE_SDK} --abi google_apis/x86 - - android-sdk-linux/tools/emulator64-x86 -avd test -no-window -no-audio & - - ./android-wait-for-emulator - - adb shell input keyevent 82 - - ./gradlew cAT + - ./gradlew -Pci --console=plain :app:testDebug diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb new file mode 100644 index 00000000000..b05dca409d1 --- /dev/null +++ b/lib/gitlab/cluster/lifecycle_events.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module Gitlab + module Cluster + # + # LifecycleEvents lets Rails initializers register application startup hooks + # that are sensitive to forking. For example, to defer the creation of + # watchdog threads. This lets us abstract away the Unix process + # lifecycles of Unicorn, Sidekiq, Puma, Puma Cluster, etc. + # + # We have three lifecycle events. + # + # - before_fork (only in forking processes) + # - worker_start + # - before_master_restart (only in forking processes) + # + # Blocks will be executed in the order in which they are registered. + # + class LifecycleEvents + class << self + # + # Hook registration methods (called from initializers) + # + def on_worker_start(&block) + if in_clustered_environment? + # Defer block execution + (@worker_start_hooks ||= []) << block + else + yield + end + end + + def on_before_fork(&block) + return unless in_clustered_environment? + + # Defer block execution + (@before_fork_hooks ||= []) << block + end + + def on_master_restart(&block) + return unless in_clustered_environment? + + # Defer block execution + (@master_restart_hooks ||= []) << block + end + + # + # Lifecycle integration methods (called from unicorn.rb, puma.rb, etc.) + # + def do_worker_start + @worker_start_hooks&.each do |block| + block.call + end + end + + def do_before_fork + @before_fork_hooks&.each do |block| + block.call + end + end + + def do_master_restart + @master_restart_hooks && @master_restart_hooks.each do |block| + block.call + end + end + + # Puma doesn't use singletons (which is good) but + # this means we need to pass through whether the + # puma server is running in single mode or cluster mode + def set_puma_options(options) + @puma_options = options + end + + private + + def in_clustered_environment? + # Sidekiq doesn't fork + return false if Sidekiq.server? + + # Unicorn always forks + return true if defined?(::Unicorn) + + # Puma sometimes forks + return true if in_clustered_puma? + + # Default assumption is that we don't fork + false + end + + def in_clustered_puma? + return false unless defined?(::Puma) + + @puma_options && @puma_options[:workers] && @puma_options[:workers] > 0 + end + end + end + end +end diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb new file mode 100644 index 00000000000..331c39f7d6b --- /dev/null +++ b/lib/gitlab/cluster/puma_worker_killer_initializer.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Cluster + class PumaWorkerKillerInitializer + def self.start(puma_options, puma_per_worker_max_memory_mb: 650) + require 'puma_worker_killer' + + PumaWorkerKiller.config do |config| + # Note! ram is expressed in megabytes (whereas GITLAB_UNICORN_MEMORY_MAX is in bytes) + # Importantly RAM is for _all_workers (ie, the cluster), + # not each worker as is the case with GITLAB_UNICORN_MEMORY_MAX + worker_count = puma_options[:workers] || 1 + config.ram = worker_count * puma_per_worker_max_memory_mb + + config.frequency = 20 # seconds + + # We just want to limit to a fixed maximum, unrelated to the total amount + # of available RAM. + config.percent_usage = 0.98 + + # Ideally we'll never hit the maximum amount of memory. If so the worker + # is restarted already, thus periodically restarting workers shouldn't be + # needed. + config.rolling_restart_frequency = false + end + + PumaWorkerKiller.start + end + end + end +end diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index 6fc86925f81..5d9ecd651a0 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -286,7 +286,7 @@ module Gitlab end def patch_name_from_branch(branch_name) - branch_name.parameterize << '.patch' + "#{branch_name.parameterize}.patch" end def patch_url @@ -434,9 +434,11 @@ module Gitlab end def conflicting_files_msg - failed_files.reduce("The conflicts detected were as follows:\n") do |memo, file| - memo << "\n - #{file}" - end + header = "The conflicts detected were as follows:\n" + separator = "\n - " + failed_items = failed_files.join(separator) + + "#{header}#{separator}#{failed_items}" end end end diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb index f0fab1e76a3..d7148165408 100644 --- a/lib/gitlab/git/lfs_changes.rb +++ b/lib/gitlab/git/lfs_changes.rb @@ -6,8 +6,8 @@ module Gitlab @newrev = newrev end - def new_pointers(object_limit: nil, not_in: nil) - @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in) + def new_pointers(object_limit: nil, not_in: nil, dynamic_timeout: nil) + @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in, dynamic_timeout) end def all_pointers diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 827c04ae035..802fa65dd63 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -9,6 +9,7 @@ module Gitlab UnauthorizedError = Class.new(StandardError) NotFoundError = Class.new(StandardError) ProjectCreationError = Class.new(StandardError) + TimeoutError = Class.new(StandardError) ProjectMovedError = Class.new(NotFoundError) ERROR_MESSAGES = { @@ -26,11 +27,18 @@ module Gitlab cannot_push_to_read_only: "You can't push code to a read-only GitLab instance." }.freeze + INTERNAL_TIMEOUT = 50.seconds.freeze + LOG_HEADER = <<~MESSAGE + Push operation timed out + + Timing information for debugging purposes: + MESSAGE + DOWNLOAD_COMMANDS = %w{git-upload-pack git-upload-archive}.freeze PUSH_COMMANDS = %w{git-receive-pack}.freeze ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS - attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes + attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes, :logger def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, project_path: nil, redirected_path: nil, auth_result_type: nil) @actor = actor @@ -44,6 +52,7 @@ module Gitlab end def check(cmd, changes) + @logger = Checks::TimedLogger.new(timeout: INTERNAL_TIMEOUT, header: LOG_HEADER) @changes = changes check_protocol! @@ -269,14 +278,19 @@ module Gitlab end def check_single_change_access(change, skip_lfs_integrity_check: false) - Checks::ChangeAccess.new( + change_access = Checks::ChangeAccess.new( change, user_access: user_access, project: project, skip_authorization: deploy_key?, skip_lfs_integrity_check: skip_lfs_integrity_check, - protocol: protocol - ).exec + protocol: protocol, + logger: logger + ) + + change_access.exec + rescue Checks::TimedLogger::TimeoutError + raise TimeoutError, logger.full_message end def deploy_key diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb index e731e654f3c..cf2329e489d 100644 --- a/lib/gitlab/git_post_receive.rb +++ b/lib/gitlab/git_post_receive.rb @@ -11,8 +11,8 @@ module Gitlab @changes = deserialize_changes(changes) end - def identify(revision) - super(identifier, project, revision) + def identify + super(identifier) end def changes_refs diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb index 1840bf45154..086ce31e678 100644 --- a/lib/gitlab/gitaly_client/blob_service.rb +++ b/lib/gitlab/gitaly_client/blob_service.rb @@ -72,7 +72,7 @@ module Gitlab GitalyClient::BlobsStitcher.new(response) end - def get_new_lfs_pointers(revision, limit, not_in) + def get_new_lfs_pointers(revision, limit, not_in, dynamic_timeout = nil) request = Gitaly::GetNewLFSPointersRequest.new( repository: @gitaly_repo, revision: encode_binary(revision), @@ -85,7 +85,20 @@ module Gitlab request.not_in_refs += not_in end - response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_new_lfs_pointers, request, timeout: GitalyClient.medium_timeout) + timeout = + if dynamic_timeout + [dynamic_timeout, GitalyClient.medium_timeout].min + else + GitalyClient.medium_timeout + end + + response = GitalyClient.call( + @gitaly_repo.storage_name, + :blob_service, + :get_new_lfs_pointers, + request, + timeout: timeout + ) map_lfs_pointers(response) end diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb index 0014ce2689b..41004408dec 100644 --- a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb +++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb @@ -6,7 +6,7 @@ module Gitlab def call(severity, datetime, _, data) time = data.delete :time - data[:params] = utf8_encode_values(data[:params]) if data.has_key?(:params) + data[:params] = process_params(data) attributes = { time: datetime.utc.iso8601(3), @@ -20,6 +20,14 @@ module Gitlab private + def process_params(data) + return [] unless data.has_key?(:params) + + data[:params] + .each_pair + .map { |k, v| { key: k, value: utf8_encode_values(v) } } + end + def utf8_encode_values(data) case data when Hash diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb index 28a9fe29465..d5f94ad04f1 100644 --- a/lib/gitlab/identifier.rb +++ b/lib/gitlab/identifier.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true # Detect user based on identifier like -# key-13 or user-36 or last commit +# key-13 or user-36 module Gitlab module Identifier - def identify(identifier, project = nil, newrev = nil) - if identifier.blank? - identify_using_commit(project, newrev) - elsif identifier =~ /\Auser-\d+\Z/ + def identify(identifier) + if identifier =~ /\Auser-\d+\Z/ # git push over http identify_using_user(identifier) elsif identifier =~ /\Akey-\d+\Z/ @@ -16,19 +14,6 @@ module Gitlab end end - # Tries to identify a user based on a commit SHA. - def identify_using_commit(project, ref) - return if project.nil? && ref.nil? - - commit = project.commit(ref) - - return if !commit || !commit.author_email - - identify_with_cache(:email, commit.author_email) do - commit.author - end - end - # Tries to identify a user based on a user identifier (e.g. "user-123"). # rubocop: disable CodeReuse/ActiveRecord def identify_using_user(identifier) diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index e88a15b8acd..f266177bec1 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -13,11 +13,21 @@ module Gitlab class KubeClient include Gitlab::Utils::StrongMemoize - SUPPORTED_API_GROUPS = [ - 'api', - 'apis/rbac.authorization.k8s.io', - 'apis/extensions' - ].freeze + SUPPORTED_API_GROUPS = { + core: { group: 'api', version: 'v1' }, + rbac: { group: 'apis/rbac.authorization.k8s.io', version: 'v1' }, + extensions: { group: 'apis/extensions', version: 'v1beta1' } + }.freeze + + SUPPORTED_API_GROUPS.each do |name, params| + client_method_name = "#{name}_client".to_sym + + define_method(client_method_name) do + strong_memoize(client_method_name) do + build_kubeclient(params[:group], params[:version]) + end + end + end # Core API methods delegates to the core api group client delegate :get_pods, @@ -62,48 +72,21 @@ module Gitlab :watch_pod_log, to: :core_client - def initialize(api_prefix, api_groups = ['api'], api_version = 'v1', **kubeclient_options) - raise ArgumentError unless check_api_groups_supported?(api_groups) + attr_reader :api_prefix, :kubeclient_options + def initialize(api_prefix, **kubeclient_options) @api_prefix = api_prefix - @api_groups = api_groups - @api_version = api_version @kubeclient_options = kubeclient_options end - def discover! - clients.each(&:discover) - end - - def clients - hashed_clients.values - end - - def core_client - hashed_clients['api'] - end - - def rbac_client - hashed_clients['apis/rbac.authorization.k8s.io'] - end - - def extensions_client - hashed_clients['apis/extensions'] - end - - def hashed_clients - strong_memoize(:hashed_clients) do - @api_groups.map do |api_group| - api_url = join_api_url(@api_prefix, api_group) - [api_group, ::Kubeclient::Client.new(api_url, @api_version, **@kubeclient_options)] - end.to_h - end - end - private - def check_api_groups_supported?(api_groups) - api_groups.all? {|api_group| SUPPORTED_API_GROUPS.include?(api_group) } + def build_kubeclient(api_group, api_version) + ::Kubeclient::Client.new( + join_api_url(api_prefix, api_group), + api_version, + **kubeclient_options + ) end def join_api_url(api_prefix, api_path) diff --git a/lib/gitlab/patch/draw_route.rb b/lib/gitlab/patch/draw_route.rb new file mode 100644 index 00000000000..b00244a6e04 --- /dev/null +++ b/lib/gitlab/patch/draw_route.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# We're patching `ActionDispatch::Routing::Mapper` in +# config/initializers/routing_draw.rb +module Gitlab + module Patch + module DrawRoute + RoutesNotFound = Class.new(StandardError) + + def draw(routes_name) + drawn_any = draw_ce(routes_name) | draw_ee(routes_name) + + drawn_any || raise(RoutesNotFound.new("Cannot find #{routes_name}")) + end + + def draw_ce(routes_name) + draw_route(route_path("config/routes/#{routes_name}.rb")) + end + + def draw_ee(_) + true + end + + def route_path(routes_name) + Rails.root.join(routes_name) + end + + def draw_route(path) + if File.exist?(path) + instance_eval(File.read(path)) + true + else + false + end + end + end + end +end diff --git a/lib/tasks/gitlab/ldap.rake b/lib/tasks/gitlab/ldap.rake index c66a2a263dc..0459de27c96 100644 --- a/lib/tasks/gitlab/ldap.rake +++ b/lib/tasks/gitlab/ldap.rake @@ -1,7 +1,7 @@ namespace :gitlab do namespace :ldap do desc 'GitLab | LDAP | Rename provider' - task :rename_provider, [:old_provider, :new_provider] => :environment do |_, args| + task :rename_provider, [:old_provider, :new_provider] => :gitlab_environment do |_, args| old_provider = args[:old_provider] || prompt('What is the old provider? Ex. \'ldapmain\': '.color(:blue)) new_provider = args[:new_provider] || diff --git a/lib/tasks/haml-lint.rake b/lib/tasks/haml-lint.rake index ad2d034b0b4..786efd14b1a 100644 --- a/lib/tasks/haml-lint.rake +++ b/lib/tasks/haml-lint.rake @@ -2,5 +2,16 @@ unless Rails.env.production? require 'haml_lint/rake_task' require 'haml_lint/inline_javascript' + # Workaround for warnings from parser/current + # Keep it even if it no longer emits any warnings, + # because we'll still see warnings in console/server anyway, + # and we don't need to break static-analysis for this. + task :haml_lint do + require 'parser' + def Parser.warn(*args) + puts(*args) # static-analysis ignores stdout if status is 0 + end + end + HamlLint::RakeTask.new end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 40d45d0dee9..f2a23f25acf 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -648,6 +648,9 @@ msgstr "" msgid "Are you sure you want to reset the health check token?" msgstr "" +msgid "Are you sure you want to stop this environment?" +msgstr "" + msgid "Are you sure?" msgstr "" @@ -2324,6 +2327,12 @@ msgstr "" msgid "DeployTokens|Your new project deploy token has been created." msgstr "" +msgid "Deployed to" +msgstr "" + +msgid "Deploying to" +msgstr "" + msgid "Deprioritize label" msgstr "" @@ -2750,6 +2759,9 @@ msgstr "" msgid "Failed to check related branches." msgstr "" +msgid "Failed to deploy to" +msgstr "" + msgid "Failed to load emoji list." msgstr "" @@ -3599,6 +3611,9 @@ msgstr "" msgid "List available repositories" msgstr "" +msgid "List view" +msgstr "" + msgid "List your Bitbucket Server repositories" msgstr "" @@ -4027,9 +4042,6 @@ msgstr "" msgid "No" msgstr "" -msgid "No Assignee" -msgstr "" - msgid "No Label" msgstr "" @@ -5604,6 +5616,9 @@ msgstr "" msgid "Something went wrong while fetching comments. Please try again." msgstr "" +msgid "Something went wrong while fetching the environments for this merge request. Please try again." +msgstr "" + msgid "Something went wrong while fetching the projects." msgstr "" @@ -5781,6 +5796,12 @@ msgstr "" msgid "Start a %{new_merge_request} with these changes" msgstr "" +msgid "Start and due date" +msgstr "" + +msgid "Start date" +msgstr "" + msgid "Start the Runner!" msgstr "" @@ -6519,6 +6540,9 @@ msgstr "" msgid "Track time with quick actions" msgstr "" +msgid "Tree view" +msgstr "" + msgid "Trending" msgstr "" diff --git a/package.json b/package.json index 5b0a92ee7a1..086617dc265 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-import-meta": "^7.0.0", "@babel/preset-env": "^7.1.0", - "@gitlab-org/gitlab-svgs": "^1.32.0", + "@gitlab-org/gitlab-svgs": "^1.33.0", "@gitlab-org/gitlab-ui": "^1.8.0", "autosize": "^4.0.0", "axios": "^0.17.1", @@ -39,7 +39,6 @@ module QA module Factory autoload :ApiFabricator, 'qa/factory/api_fabricator' autoload :Base, 'qa/factory/base' - autoload :Dependency, 'qa/factory/dependency' autoload :Product, 'qa/factory/product' module Resource @@ -100,7 +99,8 @@ module QA module Integration autoload :Github, 'qa/scenario/test/integration/github' - autoload :LDAP, 'qa/scenario/test/integration/ldap' + autoload :LDAPNoTLS, 'qa/scenario/test/integration/ldap_no_tls' + autoload :LDAPTLS, 'qa/scenario/test/integration/ldap_tls' autoload :InstanceSAML, 'qa/scenario/test/integration/instance_saml' autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes' autoload :Mattermost, 'qa/scenario/test/integration/mattermost' diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md index 10140e39510..cfce096ab39 100644 --- a/qa/qa/factory/README.md +++ b/qa/qa/factory/README.md @@ -26,11 +26,7 @@ module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size - - def initialize(name) - @name = name - end + attr_accessor :name def fabricate! Page::Dashboard::Index.perform do |dashboard_index| @@ -64,21 +60,10 @@ module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size - - def initialize(name) - @name = name - end + attr_accessor :name def fabricate! - Page::Dashboard::Index.perform do |dashboard_index| - dashboard_index.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end + # ... same as before end def api_get_path @@ -103,33 +88,69 @@ end The [`Project` factory](./resource/project.rb) is a good real example of Browser UI and API implementations. -### Define dependencies +### Define attributes + +After the resource is fabricated, we would like to access the attributes on +the resource. We define the attributes with `attribute` method. Suppose +we want to access the name on the resource, we could change `attr_accessor` +to `attribute`: + +```ruby +module QA + module Factory + module Resource + class Shirt < Factory::Base + attribute :name -A resource may need an other resource to exist first. For instance, a project + # ... same as before + end + end + end +end +``` + +The difference between `attr_accessor` and `attribute` is that by using +`attribute` it can also be accessed from the product: + +```ruby +shirt = + QA::Factory::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.name # => "GitLab QA" +``` + +In the above example, if we use `attr_accessor :name` then `shirt.name` won't +be available. On the other hand, using `attribute :name` will allow you to use +`shirt.name`, so most of the time you'll want to use `attribute` instead of +`attr_accessor` unless we clearly don't need it for the product. + +#### Resource attributes + +A resource may need another resource to exist first. For instance, a project needs a group to be created in. -To define a dependency, you can use the `dependency` DSL method. -The first argument is a factory class, then you should pass `as: <name>` to give -a name to the dependency. -That will allow access to the dependency from your resource object's methods. -You would usually use it in `#fabricate!`, `#api_get_path`, `#api_post_path`, -`#api_post_body`. +To define a resource attribute, you can use the `attribute` method with a +block using the other factory to fabricate the resource. -Let's take the `Shirt` factory, and add a `project` dependency to it: +That will allow access to the other resource from your resource object's +methods. You would usually use it in `#fabricate!`, `#api_get_path`, +`#api_post_path`, `#api_post_body`. + +Let's take the `Shirt` factory, and add a `project` attribute to it: ```ruby module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size + attribute :name - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-create-a-shirt' - end - - def initialize(name) - @name = name + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end end def fabricate! @@ -164,19 +185,19 @@ module QA end ``` -**Note that dependencies are always built via the API fabrication method if -supported by their factories.** +**Note that all the attributes are lazily constructed. This means if you want +a specific attribute to be fabricated first, you'll need to call the +attribute method first even if you're not using it.** -### Define attributes on the created resource +#### Product data attributes Once created, you may want to populate a resource with attributes that can be found in the Web page, or in the API response. For instance, once you create a project, you may want to store its repository SSH URL as an attribute. -To define an attribute, you can use the `product` DSL method. -The first argument is the attribute name, then you should define a name for the -dependency to be accessible from your resource object's methods. +Again we could use the `attribute` method with a block, using a page object +to retrieve the data on the page. Let's take the `Shirt` factory, and define a `:brand` attribute: @@ -185,22 +206,74 @@ module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size + attribute :name - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-create-a-shirt' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end end # Attribute populated from the Browser UI (using the block) - product :brand do + attribute :brand do Page::Shirt::Show.perform do |shirt_show| shirt_show.fetch_brand_from_page end end - def initialize(name) - @name = name - end + # ... same as before + end + end + end +end +``` + +**Note again that all the attributes are lazily constructed. This means if +you call `shirt.brand` after moving to the other page, it'll not properly +retrieve the data because we're no longer on the expected page.** + +Consider this: + +```ruby +shirt = + QA::Factory::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.project.visit! + +shirt.brand # => FAIL! +``` + +The above example will fail because now we're on the project page, trying to +construct the brand data from the shirt page, however we moved to the project +page already. There are two ways to solve this, one is that we could try to +retrieve the brand before visiting the project again: + +```ruby +shirt = + QA::Factory::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.brand # => OK! + +shirt.project.visit! + +shirt.brand # => OK! +``` + +The attribute will be stored in the instance therefore all the following calls +will be fine, using the data previously constructed. If we think that this +might be too brittle, we could eagerly construct the data right before +ending fabrication: + +```ruby +module QA + module Factory + module Resource + class Shirt < Factory::Base + # ... same as before def fabricate! project.visit! @@ -213,20 +286,8 @@ module QA shirt_new.set_name(name) shirt_new.create_shirt! end - end - def api_get_path - "/project/#{project.path}/shirt/#{name}" - end - - def api_post_path - "/project/#{project.path}/shirts" - end - - def api_post_body - { - name: name - } + brand # Eagerly construct the data end end end @@ -234,74 +295,48 @@ module QA end ``` -#### Inherit a factory's attribute +This will make sure we construct the data right after we created the shirt. +The drawback for this will become we're forced to construct the data even +if we don't really need to use it. -Sometimes, you want a resource to inherit its factory attributes. For instance, -it could be useful to pass the `size` attribute from the `Shirt` factory to the -created resource. -You can do that by defining `product :attribute_name` without a block. - -Let's take the `Shirt` factory, and define a `:name` and a `:size` attributes: +Alternatively, we could just make sure we're on the right page before +constructing the brand data: ```ruby module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-create-a-shirt' - end + attribute :name - # Attribute from the Browser UI (using the block) - product :brand do - Page::Shirt::Show.perform do |shirt_show| - shirt_show.fetch_brand_from_page + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' end end - # Attribute inherited from the Shirt factory if present, - # or a QA::Factory::Product::NoValueError is raised otherwise - product :name - product :size - - def initialize(name) - @name = name - end - - def fabricate! - project.visit! - - Page::Project::Show.perform do |project_show| - project_show.go_to_new_shirt - end + # Attribute populated from the Browser UI (using the block) + attribute :brand do + back_url = current_url + visit! - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page end - end - def api_get_path - "/project/#{project.path}/shirt/#{name}" + visit(back_url) end - def api_post_path - "/project/#{project.path}/shirts" - end - - def api_post_body - { - name: name - } - end + # ... same as before end end end end ``` +This will make sure it's on the shirt page before constructing brand, and +move back to the previous page to avoid breaking the state. + #### Define an attribute based on an API response Sometimes, you want to define a resource attribute based on the API response @@ -311,7 +346,6 @@ the API returns ```ruby { brand: 'a-brand-new-brand', - size: 'extra-small', style: 't-shirt', materials: [[:cotton, 80], [:polyamide, 20]] } @@ -320,18 +354,6 @@ the API returns you may want to store `style` as-is in the resource, and fetch the first value of the first `materials` item in a `main_fabric` attribute. -For both attributes, you will need to define an inherited attribute, as shown -in "Inherit a factory's attribute" above, but in the case of `main_fabric`, you -will need to implement the -`#transform_api_resource` method to first populate the `:main_fabric` key in the -API response so that it can be used later to automatically populate the -attribute on your resource. - -If an attribute can only be retrieved from the API response, you should define -a block to give it a default value, otherwise you could get a -`QA::Factory::Product::NoValueError` when creating your resource via the -Browser UI. - Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric` attributes: @@ -340,69 +362,21 @@ module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size + # ... same as before - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-create-a-shirt' - end - - # Attribute fetched from the API response if present, - # or from the Browser UI otherwise (using the block) - product :brand do - Page::Shirt::Show.perform do |shirt_show| - shirt_show.fetch_brand_from_page - end - end - - # Attribute fetched from the API response if present, - # or from the Shirt factory if present, - # or a QA::Factory::Product::NoValueError is raised otherwise - product :name - product :size - product :style do - 'unknown' - end - product :main_fabric do - 'unknown' - end - - def initialize(name) - @name = name - end - - def fabricate! - project.visit! - - Page::Project::Show.perform do |project_show| - project_show.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - end + # Attribute from the Shirt factory if present, + # or fetched from the API response if present, + # or a QA::Factory::Base::NoValueError is raised otherwise + attribute :style - def api_get_path - "/project/#{project.path}/shirt/#{name}" + # If the attribute from the Shirt factory is not present, + # and if the API does not contain this field, this block will be + # used to construct the value based on the API response. + attribute :main_fabric do + api_response.&dig(:materials, 0, 0) end - def api_post_path - "/project/#{project.path}/shirts" - end - - def api_post_body - { - name: name - } - end - - private - - def transform_api_resource(api_response) - api_response[:main_fabric] = api_response[:materials][0][0] - api_response - end + # ... same as before end end end @@ -411,11 +385,10 @@ end **Notes on attributes precedence:** +- attributes from the factory have the highest precedence - attributes from the API response take precedence over attributes from the - Browser UI -- attributes from the Browser UI take precedence over attributes from the - factory (i.e inherited) -- attributes without a value will raise a `QA::Factory::Product::NoValueError` error + block (usually from Browser UI) +- attributes without a value will raise a `QA::Factory::Base::NoValueError` error ## Creating resources in your tests @@ -428,42 +401,40 @@ Here is an example that will use the API fabrication method under the hood since it's supported by the `Shirt` factory: ```ruby -my_shirt = Factory::Resource::Shirt.fabricate!('my-shirt') do |shirt| - shirt.size = 'small' +my_shirt = Factory::Resource::Shirt.fabricate! do |shirt| + shirt.name = 'my-shirt' end +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response -expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute -expect(page).to have_text(my_shirt.size) # => "extra-small" from the API response expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response -expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the (transformed) API response +expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block ``` -If you explicitely want to use the Browser UI fabrication method, you can call +If you explicitly want to use the Browser UI fabrication method, you can call the `.fabricate_via_browser_ui!` method instead: ```ruby -my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui!('my-shirt') do |shirt| - shirt.size = 'small' +my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt| + shirt.name = 'my-shirt' end -expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page -expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute -expect(page).to have_text(my_shirt.size) # => "small" from the inherited factory's attribute -expect(page).to have_text(my_shirt.style) # => "unknown" from the attribute block -expect(page).to have_text(my_shirt.main_fabric) # => "unknown" from the attribute block +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute +expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block +expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided +expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response) ``` -You can also explicitely use the API fabrication method, by calling the +You can also explicitly use the API fabrication method, by calling the `.fabricate_via_api!` method: ```ruby -my_shirt = Factory::Resource::Shirt.fabricate_via_api!('my-shirt') do |shirt| - shirt.size = 'small' +my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt| + shirt.name = 'my-shirt' end ``` -In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!('my-shirt')`. +In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`. ## Where to ask for help? diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb index e1dc23d350d..e82e16f9415 100644 --- a/qa/qa/factory/base.rb +++ b/qa/qa/factory/base.rb @@ -10,13 +10,42 @@ module QA include ApiFabricator extend Capybara::DSL - def_delegators :evaluator, :dependency, :dependencies - def_delegators :evaluator, :product, :attributes + NoValueError = Class.new(RuntimeError) + + def_delegators :evaluator, :attribute def fabricate!(*_args) raise NotImplementedError end + def visit! + visit(web_url) + end + + private + + def populate_attribute(name, block) + value = attribute_value(name, block) + + raise NoValueError, "No value was computed for product #{name} of factory #{self.class.name}." unless value + + value + end + + def attribute_value(name, block) + api_value = api_resource&.dig(name) + + if api_value && block + log_having_both_api_result_and_block(name, api_value) + end + + api_value || (block && instance_exec(&block)) + end + + def log_having_both_api_result_and_block(name, api_value) + QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored." + end + def self.fabricate!(*args, &prepare_block) fabricate_via_api!(*args, &prepare_block) rescue NotImplementedError @@ -52,13 +81,10 @@ module QA def self.do_fabricate!(factory:, prepare_block:, parents: []) prepare_block.call(factory) if prepare_block - dependencies.each do |signature| - Factory::Dependency.new(factory, signature).build!(parents: parents + [self]) - end - resource_web_url = yield + factory.web_url = resource_web_url - Factory::Product.populate!(factory, resource_web_url) + Factory::Product.new(factory) end private_class_method :do_fabricate! @@ -85,31 +111,40 @@ module QA end private_class_method :evaluator - class DSL - attr_reader :dependencies, :attributes + def self.dynamic_attributes + const_get(:DynamicAttributes) + rescue NameError + mod = const_set(:DynamicAttributes, Module.new) + + include mod + + mod + end + def self.attributes_names + dynamic_attributes.instance_methods(false).sort.grep_v(/=$/) + end + + class DSL def initialize(base) @base = base - @dependencies = [] - @attributes = [] end - def dependency(factory, as:, &block) - as.tap do |name| - @base.class_eval { attr_accessor name } + def attribute(name, &block) + @base.dynamic_attributes.module_eval do + attr_writer(name) - Dependency::Signature.new(name, factory, block).tap do |signature| - @dependencies << signature + define_method(name) do + instance_variable_get("@#{name}") || + instance_variable_set( + "@#{name}", + populate_attribute(name, block)) end end end - - def product(attribute, &block) - Product::Attribute.new(attribute, block).tap do |signature| - @attributes << signature - end - end end + + attribute :web_url end end end diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb deleted file mode 100644 index 655e2677db0..00000000000 --- a/qa/qa/factory/dependency.rb +++ /dev/null @@ -1,28 +0,0 @@ -module QA - module Factory - class Dependency - Signature = Struct.new(:name, :factory, :block) - - def initialize(caller_factory, dependency_signature) - @caller_factory = caller_factory - @dependency_signature = dependency_signature - end - - def overridden? - !!@caller_factory.public_send(@dependency_signature.name) - end - - def build!(parents: []) - return if overridden? - - dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory| - @dependency_signature.block&.call(factory, @caller_factory) - end - - dependency.tap do |dependency| - @caller_factory.public_send("#{@dependency_signature.name}=", dependency) - end - end - end - end -end diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb index 17fe908eaa2..34df0bda8e5 100644 --- a/qa/qa/factory/product.rb +++ b/qa/qa/factory/product.rb @@ -5,46 +5,31 @@ module QA class Product include Capybara::DSL - NoValueError = Class.new(RuntimeError) + attr_reader :factory - attr_reader :factory, :web_url - - Attribute = Struct.new(:name, :block) - - def initialize(factory, web_url) + def initialize(factory) @factory = factory - @web_url = web_url - populate_attributes! + define_attributes end def visit! visit(web_url) end - def self.populate!(factory, web_url) - new(factory, web_url) + def populate(*attributes) + attributes.each(&method(:public_send)) end private - def populate_attributes! - factory.class.attributes.each do |attribute| - instance_exec(factory, attribute.block) do |factory, block| - value = attribute_value(attribute, block) - - raise NoValueError, "No value was computed for product #{attribute.name} of factory #{factory.class.name}." unless value - - define_singleton_method(attribute.name) { value } + def define_attributes + factory.class.attributes_names.each do |name| + define_singleton_method(name) do + factory.public_send(name) end end end - - def attribute_value(attribute, block) - factory.api_resource&.dig(attribute.name) || - (block && block.call(factory)) || - (factory.respond_to?(attribute.name) && factory.public_send(attribute.name)) - end end end end diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb index 6f878396f0e..a9dfbc0a783 100644 --- a/qa/qa/factory/repository/project_push.rb +++ b/qa/qa/factory/repository/project_push.rb @@ -2,13 +2,14 @@ module QA module Factory module Repository class ProjectPush < Factory::Repository::Push - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-code' - project.description = 'Project with repository' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-code' + resource.description = 'Project with repository' + end end - product :output - product :project + attribute :output def initialize @file_name = 'file.txt' diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/factory/repository/wiki_push.rb index ecc6cc18c88..25b6ffe8323 100644 --- a/qa/qa/factory/repository/wiki_push.rb +++ b/qa/qa/factory/repository/wiki_push.rb @@ -2,10 +2,12 @@ module QA module Factory module Repository class WikiPush < Factory::Repository::Push - dependency Factory::Resource::Wiki, as: :wiki do |wiki| - wiki.title = 'Home' - wiki.content = '# My First Wiki Content' - wiki.message = 'Update home' + attribute :wiki do + Factory::Resource::Wiki.fabricate! do |resource| + resource.title = 'Home' + resource.content = '# My First Wiki Content' + resource.message = 'Update home' + end end def initialize diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb index f3b52565d17..b05d1e252ec 100644 --- a/qa/qa/factory/resource/branch.rb +++ b/qa/qa/factory/resource/branch.rb @@ -5,8 +5,10 @@ module QA attr_accessor :project, :branch_name, :allow_to_push, :allow_to_merge, :protected - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'protected-branch-project' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'protected-branch-project' + end end def initialize @@ -43,9 +45,7 @@ module QA # to `allow_to_push` variable. return branch unless @protected - Page::Project::Menu.act do - click_repository_settings - end + Page::Project::Menu.perform(&:click_repository_settings) Page::Project::Settings::Repository.perform do |setting| setting.expand_protected_branches do |page| diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb index 4c53c500c27..aea99c9f80d 100644 --- a/qa/qa/factory/resource/deploy_key.rb +++ b/qa/qa/factory/resource/deploy_key.rb @@ -4,11 +4,11 @@ module QA class DeployKey < Factory::Base attr_accessor :title, :key - product :fingerprint do |resource| - Page::Project::Settings::Repository.act do - expand_deploy_keys do |key| - key_offset = key.key_titles.index do |title| - title.text == resource.title + attribute :fingerprint do + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |key| + key_offset = key.key_titles.index do |key_title| + key_title.text == title end key.key_fingerprints[key_offset].text @@ -16,17 +16,17 @@ module QA end end - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-deploy' - project.description = 'project for adding deploy key test' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy key test' + end end def fabricate! project.visit! - Page::Project::Menu.act do - click_repository_settings - end + Page::Project::Menu.perform(&:click_repository_settings) Page::Project::Settings::Repository.perform do |setting| setting.expand_deploy_keys do |page| diff --git a/qa/qa/factory/resource/deploy_token.rb b/qa/qa/factory/resource/deploy_token.rb index 159f79ac50b..68e98f0aa01 100644 --- a/qa/qa/factory/resource/deploy_token.rb +++ b/qa/qa/factory/resource/deploy_token.rb @@ -4,25 +4,27 @@ module QA class DeployToken < Factory::Base attr_accessor :name, :expires_at - product :username do |resource| - Page::Project::Settings::Repository.act do - expand_deploy_tokens do |token| + attribute :username do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| token.token_username end end end - product :password do |password| - Page::Project::Settings::Repository.act do - expand_deploy_tokens do |token| + attribute :password do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| token.token_password end end end - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-deploy' - project.description = 'project for adding deploy token test' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy token test' + end end def fabricate! diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb index f8dea06d361..1148876c2d3 100644 --- a/qa/qa/factory/resource/file.rb +++ b/qa/qa/factory/resource/file.rb @@ -8,8 +8,10 @@ module QA :content, :commit_message - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-new-file' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-new-file' + end end def initialize @@ -21,7 +23,7 @@ module QA def fabricate! project.visit! - Page::Project::Show.act { create_new_file! } + Page::Project::Show.perform(&:create_new_file!) Page::File::Form.perform do |page| page.add_name(@name) diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb index 6e2a668df64..0fac4377040 100644 --- a/qa/qa/factory/resource/fork.rb +++ b/qa/qa/factory/resource/fork.rb @@ -2,17 +2,19 @@ module QA module Factory module Resource class Fork < Factory::Base - dependency Factory::Repository::ProjectPush, as: :push + attribute :push do + Factory::Repository::ProjectPush.fabricate! + end - dependency Factory::Resource::User, as: :user do |user| - if Runtime::Env.forker? - user.username = Runtime::Env.forker_username - user.password = Runtime::Env.forker_password + attribute :user do + Factory::Resource::User.fabricate! do |resource| + if Runtime::Env.forker? + resource.username = Runtime::Env.forker_username + resource.password = Runtime::Env.forker_password + end end end - product :user - def visit_project_with_retry # The user intermittently fails to stay signed in after visiting the # project page. The new user is registered and then signs in and a @@ -48,15 +50,20 @@ module QA end def fabricate! + push + user + visit_project_with_retry - Page::Project::Show.act { fork_project } + Page::Project::Show.perform(&:fork_project) Page::Project::Fork::New.perform do |fork_new| fork_new.choose_namespace(user.name) end - Page::Layout::Banner.act { has_notice?('The project was successfully forked.') } + Page::Layout::Banner.perform do |page| + page.has_notice?('The project was successfully forked.') + end end end end diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb index 2688328df92..45e49da86f9 100644 --- a/qa/qa/factory/resource/group.rb +++ b/qa/qa/factory/resource/group.rb @@ -4,12 +4,12 @@ module QA class Group < Factory::Base attr_accessor :path, :description - dependency Factory::Resource::Sandbox, as: :sandbox - - product :id do - true # We don't retrieve the Group ID when using the Browser UI + attribute :sandbox do + Factory::Resource::Sandbox.fabricate! end + attribute :id + def initialize @path = Runtime::Namespace.name @description = "QA test run at #{Runtime::Namespace.time}" diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb index 9b444cb0bf1..3a28e0d5aa6 100644 --- a/qa/qa/factory/resource/issue.rb +++ b/qa/qa/factory/resource/issue.rb @@ -2,22 +2,21 @@ module QA module Factory module Resource class Issue < Factory::Base - attr_accessor :title, :description, :project + attr_writer :description - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-for-issues' - project.description = 'project for adding issues' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-for-issues' + resource.description = 'project for adding issues' + end end - product :project - product :title + attribute :title def fabricate! project.visit! - Page::Project::Show.act do - go_to_new_issue - end + Page::Project::Show.perform(&:go_to_new_issue) Page::Project::Issue::New.perform do |page| page.add_title(@title) diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb index cdee35c54e3..aac6864f42f 100644 --- a/qa/qa/factory/resource/kubernetes_cluster.rb +++ b/qa/qa/factory/resource/kubernetes_cluster.rb @@ -7,24 +7,21 @@ module QA attr_writer :project, :cluster, :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner - product :ingress_ip do - Page::Project::Operations::Kubernetes::Show.perform do |page| - page.ingress_ip - end + attribute :ingress_ip do + Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) end def fabricate! @project.visit! - Page::Project::Menu.act { click_operations_kubernetes } + Page::Project::Menu.perform( + &:click_operations_kubernetes) - Page::Project::Operations::Kubernetes::Index.perform do |page| - page.add_kubernetes_cluster - end + Page::Project::Operations::Kubernetes::Index.perform( + &:add_kubernetes_cluster) - Page::Project::Operations::Kubernetes::Add.perform do |page| - page.add_existing_cluster - end + Page::Project::Operations::Kubernetes::Add.perform( + &:add_existing_cluster) Page::Project::Operations::Kubernetes::AddExisting.perform do |page| page.set_cluster_name(@cluster.cluster_name) diff --git a/qa/qa/factory/resource/label.rb b/qa/qa/factory/resource/label.rb index 4080f15bf66..32bc519b48c 100644 --- a/qa/qa/factory/resource/label.rb +++ b/qa/qa/factory/resource/label.rb @@ -4,14 +4,14 @@ module QA module Factory module Resource class Label < Factory::Base - attr_accessor :title, - :description, - :color + attr_accessor :description, :color - product(:title) { |factory| factory.title } + attribute :title - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-label' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-label' + end end def initialize @@ -23,8 +23,8 @@ module QA def fabricate! project.visit! - Page::Project::Menu.act { go_to_labels } - Page::Label::Index.act { go_to_new_label } + Page::Project::Menu.perform(&:go_to_labels) + Page::Label::Index.perform(&:go_to_new_label) Page::Label::New.perform do |page| page.fill_title(@title) diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb index d30da8a3db0..92b8bdf4a21 100644 --- a/qa/qa/factory/resource/merge_request.rb +++ b/qa/qa/factory/resource/merge_request.rb @@ -12,27 +12,33 @@ module QA :milestone, :labels - product :project - product :source_branch + attribute :source_branch - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-merge-request' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-merge-request' + end end - dependency Factory::Repository::ProjectPush, as: :target do |push, factory| - factory.project.visit! - push.project = factory.project - push.branch_name = 'master' - push.remote_branch = factory.target_branch + attribute :target do + project.visit! + + Factory::Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = 'master' + resource.remote_branch = target_branch + end end - dependency Factory::Repository::ProjectPush, as: :source do |push, factory| - push.project = factory.project - push.branch_name = factory.target_branch - push.remote_branch = factory.source_branch - push.new_branch = false - push.file_name = "added_file.txt" - push.file_content = "File Added" + attribute :source do + Factory::Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = target_branch + resource.remote_branch = source_branch + resource.new_branch = false + resource.file_name = "added_file.txt" + resource.file_content = "File Added" + end end def initialize @@ -46,8 +52,10 @@ module QA end def fabricate! + target + source project.visit! - Page::Project::Show.act { new_merge_request } + Page::Project::Show.perform(&:new_merge_request) Page::MergeRequest::New.perform do |page| page.fill_title(@title) page.fill_description(@description) diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb index 6caaf65f673..fbe062539b9 100644 --- a/qa/qa/factory/resource/merge_request_from_fork.rb +++ b/qa/qa/factory/resource/merge_request_from_fork.rb @@ -4,19 +4,24 @@ module QA class MergeRequestFromFork < MergeRequest attr_accessor :fork_branch - dependency Factory::Resource::Fork, as: :fork + attribute :fork do + Factory::Resource::Fork.fabricate! + end - dependency Factory::Repository::ProjectPush, as: :push do |push, factory| - push.project = factory.fork - push.branch_name = factory.fork_branch - push.file_name = 'file2.txt' - push.user = factory.fork.user + attribute :push do + Factory::Repository::ProjectPush.fabricate! do |resource| + resource.project = fork + resource.branch_name = fork_branch + resource.file_name = 'file2.txt' + resource.user = fork.user + end end def fabricate! + push fork.visit! - Page::Project::Show.act { new_merge_request } - Page::MergeRequest::New.act { create_merge_request } + Page::Project::Show.perform(&:new_merge_request) + Page::MergeRequest::New.perform(&:create_merge_request) end end end diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb index 166054cfcdc..ceb0f1c3d75 100644 --- a/qa/qa/factory/resource/personal_access_token.rb +++ b/qa/qa/factory/resource/personal_access_token.rb @@ -7,13 +7,13 @@ module QA class PersonalAccessToken < Factory::Base attr_accessor :name - product :access_token do - Page::Profile::PersonalAccessTokens.act { created_access_token } + attribute :access_token do + Page::Profile::PersonalAccessTokens.perform(&:created_access_token) end def fabricate! - Page::Main::Menu.act { go_to_profile_settings } - Page::Profile::Menu.act { click_access_tokens } + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_access_tokens) Page::Profile::PersonalAccessTokens.perform do |page| page.fill_token_name(name || 'api-test-token') diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb index 105e42b23ec..f691ae5a342 100644 --- a/qa/qa/factory/resource/project.rb +++ b/qa/qa/factory/resource/project.rb @@ -4,25 +4,24 @@ module QA module Factory module Resource class Project < Factory::Base - attr_accessor :description - attr_reader :name + attribute :name + attribute :description - dependency Factory::Resource::Group, as: :group - - product :group - product :name + attribute :group do + Factory::Resource::Group.fabricate! + end - product :repository_ssh_location do - Page::Project::Show.act do - choose_repository_clone_ssh - repository_location + attribute :repository_ssh_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_ssh + page.repository_location end end - product :repository_http_location do - Page::Project::Show.act do - choose_repository_clone_http - repository_location + attribute :repository_http_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_http + page.repository_location end end @@ -37,7 +36,7 @@ module QA def fabricate! group.visit! - Page::Group::Show.act { go_to_new_project } + Page::Group::Show.perform(&:go_to_new_project) Page::Project::New.perform do |page| page.choose_test_namespace diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb index a45e7fee03b..f62092ae122 100644 --- a/qa/qa/factory/resource/project_imported_from_github.rb +++ b/qa/qa/factory/resource/project_imported_from_github.rb @@ -6,14 +6,16 @@ module QA class ProjectImportedFromGithub < Resource::Project attr_writer :personal_access_token, :github_repository_path - dependency Factory::Resource::Group, as: :group + attribute :group do + Factory::Resource::Group.fabricate! + end - product :name + attribute :name def fabricate! group.visit! - Page::Group::Show.act { go_to_new_project } + Page::Group::Show.perform(&:go_to_new_project) Page::Project::New.perform do |page| page.go_to_import_project diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb index 35383842142..cfda58dc103 100644 --- a/qa/qa/factory/resource/project_milestone.rb +++ b/qa/qa/factory/resource/project_milestone.rb @@ -3,11 +3,12 @@ module QA module Resource class ProjectMilestone < Factory::Base attr_accessor :description - attr_reader :title - dependency Factory::Resource::Project, as: :project + attribute :project do + Factory::Resource::Project.fabricate! + end - product :title + attribute :title def title=(title) @title = "#{title}-#{SecureRandom.hex(4)}" @@ -17,12 +18,12 @@ module QA def fabricate! project.visit! - Page::Project::Menu.act do - click_issues - click_milestones + Page::Project::Menu.perform do |page| + page.click_issues + page.click_milestones end - Page::Project::Milestone::Index.act { click_new_milestone } + Page::Project::Milestone::Index.perform(&:click_new_milestone) Page::Project::Milestone::New.perform do |milestone_new| milestone_new.set_title(@title) diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb index 7ac65fe6913..7108db1e55a 100644 --- a/qa/qa/factory/resource/runner.rb +++ b/qa/qa/factory/resource/runner.rb @@ -6,9 +6,11 @@ module QA class Runner < Factory::Base attr_writer :name, :tags, :image - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-ci-cd' - project.description = 'Project with CI/CD Pipelines' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-ci-cd' + resource.description = 'Project with CI/CD Pipelines' + end end def name @@ -26,7 +28,7 @@ module QA def fabricate! project.visit! - Page::Project::Menu.act { click_ci_cd_settings } + Page::Project::Menu.perform(&:click_ci_cd_settings) Service::Runner.new(name).tap do |runner| Page::Project::Settings::CICD.perform do |settings| diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb index e592f4e0dd2..56bcda9e2f3 100644 --- a/qa/qa/factory/resource/sandbox.rb +++ b/qa/qa/factory/resource/sandbox.rb @@ -8,17 +8,15 @@ module QA class Sandbox < Factory::Base attr_reader :path - product :id do - true # We don't retrieve the Group ID when using the Browser UI - end - product :path + attribute :id + attribute :path def initialize @path = Runtime::Namespace.sandbox_name end def fabricate! - Page::Main::Menu.act { go_to_groups } + Page::Main::Menu.perform(&:go_to_groups) Page::Dashboard::Groups.perform do |page| if page.has_group?(path) diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb index 4084a7fc2cd..24ba3408810 100644 --- a/qa/qa/factory/resource/secret_variable.rb +++ b/qa/qa/factory/resource/secret_variable.rb @@ -4,15 +4,17 @@ module QA class SecretVariable < Factory::Base attr_accessor :key, :value - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-secret-variables' - project.description = 'project for adding secret variable test' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-secret-variables' + resource.description = 'project for adding secret variable test' + end end def fabricate! project.visit! - Page::Project::Menu.act { click_ci_cd_settings } + Page::Project::Menu.perform(&:click_ci_cd_settings) Page::Project::Settings::CICD.perform do |setting| setting.expand_secret_variables do |page| diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb index a512d071dd4..a48a93fbe65 100644 --- a/qa/qa/factory/resource/ssh_key.rb +++ b/qa/qa/factory/resource/ssh_key.rb @@ -6,21 +6,19 @@ module QA class SSHKey < Factory::Base extend Forwardable - attr_accessor :title - attr_reader :private_key, :public_key, :fingerprint def_delegators :key, :private_key, :public_key, :fingerprint - product :private_key - product :title - product :fingerprint + attribute :private_key + attribute :title + attribute :fingerprint def key @key ||= Runtime::Key::RSA.new end def fabricate! - Page::Main::Menu.act { go_to_profile_settings } - Page::Profile::Menu.act { click_ssh_keys } + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_ssh_keys) Page::Profile::SSHKeys.perform do |page| page.add_key(public_key, title) diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb index 36edf787b64..6e6f46f7a95 100644 --- a/qa/qa/factory/resource/user.rb +++ b/qa/qa/factory/resource/user.rb @@ -5,7 +5,6 @@ module QA module Resource class User < Factory::Base attr_reader :unique_id - attr_writer :username, :password, :name, :email def initialize @unique_id = SecureRandom.hex(8) @@ -31,14 +30,14 @@ module QA defined?(@username) && defined?(@password) end - product :name - product :username - product :email - product :password + attribute :name + attribute :username + attribute :email + attribute :password def fabricate! # Don't try to log-out if we're not logged-in - if Page::Main::Menu.act { has_personal_area?(wait: 0) } + if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } Page::Main::Menu.perform { |main| main.sign_out } end diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb index d697433736e..769f394e85c 100644 --- a/qa/qa/factory/resource/wiki.rb +++ b/qa/qa/factory/resource/wiki.rb @@ -4,9 +4,11 @@ module QA class Wiki < Factory::Base attr_accessor :title, :content, :message - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-for-wikis' - project.description = 'project for adding wikis' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-for-wikis' + resource.description = 'project for adding wikis' + end end def fabricate! diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb index 5e8f883e25f..4e32382f910 100644 --- a/qa/qa/factory/settings/hashed_storage.rb +++ b/qa/qa/factory/settings/hashed_storage.rb @@ -5,9 +5,9 @@ module QA def fabricate!(*traits) raise ArgumentError unless traits.include?(:enabled) - Page::Main::Login.act { sign_in_using_credentials } - Page::Main::Menu.act { go_to_admin_area } - Page::Admin::Menu.act { go_to_repository_settings } + Page::Main::Login.perform(&:sign_in_using_credentials) + Page::Main::Menu.perform(&:go_to_admin_area) + Page::Admin::Menu.perform(&:go_to_repository_settings) Page::Admin::Settings::Repository.perform do |setting| setting.expand_repository_storage do |page| @@ -16,7 +16,7 @@ module QA end end - QA::Page::Main::Menu.act { sign_out } + QA::Page::Main::Menu.perform(&:sign_out) end end end diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index c6a8891d398..27a88534258 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -113,21 +113,17 @@ module QA attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file - def debug? - Runtime::Env.respond_to?(:verbose?) && Runtime::Env.verbose? - end - def ssh_key_set? !private_key_file.nil? end def run(command_str) command = [env_vars, command_str, '2>&1'].compact.join(' ') - warn "DEBUG: command=[#{command}]" if debug? + Runtime::Logger.debug "Git: command=[#{command}]" output, _ = Open3.capture2(command) output = output.chomp.gsub(/\s+$/, '') - warn "DEBUG: output=[#{output}]" if debug? + Runtime::Logger.debug "Git: output=[#{output}]" output end diff --git a/qa/qa/runtime/logger.rb b/qa/qa/runtime/logger.rb index 3baa24de0ec..bd5c4fe5bf5 100644 --- a/qa/qa/runtime/logger.rb +++ b/qa/qa/runtime/logger.rb @@ -7,14 +7,16 @@ module QA module Logger extend SingleForwardable - def_delegators :logger, :debug, :info, :error, :warn, :fatal, :unknown + def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown singleton_class.module_eval do + attr_writer :logger + def logger return @logger if @logger @logger = ::Logger.new Runtime::Env.log_destination - @logger.level = ::Logger::DEBUG + @logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR @logger end end diff --git a/qa/qa/scenario/test/integration/ldap_no_tls.rb b/qa/qa/scenario/test/integration/ldap_no_tls.rb new file mode 100644 index 00000000000..bbf4c847f33 --- /dev/null +++ b/qa/qa/scenario/test/integration/ldap_no_tls.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Integration + class LDAPNoTLS < Test::Instance::All + tags :ldap_no_tls + end + end + end + end +end diff --git a/qa/qa/scenario/test/integration/ldap.rb b/qa/qa/scenario/test/integration/ldap_tls.rb index 769fa389785..2a767e57bc6 100644 --- a/qa/qa/scenario/test/integration/ldap.rb +++ b/qa/qa/scenario/test/integration/ldap_tls.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module QA module Scenario module Test module Integration - class LDAP < Test::Instance::All - tags :ldap + class LDAPTLS < Test::Instance::All + tags :ldap_tls end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb index eb9e0297287..a397df03bd2 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Manage', :orchestrated, :ldap do + context 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls do describe 'LDAP login' do it 'user logs into GitLab using LDAP credentials' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index 45cb5df8252..44071ec3e45 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -22,7 +22,7 @@ module QA end end - context 'Manage', :orchestrated, :ldap, :skip_signup_disabled do + context 'Manage', :orchestrated, :ldap_no_tls, :skip_signup_disabled do describe 'while LDAP is enabled' do it_behaves_like 'registration and login' end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index 9c64a9a3439..b18dee53cbc 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -2,7 +2,7 @@ module QA context 'Create' do - describe 'Git clone over HTTP', :ldap do + describe 'Git clone over HTTP', :ldap_no_tls do let(:location) do Page::Project::Show.act do choose_repository_clone_http diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index b9bed39662f..2f63a07e0c3 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -2,7 +2,7 @@ module QA context 'Create' do - describe 'Git push over HTTP', :ldap do + describe 'Git push over HTTP', :ldap_no_tls do it 'user pushes code to the repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb index 5f42cb00bd3..ac71cf52b6f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -2,7 +2,7 @@ module QA context 'Create' do - describe 'Protected branch support', :ldap do + describe 'Protected branch support', :ldap_no_tls do let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } let(:project) do diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index c98ede25b68..40cae0793dd 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -49,11 +49,13 @@ module QA cluster.install_prometheus = true cluster.install_runner = true end + kubernetes_cluster.populate(:ingress_ip) project.visit! Page::Project::Menu.act { click_ci_cd_settings } Page::Project::Settings::CICD.perform do |p| - p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io") + p.enable_auto_devops_with_domain( + "#{kubernetes_cluster.ingress_ip}.nip.io") end project.visit! diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb index 229f93a1041..d7b92052894 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/factory/base_spec.rb @@ -19,7 +19,7 @@ describe QA::Factory::Base do before do allow(subject).to receive(:current_url).and_return(product_location) allow(subject).to receive(:new).and_return(factory) - allow(QA::Factory::Product).to receive(:populate!).with(factory, product_location).and_return(product) + allow(QA::Factory::Product).to receive(:new).with(factory).and_return(product) end end @@ -115,73 +115,134 @@ describe QA::Factory::Base do end end - describe '.dependency' do - let(:dependency) { spy('dependency') } + shared_context 'simple factory' do + subject do + Class.new(QA::Factory::Base) do + attribute :test do + 'block' + end - before do - stub_const('Some::MyDependency', dependency) - end + attribute :no_block - subject do - Class.new(described_class) do - dependency Some::MyDependency, as: :mydep do |factory| - factory.something! + def fabricate! + 'any' + end + + def self.current_url + 'http://stub' end end end - it 'appends a new dependency and accessors' do - expect(subject.dependencies).to be_one + let(:factory) { subject.new } + end + + describe '.attribute' do + include_context 'simple factory' + + it 'appends new product attribute' do + expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) end - it 'defines dependency accessors' do - expect(subject.new).to respond_to :mydep, :mydep= + context 'when the product attribute is populated via a block' do + it 'returns a fabrication product and defines factory attributes as its methods' do + result = subject.fabricate!(factory: factory) + + expect(result).to be_a(QA::Factory::Product) + expect(result.test).to eq('block') + end end - describe 'dependencies fabrication' do - let(:dependency) { double('dependency') } - let(:instance) { spy('instance') } + context 'when the product attribute is populated via the api' do + let(:api_resource) { { no_block: 'api' } } + + before do + expect(factory).to receive(:api_resource).and_return(api_resource) + end + + it 'returns a fabrication product and defines factory attributes as its methods' do + result = subject.fabricate!(factory: factory) + + expect(result).to be_a(QA::Factory::Product) + expect(result.no_block).to eq('api') + end + + context 'when the attribute also has a block in the factory' do + let(:api_resource) { { test: 'api_with_block' } } + + before do + allow(QA::Runtime::Logger).to receive(:info) + end + + it 'returns the api value and emits an INFO log entry' do + result = subject.fabricate!(factory: factory) - subject do - Class.new(described_class) do - dependency Some::MyDependency, as: :mydep + expect(result).to be_a(QA::Factory::Product) + expect(result.test).to eq('api_with_block') + expect(QA::Runtime::Logger) + .to have_received(:info).with(/api_with_block/) end end + end + context 'when the product attribute is populated via a factory attribute' do before do - stub_const('Some::MyDependency', dependency) + factory.test = 'value' + end + + it 'returns a fabrication product and defines factory attributes as its methods' do + result = subject.fabricate!(factory: factory) + + expect(result).to be_a(QA::Factory::Product) + expect(result.test).to eq('value') + end - allow(subject).to receive(:new).and_return(instance) - allow(subject).to receive(:current_url).and_return(product_location) - allow(instance).to receive(:mydep).and_return(nil) - expect(QA::Factory::Product).to receive(:populate!) + context 'when the api also has such response' do + before do + allow(factory).to receive(:api_resource).and_return({ test: 'api' }) + end + + it 'returns the factory attribute for the product' do + result = subject.fabricate!(factory: factory) + + expect(result).to be_a(QA::Factory::Product) + expect(result.test).to eq('value') + end end + end - it 'builds all dependencies first' do - expect(dependency).to receive(:fabricate!).once + context 'when the product attribute has no value' do + it 'raises an error because no values could be found' do + result = subject.fabricate!(factory: factory) - subject.fabricate! + expect { result.no_block } + .to raise_error(described_class::NoValueError, "No value was computed for product no_block of factory #{factory.class.name}.") end end end - describe '.product' do - include_context 'fabrication context' + describe '#web_url' do + include_context 'simple factory' - subject do - Class.new(described_class) do - def fabricate! - "any" - end + it 'sets #web_url to #current_url after fabrication' do + subject.fabricate!(factory: factory) - product :token - end + expect(factory.web_url).to eq(subject.current_url) + end + end + + describe '#visit!' do + include_context 'simple factory' + + before do + allow(factory).to receive(:visit) end - it 'appends new product attribute' do - expect(subject.attributes).to be_one - expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute) - expect(subject.attributes[0].name).to eq(:token) + it 'calls #visit with the underlying #web_url' do + factory.web_url = subject.current_url + factory.visit! + + expect(factory).to have_received(:visit).with(subject.current_url) end end end diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb deleted file mode 100644 index 657beddffb1..00000000000 --- a/qa/spec/factory/dependency_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -describe QA::Factory::Dependency do - let(:dependency) { spy('dependency' ) } - let(:factory) { spy('factory') } - let(:block) { spy('block') } - - let(:signature) do - double('signature', name: :mydep, factory: dependency, block: block) - end - - subject do - described_class.new(factory, signature) - end - - describe '#overridden?' do - it 'returns true if factory has overridden dependency' do - allow(factory).to receive(:mydep).and_return('something') - - expect(subject).to be_overridden - end - - it 'returns false if dependency has not been overridden' do - allow(factory).to receive(:mydep).and_return(nil) - - expect(subject).not_to be_overridden - end - end - - describe '#build!' do - context 'when dependency has been overridden' do - before do - allow(subject).to receive(:overridden?).and_return(true) - end - - it 'does not fabricate dependency' do - subject.build! - - expect(dependency).not_to have_received(:fabricate!) - end - end - - context 'when dependency has not been overridden' do - before do - allow(subject).to receive(:overridden?).and_return(false) - end - - it 'fabricates dependency' do - subject.build! - - expect(dependency).to have_received(:fabricate!) - end - - it 'sets product in the factory' do - subject.build! - - expect(factory).to have_received(:mydep=).with(dependency) - end - - it 'calls given block with dependency factory and caller factory' do - expect(dependency).to receive(:fabricate!).and_yield(dependency) - - subject.build! - - expect(block).to have_received(:call).with(dependency, factory) - end - - context 'with no block given' do - let(:signature) do - double('signature', name: :mydep, factory: dependency, block: nil) - end - - it 'does not error' do - subject.build! - - expect(dependency).to have_received(:fabricate!) - end - end - end - end -end diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb index 43b1d93d769..5b6eaa13e9c 100644 --- a/qa/spec/factory/product_spec.rb +++ b/qa/spec/factory/product_spec.rb @@ -1,73 +1,21 @@ describe QA::Factory::Product do let(:factory) do Class.new(QA::Factory::Base) do - def foo - 'bar' + attribute :test do + 'block' end + + attribute :no_block end.new end let(:product) { spy('product') } let(:product_location) { 'http://product_location' } - subject { described_class.new(factory, product_location) } - - describe '.populate!' do - before do - expect(factory.class).to receive(:attributes).and_return(attributes) - end - - context 'when the product attribute is populated via a block' do - let(:attributes) do - [QA::Factory::Product::Attribute.new(:test, proc { 'returned' })] - end - - it 'returns a fabrication product and defines factory attributes as its methods' do - result = described_class.populate!(factory, product_location) - - expect(result).to be_a(described_class) - expect(result.test).to eq('returned') - end - end - - context 'when the product attribute is populated via the api' do - let(:attributes) do - [QA::Factory::Product::Attribute.new(:test)] - end - - it 'returns a fabrication product and defines factory attributes as its methods' do - expect(factory).to receive(:api_resource).and_return({ test: 'returned' }) - - result = described_class.populate!(factory, product_location) + subject { described_class.new(factory) } - expect(result).to be_a(described_class) - expect(result.test).to eq('returned') - end - end - - context 'when the product attribute is populated via a factory attribute' do - let(:attributes) do - [QA::Factory::Product::Attribute.new(:foo)] - end - - it 'returns a fabrication product and defines factory attributes as its methods' do - result = described_class.populate!(factory, product_location) - - expect(result).to be_a(described_class) - expect(result.foo).to eq('bar') - end - end - - context 'when the product attribute has no value' do - let(:attributes) do - [QA::Factory::Product::Attribute.new(:bar)] - end - - it 'returns a fabrication product and defines factory attributes as its methods' do - expect { described_class.populate!(factory, product_location) } - .to raise_error(described_class::NoValueError, "No value was computed for product bar of factory #{factory.class.name}.") - end - end + before do + factory.web_url = product_location end describe '.visit!' do diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index 9f17de4edbf..9d56353062b 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -3,9 +3,15 @@ require 'capybara/dsl' describe QA::Support::Page::Logging do + include Support::StubENV + let(:page) { double().as_null_object } before do + logger = Logger.new $stdout + logger.level = ::Logger::DEBUG + QA::Runtime::Logger.logger = logger + allow(Capybara).to receive(:current_session).and_return(page) allow(page).to receive(:current_url).and_return('http://current-url') allow(page).to receive(:has_css?).with(any_args).and_return(true) diff --git a/qa/spec/runtime/logger_spec.rb b/qa/spec/runtime/logger_spec.rb index 794e1f9bfe6..44be3381bff 100644 --- a/qa/spec/runtime/logger_spec.rb +++ b/qa/spec/runtime/logger_spec.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true describe QA::Runtime::Logger do + before do + logger = Logger.new $stdout + logger.level = ::Logger::DEBUG + described_class.logger = logger + end + it 'logs debug' do expect { described_class.debug('test') }.to output(/DEBUG -- : test/).to_stdout_from_any_process end diff --git a/qa/spec/scenario/test/integration/ldap_spec.rb b/qa/spec/scenario/test/integration/ldap_spec.rb index 198856aec3f..b6d798bf504 100644 --- a/qa/spec/scenario/test/integration/ldap_spec.rb +++ b/qa/spec/scenario/test/integration/ldap_spec.rb @@ -1,9 +1,17 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Integration::LDAP do +describe QA::Scenario::Test::Integration::LDAPNoTLS do context '#perform' do it_behaves_like 'a QA scenario class' do - let(:tags) { [:ldap] } + let(:tags) { [:ldap_no_tls] } + end + end +end + +describe QA::Scenario::Test::Integration::LDAPTLS do + context '#perform' do + it_behaves_like 'a QA scenario class' do + let(:tags) { [:ldap_tls] } end end end diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 78293464265..d372bcbdab1 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -47,15 +47,23 @@ function create_secret() { --dry-run -o json | kubectl apply -f - } +function deployExists() { + local namespace="${1}" + local deploy="${2}" + helm status --tiller-namespace "${namespace}" "${deploy}" >/dev/null 2>&1 + return $? +} + function previousDeployFailed() { set +e - echo "Checking for previous deployment of $CI_ENVIRONMENT_SLUG" - deployment_status=$(helm status $CI_ENVIRONMENT_SLUG >/dev/null 2>&1) + deploy="${1}" + echo "Checking for previous deployment of ${deploy}" + deployment_status=$(helm status ${deploy} >/dev/null 2>&1) status=$? # if `status` is `0`, deployment exists, has a status if [ $status -eq 0 ]; then echo "Previous deployment found, checking status" - deployment_status=$(helm status $CI_ENVIRONMENT_SLUG | grep ^STATUS | cut -d' ' -f2) + deployment_status=$(helm status ${deploy} | grep ^STATUS | cut -d' ' -f2) echo "Previous deployment state: $deployment_status" if [[ "$deployment_status" == "FAILED" || "$deployment_status" == "PENDING_UPGRADE" || "$deployment_status" == "PENDING_INSTALL" ]]; then status=0; @@ -113,7 +121,7 @@ function deploy() { fi # Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade` - if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previousDeployFailed ; then + if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previousDeployFailed "$CI_ENVIRONMENT_SLUG" ; then echo "Deployment in bad state, cleaning up $CI_ENVIRONMENT_SLUG" delete cleanup @@ -149,6 +157,7 @@ HELM_CMD=$(cat << EOF --set gitlab.gitlab-shell.image.tag="v$GITLAB_SHELL_VERSION" \ --set gitlab.unicorn.workhorse.image="$gitlab_workhorse_image_repository" \ --set gitlab.unicorn.workhorse.tag="$CI_COMMIT_REF_NAME" \ + --set nginx-ingress.controller.config.ssl-ciphers="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" \ --namespace="$KUBE_NAMESPACE" \ --version="$CI_PIPELINE_ID-$CI_JOB_ID" \ "$name" \ @@ -182,3 +191,23 @@ function cleanup() { | xargs kubectl -n "$KUBE_NAMESPACE" delete \ || true } + +function install_external_dns() { + local release_name="dns-gitlab-review-app" + local domain=$(echo "${REVIEW_APPS_DOMAIN}" | awk -F. '{printf "%s.%s", $(NF-1), $NF}') + + if ! deployExists "${KUBE_NAMESPACE}" "${release_name}" || previousDeployFailed "${release_name}" ; then + echo "Installing external-dns helm chart" + helm repo update + helm install stable/external-dns \ + -n "${release_name}" \ + --namespace "${KUBE_NAMESPACE}" \ + --set provider="aws" \ + --set aws.secretKey="${REVIEW_APPS_AWS_SECRET_KEY}" \ + --set aws.accessKey="${REVIEW_APPS_AWS_ACCESS_KEY}" \ + --set aws.zoneType="public" \ + --set domainFilters[0]="${domain}" \ + --set txtOwnerId="${KUBE_NAMESPACE}" \ + --set rbac.create="true" + fi +} diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index c365988a100..98946e4287b 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -208,11 +208,22 @@ describe Boards::IssuesController do end end - context 'with unauthorized user' do - it 'returns a forbidden 403 response' do - create_issue user: guest, board: board, list: list1, title: 'New issue' + context 'with guest user' do + context 'in open list' do + it 'returns a successful 200 response' do + open_list = board.lists.create(list_type: :backlog) + create_issue user: guest, board: board, list: open_list, title: 'New issue' - expect(response).to have_gitlab_http_status(403) + expect(response).to have_gitlab_http_status(200) + end + end + + context 'in label list' do + it 'returns a forbidden 403 response' do + create_issue user: guest, board: board, list: list1, title: 'New issue' + + expect(response).to have_gitlab_http_status(403) + end end end diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 56047c0c8d2..278b980b6d8 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -45,6 +45,8 @@ describe Dashboard::MilestonesController do end describe "#index" do + render_views + it 'returns group and project milestones to which the user belongs' do get :index, format: :json @@ -53,5 +55,12 @@ describe Dashboard::MilestonesController do expect(json_response.map { |i| i["first_milestone"]["id"] }).to match_array([group_milestone.id, project_milestone.id]) expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name) end + + it 'should contain group and project milestones to which the user belongs to' do + get :index + + expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>") + expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>") + end end end diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb index bf41aa0706f..d1d08391164 100644 --- a/spec/controllers/groups/boards_controller_spec.rb +++ b/spec/controllers/groups/boards_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Groups::BoardsController do let(:group) { create(:group) } - let(:user) { create(:user) } + let(:user) { create(:user) } before do group.add_maintainer(user) @@ -22,6 +22,27 @@ describe Groups::BoardsController do expect(response.content_type).to eq 'text/html' end + it 'redirects to latest visited board' do + board = create(:board, group: group) + create(:board_group_recent_visit, group: board.group, board: board, user: user) + + list_boards + + expect(response).to redirect_to(group_board_path(id: board.id)) + end + + it 'renders template if visited board is not found' do + visited = double + + allow(visited).to receive(:board_id).and_return(12) + allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) + + list_boards + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + context 'with unauthorized user' do before do allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(true) @@ -35,12 +56,30 @@ describe Groups::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:group) { create(:group, :public) } + + it 'renders template' do + sign_out(user) + + board = create(:board, group: group) + create(:board_group_recent_visit, group: board.group, board: board, user: user) + + list_boards + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'return an array with one group board' do create(:board, group: group) + expect(Boards::Visits::LatestService).not_to receive(:new) + list_boards format: :json parsed_response = JSON.parse(response.body) @@ -74,7 +113,7 @@ describe Groups::BoardsController do context 'when format is HTML' do it 'renders template' do - read_board board: board + expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(1) expect(response).to render_template :show expect(response.content_type).to eq 'text/html' @@ -93,10 +132,25 @@ describe Groups::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:group) { create(:group, :public) } + + it 'does not save visit' do + sign_out(user) + + expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(0) + + expect(response).to render_template :show + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'returns project board' do + expect(Boards::Visits::CreateService).not_to receive(:new) + read_board board: board, format: :json expect(response).to match_response_schema('board') diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 096efc1c7b2..667eaa5e34f 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -28,6 +28,27 @@ describe Projects::BoardsController do expect(response.content_type).to eq 'text/html' end + it 'redirects to latest visited board' do + board = create(:board, project: project) + create(:board_project_recent_visit, project: board.project, board: board, user: user) + + list_boards + + expect(response).to redirect_to(namespace_project_board_path(id: board.id)) + end + + it 'renders template if visited board is not found' do + visited = double + + allow(visited).to receive(:board_id).and_return(12) + allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) + + list_boards + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + context 'with unauthorized user' do before do allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) @@ -41,12 +62,30 @@ describe Projects::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:project) { create(:project, :public) } + + it 'renders template' do + sign_out(user) + + board = create(:board, project: project) + create(:board_project_recent_visit, project: board.project, board: board, user: user) + + list_boards + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'returns a list of project boards' do create_list(:board, 2, project: project) + expect(Boards::Visits::LatestService).not_to receive(:new) + list_boards format: :json parsed_response = JSON.parse(response.body) @@ -98,7 +137,7 @@ describe Projects::BoardsController do context 'when format is HTML' do it 'renders template' do - read_board board: board + expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(1) expect(response).to render_template :show expect(response.content_type).to eq 'text/html' @@ -117,10 +156,25 @@ describe Projects::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:project) { create(:project, :public) } + + it 'does not save visit' do + sign_out(user) + + expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(0) + + expect(response).to render_template :show + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'returns project board' do + expect(Boards::Visits::CreateService).not_to receive(:new) + read_board board: board, format: :json expect(response).to match_response_schema('board') diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 1484676eea3..2023d4b0bd0 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -297,6 +297,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to match_response_schema('job/job_details') expect(json_response['runners']['online']).to be false expect(json_response['runners']['available']).to be false + expect(json_response['stuck']).to be true end end @@ -309,6 +310,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to match_response_schema('job/job_details') expect(json_response['runners']['online']).to be false expect(json_response['runners']['available']).to be true + expect(json_response['stuck']).to be true end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index dcfd6c05200..7b0459e0325 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -749,13 +749,15 @@ describe Projects::MergeRequestsController do describe 'GET ci_environments_status' do context 'the environment is from a forked project' do - let!(:forked) { fork_project(project, user, repository: true) } - let!(:environment) { create(:environment, project: forked) } - let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } - let(:admin) { create(:admin) } + let(:forked) { fork_project(project, user, repository: true) } + let(:sha) { forked.commit.sha } + let(:environment) { create(:environment, project: forked) } + let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: 'master', deployable: build) } let(:merge_request) do - create(:merge_request, source_project: forked, target_project: project) + create(:merge_request, source_project: forked, target_project: project, target_branch: 'master', head_pipeline: pipeline) end it 'links to the environment on that project' do @@ -764,6 +766,35 @@ describe Projects::MergeRequestsController do expect(json_response.first['url']).to match /#{forked.full_path}/ end + context "when environment_target is 'merge_commit'" do + it 'returns nothing' do + get_ci_environments_status(environment_target: 'merge_commit') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + end + + context 'when is merged' do + let(:source_environment) { create(:environment, project: project) } + let(:merge_commit_sha) { project.repository.merge(user, forked.commit.id, merge_request, "merged in test") } + let(:post_merge_pipeline) { create(:ci_pipeline, sha: merge_commit_sha, project: project) } + let(:post_merge_build) { create(:ci_build, pipeline: post_merge_pipeline) } + let!(:source_deployment) { create(:deployment, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) } + + before do + merge_request.update!(merge_commit_sha: merge_commit_sha) + merge_request.mark_as_merged! + end + + it 'returns the enviroment on the source project' do + get_ci_environments_status(environment_target: 'merge_commit') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.first['url']).to match /#{project.full_path}/ + end + end + end + # we're trying to reduce the overall number of queries for this method. # set a hard limit for now. https://gitlab.com/gitlab-org/gitlab-ce/issues/52287 it 'keeps queries in check' do @@ -772,11 +803,15 @@ describe Projects::MergeRequestsController do expect(control_count).to be <= 137 end - def get_ci_environments_status - get :ci_environments_status, + def get_ci_environments_status(extra_params = {}) + params = { namespace_id: merge_request.project.namespace.to_param, project_id: merge_request.project, - id: merge_request.iid, format: 'json' + id: merge_request.iid, + format: 'json' + } + + get :ci_environments_status, params.merge(extra_params) end end end diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb index 6114eef7003..00c1e617e3a 100644 --- a/spec/controllers/projects/mirrors_controller_spec.rb +++ b/spec/controllers/projects/mirrors_controller_spec.rb @@ -63,6 +63,69 @@ describe Projects::MirrorsController do end end + describe '#ssh_host_keys', :use_clean_rails_memory_store_caching do + let(:project) { create(:project) } + let(:cache) { SshHostKey.new(project: project, url: "ssh://example.com:22") } + + before do + sign_in(project.owner) + end + + context 'invalid URLs' do + %w[ + INVALID + git@example.com:foo/bar.git + ssh://git@example.com:foo/bar.git + ].each do |url| + it "returns an error with a 400 response for URL #{url.inspect}" do + do_get(project, url) + + expect(response).to have_gitlab_http_status(400) + expect(json_response).to eq('message' => 'Invalid URL') + end + end + end + + context 'no data in cache' do + it 'requests the cache to be filled and returns a 204 response' do + expect(ReactiveCachingWorker).to receive(:perform_async).with(cache.class, cache.id).at_least(:once) + + do_get(project) + + expect(response).to have_gitlab_http_status(204) + end + end + + context 'error in the cache' do + it 'returns the error with a 400 response' do + stub_reactive_cache(cache, error: 'An error') + + do_get(project) + + expect(response).to have_gitlab_http_status(400) + expect(json_response).to eq('message' => 'An error') + end + end + + context 'data in the cache' do + let(:ssh_key) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf' } + let(:ssh_fp) { { type: 'ed25519', bits: 256, fingerprint: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16', index: 0 } } + + it 'returns the data with a 200 response' do + stub_reactive_cache(cache, known_hosts: ssh_key) + + do_get(project) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to eq('known_hosts' => ssh_key, 'fingerprints' => [ssh_fp.stringify_keys], 'host_keys_changed' => true) + end + end + + def do_get(project, url = 'ssh://example.com') + get :ssh_host_keys, namespace_id: project.namespace, project_id: project, ssh_url: url + end + end + def do_put(project, options, extra_attrs = {}) attrs = extra_attrs.merge(namespace_id: project.namespace.to_param, project_id: project.to_param) attrs[:project] = options diff --git a/spec/factories/board_group_recent_visit.rb b/spec/factories/board_group_recent_visit.rb new file mode 100644 index 00000000000..97ad5d6d068 --- /dev/null +++ b/spec/factories/board_group_recent_visit.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :board_group_recent_visit do + user + group + board + end +end diff --git a/spec/factories/board_project_recent_visit.rb b/spec/factories/board_project_recent_visit.rb new file mode 100644 index 00000000000..49ae4d7b391 --- /dev/null +++ b/spec/factories/board_project_recent_visit.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :board_project_recent_visit do + user + project + board + end +end diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index bbeba8ce8b9..c9f5d0a813e 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -2,13 +2,28 @@ FactoryBot.define do factory :cluster, class: Clusters::Cluster do user name 'test-cluster' + cluster_type :project_type + + trait :instance do + cluster_type { Clusters::Cluster.cluster_types[:instance_type] } + end trait :project do + cluster_type { Clusters::Cluster.cluster_types[:project_type] } + before(:create) do |cluster, evaluator| cluster.projects << create(:project, :repository) end end + trait :group do + cluster_type { Clusters::Cluster.cluster_types[:group_type] } + + before(:create) do |cluster, evalutor| + cluster.groups << create(:group) + end + end + trait :provided_by_user do provider_type :user platform_type :kubernetes diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb index 134eb25e4b1..fe475e1f7a0 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -8,3 +8,4 @@ require_relative 'support/rspec' require 'active_support/all' ActiveSupport::Dependencies.autoload_paths << 'lib' +ActiveSupport::Dependencies.autoload_paths << 'ee/lib' diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index 615223a2a88..2cdd3f55b50 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -106,7 +106,7 @@ describe 'Issue Boards add issue modal filtering', :js do it 'filters by unassigned' do set_filter('assignee') - click_filter_link('No Assignee') + click_filter_link('None') submit_filter page.within('.add-issues-modal') do diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 0bf1ecbc433..164442a47f5 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -94,8 +94,14 @@ describe 'Issue Boards new issue', :js do wait_for_requests end - it 'does not display new issue button' do - expect(page).to have_selector('.issue-count-badge-add-button', count: 0) + it 'displays new issue button in open list' do + expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) + end + + it 'does not display new issue button in label list' do + page.within('.board:nth-child(2)') do + expect(page).not_to have_selector('.issue-count-badge-add-button') + end end end end diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index e8ca6a6714f..174840794ed 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -95,9 +95,9 @@ describe 'Group milestones' do end it 'counts milestones correctly' do - expect(find('.top-area .active .badge').text).to eq("2") - expect(find('.top-area .closed .badge').text).to eq("2") - expect(find('.top-area .all .badge').text).to eq("4") + expect(find('.top-area .active .badge').text).to eq("3") + expect(find('.top-area .closed .badge').text).to eq("3") + expect(find('.top-area .all .badge').text).to eq("6") end it 'lists legacy group milestones and group milestones' do diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index d011d2545bb..e910fb54d23 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -156,13 +156,21 @@ describe 'Dropdown assignee', :js do expect_filtered_search_input_empty end - it 'selects `no assignee`' do - find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click + it 'selects `None`' do + find('#js-dropdown-assignee .filter-dropdown-item', text: 'None').click expect(page).to have_css(js_dropdown_assignee, visible: false) expect_tokens([assignee_token('none')]) expect_filtered_search_input_empty end + + it 'selects `Any`' do + find('#js-dropdown-assignee .filter-dropdown-item', text: 'Any').click + + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect_tokens([assignee_token('any')]) + expect_filtered_search_input_empty + end end describe 'selecting from dropdown without Ajax call' do diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 6ac7ccd00f7..1e1dd5691ab 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -118,7 +118,7 @@ describe 'Visual tokens', :js do describe 'selecting static option from dropdown' do before do - find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click + find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click end it 'changes value in visual token' do diff --git a/spec/features/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb index 40c452c991a..b0764db7751 100644 --- a/spec/features/issues/resource_label_events_spec.rb +++ b/spec/features/issues/resource_label_events_spec.rb @@ -7,6 +7,7 @@ describe 'List issue resource label events', :js do let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project, author: user) } let!(:label) { create(:label, project: project, title: 'foo') } + let!(:user_status) { create(:user_status, user: user) } context 'when user displays the issue' do let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue, note: 'some note') } @@ -23,6 +24,12 @@ describe 'List issue resource label events', :js do expect(find("#note_#{event.discussion_id}")).to have_content 'added foo label' end end + + it 'shows the user status on the system note for the label' do + page.within("#note_#{event.discussion_id}") do + expect(page).to show_user_status user_status + end + end end context 'when user adds label to the issue' do diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index f744d7941f5..a298ead43db 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -3,15 +3,19 @@ require 'rails_helper' describe 'Merge request > User sees deployment widget', :js do describe 'when deployed to an environment' do let(:user) { create(:user) } - let(:project) { merge_request.target_project } - let(:merge_request) { create(:merge_request, :merged) } + let(:project) { create(:project, :repository) } + let(:merge_request) { create(:merge_request, :merged, source_project: project) } let(:environment) { create(:environment, project: project) } let(:role) { :developer } - let(:sha) { project.commit('master').id } - let!(:deployment) { create(:deployment, environment: environment, sha: sha) } + let(:ref) { merge_request.target_branch } + let(:sha) { project.commit(ref).id } + let(:pipeline) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: ref) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: ref, deployable: build) } let!(:manual) { } before do + merge_request.update!(merge_commit_sha: sha) project.add_user(user, role) sign_in(user) visit project_merge_request_path(project, merge_request) @@ -26,15 +30,10 @@ describe 'Merge request > User sees deployment widget', :js do end context 'with stop action' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, pipeline: pipeline) } let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - let(:deployment) do - create(:deployment, environment: environment, ref: merge_request.target_branch, - sha: sha, deployable: build, on_stop: 'close_app') - end before do + deployment.update!(on_stop: manual.name) wait_for_requests end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index f15129759de..4e5699f9571 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -40,21 +40,26 @@ describe 'Merge request > User sees merge widget', :js do context 'view merge request' do let!(:environment) { create(:environment, project: project) } + let(:sha) { project.commit(merge_request.source_branch).sha } + let(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } let!(:deployment) do create(:deployment, environment: environment, - ref: 'feature', - sha: merge_request.diff_head_sha) + ref: merge_request.source_branch, + deployable: build, + sha: sha) end before do + merge_request.update!(head_pipeline: pipeline) visit project_merge_request_path(project, merge_request) end it 'shows environments link' do wait_for_requests - page.within('.mr-widget-heading') do + page.within('.js-pre-merge-deploy') do expect(page).to have_content("Deployed to #{environment.name}") expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url) end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 1ea8a640e17..b3bea92e635 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -151,9 +151,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end it 'renders escaped tooltip name' do - page.within('aside.right-sidebar') do - expect(find('.active.build-job a')['data-original-title']).to eq('<img src=x onerror=alert(document.domain)> - passed') - end + page.find('.active.build-job a').hover + expect(page).to have_content('<img src=x onerror=alert(document.domain)> - passed') end end @@ -722,6 +721,62 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do expect(page).not_to have_css('.js-job-sidebar.right-sidebar-collpased') end end + + context 'stuck', :js do + before do + visit project_job_path(project, job) + wait_for_requests + end + + context 'without active runners available' do + let(:runner) { create(:ci_runner, :instance, active: false) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) } + + it 'renders message about job being stuck because no runners are active' do + expect(page).to have_css('.js-stuck-no-active-runner') + expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.") + end + end + + context 'when available runners can not run specified tag' do + let(:runner) { create(:ci_runner, :instance, active: false) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner, tag_list: %w(docker linux)) } + + it 'renders message about job being stuck because of no runners with the specified tags' do + expect(page).to have_css('.js-stuck-with-tags') + expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:") + end + end + + context 'when runners are offline and build has tags' do + let(:runner) { create(:ci_runner, :instance, active: true) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner, tag_list: %w(docker linux)) } + + it 'renders message about job being stuck because of no runners with the specified tags' do + expect(page).to have_css('.js-stuck-with-tags') + expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:") + end + end + + context 'without any runners available' do + let(:job) { create(:ci_build, :pending, pipeline: pipeline) } + + it 'renders message about job being stuck because not runners are available' do + expect(page).to have_css('.js-stuck-no-active-runner') + expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.") + end + end + + context 'without available runners online' do + let(:runner) { create(:ci_runner, :instance, active: true) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) } + + it 'renders message about job being stuck because runners are offline' do + expect(page).to have_css('.js-stuck-no-runners') + expect(page).to have_content("This job is stuck, because the project doesn't have any runners online assigned to it.") + end + end + end end describe "POST /:project/jobs/:id/cancel", :js do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 0689c843104..2f164ffa8b0 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -57,11 +57,37 @@ describe IssuesFinder do end context 'filtering by no assignee' do - let(:params) { { assignee_id: 0 } } + let(:params) { { assignee_id: 'None' } } - it 'returns issues not assign to any assignee' do + it 'returns issues not assigned to any assignee' do expect(issues).to contain_exactly(issue4) end + + it 'returns issues not assigned to any assignee' do + params[:assignee_id] = 0 + + expect(issues).to contain_exactly(issue4) + end + + it 'returns issues not assigned to any assignee' do + params[:assignee_id] = 'none' + + expect(issues).to contain_exactly(issue4) + end + end + + context 'filtering by any assignee' do + let(:params) { { assignee_id: 'Any' } } + + it 'returns issues assigned to any assignee' do + expect(issues).to contain_exactly(issue1, issue2, issue3) + end + + it 'returns issues assigned to any assignee' do + params[:assignee_id] = 'any' + + expect(issues).to contain_exactly(issue1, issue2, issue3) + end end context 'filtering by group_id' do diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index c40977bc4ee..35971d564d5 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -46,6 +46,7 @@ "diff_head_commit_short_id": { "type": ["string", "null"] }, "merge_commit_message": { "type": ["string", "null"] }, "pipeline": { "type": ["object", "null"] }, + "merge_pipeline": { "type": ["object", "null"] }, "work_in_progress": { "type": "boolean" }, "source_branch_exists": { "type": "boolean" }, "mergeable_discussions_state": { "type": "boolean" }, diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json index 8218474705c..cdf7b049ab6 100644 --- a/spec/fixtures/api/schemas/job/job_details.json +++ b/spec/fixtures/api/schemas/job/job_details.json @@ -18,6 +18,7 @@ "runner": { "$ref": "runner.json" }, "runners": { "$ref": "runners.json" }, "has_trace": { "type": "boolean" }, - "stage": { "type": "string" } + "stage": { "type": "string" }, + "stuck": { "type": "boolean" } } } diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js index 08e25d2004e..fc94d0bab5b 100644 --- a/spec/javascripts/diffs/components/tree_list_spec.js +++ b/spec/javascripts/diffs/components/tree_list_spec.js @@ -53,7 +53,7 @@ describe('Diffs tree list component', () => { fileHash: 'test', key: 'index.js', name: 'index.js', - path: 'index.js', + path: 'app/index.js', removedLines: 0, tempFile: true, type: 'blob', @@ -104,7 +104,55 @@ describe('Diffs tree list component', () => { vm.$el.querySelector('.file-row').click(); - expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'index.js'); + expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'app/index.js'); + }); + + it('renders as file list when renderTreeList is false', done => { + vm.renderTreeList = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('.file-row').length).toBe(1); + + done(); + }); + }); + + it('renders file paths when renderTreeList is false', done => { + vm.renderTreeList = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.file-row').textContent).toContain('app/index.js'); + + done(); + }); + }); + + it('hides render buttons when input is focused', done => { + const focusEvent = new Event('focus'); + + vm.$el.querySelector('.form-control').dispatchEvent(focusEvent); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).toBe('none'); + + done(); + }); + }); + + it('shows render buttons when input is blurred', done => { + const blurEvent = new Event('blur'); + vm.focusSearch = true; + + vm.$nextTick() + .then(() => { + vm.$el.querySelector('.form-control').dispatchEvent(blurEvent); + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).not.toBe('none'); + }) + .then(done) + .catch(done.fail); }); }); @@ -117,4 +165,24 @@ describe('Diffs tree list component', () => { expect(vm.search).toBe(''); }); }); + + describe('toggleRenderTreeList', () => { + it('updates renderTreeList', () => { + expect(vm.renderTreeList).toBe(true); + + vm.toggleRenderTreeList(false); + + expect(vm.renderTreeList).toBe(false); + }); + }); + + describe('toggleFocusSearch', () => { + it('updates focusSearch', () => { + expect(vm.focusSearch).toBe(false); + + vm.toggleFocusSearch(true); + + expect(vm.focusSearch).toBe(true); + }); + }); }); diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index 85c1926fcb1..bb623953710 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -27,7 +27,6 @@ import actions, { toggleShowTreeList, } from '~/diffs/store/actions'; import * as types from '~/diffs/store/mutation_types'; -import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils'; import axios from '~/lib/utils/axios_utils'; import testAction from '../../helpers/vuex_action_helper'; @@ -152,7 +151,7 @@ describe('DiffsStoreActions', () => { original_position: diffPosition, }; - const discussions = reduceDiscussionsToLineCodes([singleDiscussion]); + const discussions = [singleDiscussion]; testAction( assignDiscussionsToDiff, @@ -162,8 +161,7 @@ describe('DiffsStoreActions', () => { { type: types.SET_LINE_DISCUSSIONS_FOR_FILE, payload: { - fileHash: 'ABC', - discussions: [singleDiscussion], + discussion: singleDiscussion, diffPositionByLineCode: { ABC_1_1: { baseSha: 'abc', @@ -581,7 +579,6 @@ describe('DiffsStoreActions', () => { describe('saveDiffDiscussion', () => { beforeEach(() => { spyOnDependency(actions, 'getNoteFormData').and.returnValue('testData'); - spyOnDependency(actions, 'reduceDiscussionsToLineCodes').and.returnValue('discussions'); }); it('dispatches actions', done => { @@ -602,7 +599,7 @@ describe('DiffsStoreActions', () => { .then(() => { expect(dispatch.calls.argsFor(0)).toEqual(['saveNote', 'testData', { root: true }]); expect(dispatch.calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]); - expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', 'discussions']); + expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index b7e28391419..4b6d3d5bcba 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -198,40 +198,32 @@ describe('DiffsStoreMutations', () => { }, ], }; - const discussions = [ - { - id: 1, - line_code: 'ABC_1', - diff_discussion: true, - resolvable: true, - original_position: diffPosition, - position: diffPosition, + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: state.diffFiles[0].fileHash, }, - { - id: 2, - line_code: 'ABC_1', - diff_discussion: true, - resolvable: true, - original_position: diffPosition, - position: diffPosition, - }, - ]; + }; const diffPositionByLineCode = { ABC_1: diffPosition, }; mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { - fileHash: 'ABC', - discussions, + discussion, diffPositionByLineCode, }); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1); }); it('should add legacy discussions to the given line', () => { @@ -272,36 +264,30 @@ describe('DiffsStoreMutations', () => { }, ], }; - const discussions = [ - { - id: 1, - line_code: 'ABC_1', - diff_discussion: true, - active: true, + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + active: true, + diff_file: { + file_hash: state.diffFiles[0].fileHash, }, - { - id: 2, - line_code: 'ABC_1', - diff_discussion: true, - active: true, - }, - ]; + }; const diffPositionByLineCode = { ABC_1: diffPosition, }; mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { - fileHash: 'ABC', - discussions, + discussion, diffPositionByLineCode, }); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1); }); }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index ef367fc09fa..f49dee3696d 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -445,6 +445,14 @@ describe('DiffsStoreUtils', () => { fileHash: 'test', }, { + newPath: 'app/test/filepathneedstruncating.js', + deletedFile: false, + newFile: true, + removedLines: 0, + addedLines: 0, + fileHash: 'test', + }, + { newPath: 'package.json', deletedFile: true, newFile: false, @@ -498,6 +506,19 @@ describe('DiffsStoreUtils', () => { type: 'blob', tree: [], }, + { + addedLines: 0, + changed: true, + deleted: false, + fileHash: 'test', + key: 'app/test/filepathneedstruncating.js', + name: 'filepathneedstruncating.js', + path: 'app/test/filepathneedstruncating.js', + removedLines: 0, + tempFile: true, + type: 'blob', + tree: [], + }, ], }, ], @@ -527,6 +548,7 @@ describe('DiffsStoreUtils', () => { 'app/index.js', 'app/test', 'app/test/index.js', + 'app/test/filepathneedstruncating.js', 'package.json', ]); }); diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js index d7338ee0f66..aecab331ead 100644 --- a/spec/javascripts/flash_spec.js +++ b/spec/javascripts/flash_spec.js @@ -172,7 +172,7 @@ describe('Flash', () => { flash('test'); expect(document.querySelector('.flash-text').className).toBe( - 'flash-text container-fluid container-limited', + 'flash-text container-fluid container-limited limit-container-width', ); }); @@ -180,7 +180,7 @@ describe('Flash', () => { document.querySelector('.content-wrapper').className = 'js-content-wrapper'; flash('test'); - expect(document.querySelector('.flash-text').className.trim()).toBe('flash-text'); + expect(document.querySelector('.flash-text').className.trim()).toContain('flash-text'); }); it('removes element after clicking', () => { diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index e6d403dc826..288c06d6615 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -88,7 +88,9 @@ describe('Job App ', () => { describe('triggered job', () => { beforeEach(() => { - mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' })); + mock + .onGet(props.endpoint) + .replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' })); vm = mountComponentWithStore(Component, { props, store }); }); @@ -133,57 +135,106 @@ describe('Job App ', () => { }); describe('stuck block', () => { - it('renders stuck block when there are no runners', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - runners: { - available: false, - }, - }), - ); - vm = mountComponentWithStore(Component, { props, store }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); + describe('without active runners availabl', () => { + it('renders stuck block when there are no runners', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: false, + online: false, + }, + tags: [], + }), + ); + vm = mountComponentWithStore(Component, { props, store }); - done(); - }, 0); + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( + "This job is stuck, because you don't have any active runners that can run this job.", + ); + done(); + }, 0); + }); }); - it('renders tags in stuck block when there are no runners', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - runners: { - available: false, - }, - }), - ); + describe('when available runners can not run specified tag', () => { + it('renders tags in stuck block when there are no runners', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: false, + online: false, + }, + }), + ); - vm = mountComponentWithStore(Component, { - props, - store, + vm = mountComponentWithStore(Component, { + props, + store, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( + "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", + ); + done(); + }, 0); }); + }); - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - done(); - }, 0); + describe('when runners are offline and build has tags', () => { + it('renders message about job being stuck because of no runners with the specified tags', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: true, + online: true, + }, + }), + ); + + vm = mountComponentWithStore(Component, { + props, + store, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( + "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", + ); + done(); + }, 0); + }) }); it('does not renders stuck block when there are no runners', done => { @@ -418,10 +469,11 @@ describe('Job App ', () => { vm.$store.state.trace = 'Update'; setTimeout(() => { - expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain('Update'); - expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain( - 'Different', + expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain( + 'Update', ); + + expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Different'); done(); }, 0); }); diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index 460a2e1b5da..424092d2d88 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -140,10 +140,11 @@ describe('Sidebar details block', () => { }); describe('while fetching stages', () => { - it('renders dropdown with More label', () => { + it('it does not render dropdown', () => { + store.dispatch('requestStages'); vm = mountComponentWithStore(SidebarComponent, { store }); - expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual('More'); + expect(vm.$el.querySelector('.js-selected-stage')).toBeNull(); }); }); diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js index 46a20122ec8..34e9707eadd 100644 --- a/spec/javascripts/jobs/store/getters_spec.js +++ b/spec/javascripts/jobs/store/getters_spec.js @@ -175,43 +175,37 @@ describe('Job Store Getters', () => { }); }); - describe('isJobStuck', () => { - describe('when job is pending and runners are not available', () => { + describe('hasRunnersForProject', () => { + describe('with available and offline runners', () => { it('returns true', () => { - localState.job.status = { - group: 'pending', - }; localState.job.runners = { - available: false, + available: true, + online: false }; - expect(getters.isJobStuck(localState)).toEqual(true); + expect(getters.hasRunnersForProject(localState)).toEqual(true); }); }); - describe('when job is not pending', () => { + describe('with non available runners', () => { it('returns false', () => { - localState.job.status = { - group: 'running', - }; localState.job.runners = { available: false, + online: false }; - expect(getters.isJobStuck(localState)).toEqual(false); + expect(getters.hasRunnersForProject(localState)).toEqual(false); }); }); - describe('when runners are available', () => { + describe('with online runners', () => { it('returns false', () => { - localState.job.status = { - group: 'pending', - }; localState.job.runners = { - available: true, + available: false, + online: true }; - expect(getters.isJobStuck(localState)).toEqual(false); + expect(getters.hasRunnersForProject(localState)).toEqual(false); }); }); }); diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/javascripts/jobs/store/mutations_spec.js index 4230a7c42cf..d7908efcf13 100644 --- a/spec/javascripts/jobs/store/mutations_spec.js +++ b/spec/javascripts/jobs/store/mutations_spec.js @@ -124,8 +124,8 @@ describe('Jobs Store Mutations', () => { expect(stateCopy.job).toEqual({ id: 1312321 }); }); - it('sets selectedStage when the selectedStage is More', () => { - expect(stateCopy.selectedStage).toEqual('More'); + it('sets selectedStage when the selectedStage is empty', () => { + expect(stateCopy.selectedStage).toEqual(''); mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321, stage: 'deploy' }); expect(stateCopy.selectedStage).toEqual('deploy'); diff --git a/spec/javascripts/lib/utils/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js deleted file mode 100644 index a9f3abcf2a4..00000000000 --- a/spec/javascripts/lib/utils/datefix_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import { pad, pikadayToString } from '~/lib/utils/datefix'; - -describe('datefix', () => { - describe('pad', () => { - it('should add a 0 when length is smaller than 2', () => { - expect(pad(2)).toEqual('02'); - }); - - it('should not add a zero when lenght matches the default', () => { - expect(pad(12)).toEqual('12'); - }); - - it('should add a 0 when lenght is smaller than the provided', () => { - expect(pad(12, 3)).toEqual('012'); - }); - }); - - describe('parsePikadayDate', () => { - // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834 - }); - - describe('pikadayToString', () => { - it('should format a UTC date into yyyy-mm-dd format', () => { - expect(pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29'); - }); - }); -}); diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js index 9fedbcc4c25..de6b96aab57 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/lib/utils/datetime_utility_spec.js @@ -192,3 +192,181 @@ describe('formatTime', () => { }); }); }); + +describe('datefix', () => { + describe('pad', () => { + it('should add a 0 when length is smaller than 2', () => { + expect(datetimeUtility.pad(2)).toEqual('02'); + }); + + it('should not add a zero when lenght matches the default', () => { + expect(datetimeUtility.pad(12)).toEqual('12'); + }); + + it('should add a 0 when lenght is smaller than the provided', () => { + expect(datetimeUtility.pad(12, 3)).toEqual('012'); + }); + }); + + describe('parsePikadayDate', () => { + // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834 + }); + + describe('pikadayToString', () => { + it('should format a UTC date into yyyy-mm-dd format', () => { + expect(datetimeUtility.pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29'); + }); + }); +}); + +describe('prettyTime methods', () => { + const assertTimeUnits = (obj, minutes, hours, days, weeks) => { + expect(obj.minutes).toBe(minutes); + expect(obj.hours).toBe(hours); + expect(obj.days).toBe(days); + expect(obj.weeks).toBe(weeks); + }; + + describe('parseSeconds', () => { + it('should correctly parse a negative value', () => { + const zeroSeconds = datetimeUtility.parseSeconds(-1000); + + assertTimeUnits(zeroSeconds, 16, 0, 0, 0); + }); + + it('should correctly parse a zero value', () => { + const zeroSeconds = datetimeUtility.parseSeconds(0); + + assertTimeUnits(zeroSeconds, 0, 0, 0, 0); + }); + + it('should correctly parse a small non-zero second values', () => { + const subOneMinute = datetimeUtility.parseSeconds(10); + const aboveOneMinute = datetimeUtility.parseSeconds(100); + const manyMinutes = datetimeUtility.parseSeconds(1000); + + assertTimeUnits(subOneMinute, 0, 0, 0, 0); + assertTimeUnits(aboveOneMinute, 1, 0, 0, 0); + assertTimeUnits(manyMinutes, 16, 0, 0, 0); + }); + + it('should correctly parse large second values', () => { + const aboveOneHour = datetimeUtility.parseSeconds(4800); + const aboveOneDay = datetimeUtility.parseSeconds(110000); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 3, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 3, 173); + }); + + it('should correctly accept a custom param for hoursPerDay', () => { + const config = { hoursPerDay: 24 }; + + const aboveOneHour = datetimeUtility.parseSeconds(4800, config); + const aboveOneDay = datetimeUtility.parseSeconds(110000, config); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 1, 0); + assertTimeUnits(aboveOneWeek, 26, 8, 4, 57); + }); + + it('should correctly accept a custom param for daysPerWeek', () => { + const config = { daysPerWeek: 7 }; + + const aboveOneHour = datetimeUtility.parseSeconds(4800, config); + const aboveOneDay = datetimeUtility.parseSeconds(110000, config); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 3, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 0, 124); + }); + + it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => { + const config = { daysPerWeek: 55, hoursPerDay: 14 }; + + const aboveOneHour = datetimeUtility.parseSeconds(4800, config); + const aboveOneDay = datetimeUtility.parseSeconds(110000, config); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 2, 2, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 1, 9); + }); + }); + + describe('stringifyTime', () => { + it('should stringify values with all non-zero units', () => { + const timeObject = { + weeks: 1, + days: 4, + hours: 7, + minutes: 20, + }; + + const timeString = datetimeUtility.stringifyTime(timeObject); + + expect(timeString).toBe('1w 4d 7h 20m'); + }); + + it('should stringify values with some non-zero units', () => { + const timeObject = { + weeks: 0, + days: 4, + hours: 0, + minutes: 20, + }; + + const timeString = datetimeUtility.stringifyTime(timeObject); + + expect(timeString).toBe('4d 20m'); + }); + + it('should stringify values with no non-zero units', () => { + const timeObject = { + weeks: 0, + days: 0, + hours: 0, + minutes: 0, + }; + + const timeString = datetimeUtility.stringifyTime(timeObject); + + expect(timeString).toBe('0m'); + }); + }); + + describe('abbreviateTime', () => { + it('should abbreviate stringified times for weeks', () => { + const fullTimeString = '1w 3d 4h 5m'; + + expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('1w'); + }); + + it('should abbreviate stringified times for non-weeks', () => { + const fullTimeString = '0w 3d 4h 5m'; + + expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('3d'); + }); + }); +}); + +describe('calculateRemainingMilliseconds', () => { + beforeEach(() => { + spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime()); + }); + + it('calculates the remaining time for a given end date', () => { + const milliseconds = datetimeUtility.calculateRemainingMilliseconds('2063-04-04T01:44:03Z'); + + expect(milliseconds).toBe(3723000); + }); + + it('returns 0 if the end date has passed', () => { + const milliseconds = datetimeUtility.calculateRemainingMilliseconds('2063-04-03T00:00:00Z'); + + expect(milliseconds).toBe(0); + }); +}); diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js index bb7a29fe30a..b9e805628f8 100644 --- a/spec/javascripts/lib/utils/text_markdown_spec.js +++ b/spec/javascripts/lib/utils/text_markdown_spec.js @@ -166,6 +166,33 @@ describe('init markdown', () => { expect(textArea.selectionStart).toEqual(expectedText.lastIndexOf(select)); expect(textArea.selectionEnd).toEqual(expectedText.lastIndexOf(select) + select.length); }); + + it('should support selected urls', () => { + const expectedUrl = 'http://www.gitlab.com'; + const expectedSelectionText = 'text'; + const expectedText = `text [${expectedSelectionText}](${expectedUrl}) text`; + const initialValue = `text ${expectedUrl} text`; + + textArea.value = initialValue; + const selectedIndex = initialValue.indexOf(expectedUrl); + textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length); + + insertMarkdownText({ + textArea, + text: textArea.value, + tag, + blockTag: null, + selected: expectedUrl, + wrap: false, + select, + }); + + expect(textArea.value).toEqual(expectedText); + expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedSelectionText, 1)); + expect(textArea.selectionEnd).toEqual( + expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length, + ); + }); }); }); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index a3477c5f8c6..565b87de248 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -113,6 +113,22 @@ describe('Dashboard', () => { }); }); + it('hides the dropdown list when there is no environments', done => { + const component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { ...propsData, hasMetrics: true, showPanels: false }, + }); + + component.store.storeEnvironmentsData([]); + + setTimeout(() => { + const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul'); + + expect(dropdownMenuEnvironments.length).toEqual(0); + done(); + }); + }); + it('renders the dropdown with a single is-active element', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), diff --git a/spec/javascripts/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js deleted file mode 100644 index 158cd76dd13..00000000000 --- a/spec/javascripts/pretty_time_spec.js +++ /dev/null @@ -1,135 +0,0 @@ -import { parseSeconds, abbreviateTime, stringifyTime } from '~/lib/utils/pretty_time'; - -function assertTimeUnits(obj, minutes, hours, days, weeks) { - expect(obj.minutes).toBe(minutes); - expect(obj.hours).toBe(hours); - expect(obj.days).toBe(days); - expect(obj.weeks).toBe(weeks); -} - -describe('prettyTime methods', () => { - describe('parseSeconds', () => { - it('should correctly parse a negative value', () => { - const zeroSeconds = parseSeconds(-1000); - - assertTimeUnits(zeroSeconds, 16, 0, 0, 0); - }); - - it('should correctly parse a zero value', () => { - const zeroSeconds = parseSeconds(0); - - assertTimeUnits(zeroSeconds, 0, 0, 0, 0); - }); - - it('should correctly parse a small non-zero second values', () => { - const subOneMinute = parseSeconds(10); - const aboveOneMinute = parseSeconds(100); - const manyMinutes = parseSeconds(1000); - - assertTimeUnits(subOneMinute, 0, 0, 0, 0); - assertTimeUnits(aboveOneMinute, 1, 0, 0, 0); - assertTimeUnits(manyMinutes, 16, 0, 0, 0); - }); - - it('should correctly parse large second values', () => { - const aboveOneHour = parseSeconds(4800); - const aboveOneDay = parseSeconds(110000); - const aboveOneWeek = parseSeconds(25000000); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 6, 3, 0); - assertTimeUnits(aboveOneWeek, 26, 0, 3, 173); - }); - - it('should correctly accept a custom param for hoursPerDay', () => { - const config = { hoursPerDay: 24 }; - - const aboveOneHour = parseSeconds(4800, config); - const aboveOneDay = parseSeconds(110000, config); - const aboveOneWeek = parseSeconds(25000000, config); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 6, 1, 0); - assertTimeUnits(aboveOneWeek, 26, 8, 4, 57); - }); - - it('should correctly accept a custom param for daysPerWeek', () => { - const config = { daysPerWeek: 7 }; - - const aboveOneHour = parseSeconds(4800, config); - const aboveOneDay = parseSeconds(110000, config); - const aboveOneWeek = parseSeconds(25000000, config); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 6, 3, 0); - assertTimeUnits(aboveOneWeek, 26, 0, 0, 124); - }); - - it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => { - const config = { daysPerWeek: 55, hoursPerDay: 14 }; - - const aboveOneHour = parseSeconds(4800, config); - const aboveOneDay = parseSeconds(110000, config); - const aboveOneWeek = parseSeconds(25000000, config); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 2, 2, 0); - assertTimeUnits(aboveOneWeek, 26, 0, 1, 9); - }); - }); - - describe('stringifyTime', () => { - it('should stringify values with all non-zero units', () => { - const timeObject = { - weeks: 1, - days: 4, - hours: 7, - minutes: 20, - }; - - const timeString = stringifyTime(timeObject); - - expect(timeString).toBe('1w 4d 7h 20m'); - }); - - it('should stringify values with some non-zero units', () => { - const timeObject = { - weeks: 0, - days: 4, - hours: 0, - minutes: 20, - }; - - const timeString = stringifyTime(timeObject); - - expect(timeString).toBe('4d 20m'); - }); - - it('should stringify values with no non-zero units', () => { - const timeObject = { - weeks: 0, - days: 0, - hours: 0, - minutes: 0, - }; - - const timeString = stringifyTime(timeObject); - - expect(timeString).toBe('0m'); - }); - }); - - describe('abbreviateTime', () => { - it('should abbreviate stringified times for weeks', () => { - const fullTimeString = '1w 3d 4h 5m'; - - expect(abbreviateTime(fullTimeString)).toBe('1w'); - }); - - it('should abbreviate stringified times for non-weeks', () => { - const fullTimeString = '0w 3d 4h 5m'; - - expect(abbreviateTime(fullTimeString)).toBe('3d'); - }); - }); -}); diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index ce850bc621e..d26ffa4b4a6 100644 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js @@ -2,54 +2,48 @@ import Vue from 'vue'; import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; import { getTimeago } from '~/lib/utils/datetime_utility'; +import mountComponent from '../../helpers/vue_mount_component_helper'; -const deploymentMockData = { - id: 15, - name: 'review/diplo', - url: '/root/acets-review-apps/environments/15', - stop_url: '/root/acets-review-apps/environments/15/stop', - metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', - metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', - external_url: 'http://diplo.', - external_url_formatted: 'diplo.', - deployed_at: '2017-03-22T22:44:42.258Z', - deployed_at_formatted: 'Mar 22, 2017 10:44pm', - changes: [ - { - path: 'index.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', - }, - { - path: 'imgs/gallery.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', - }, - { - path: 'about/', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', - }, - ], -}; -const createComponent = () => { +describe('Deployment component', () => { const Component = Vue.extend(deploymentComponent); + const deploymentMockData = { + id: 15, + name: 'review/diplo', + url: '/root/review-apps/environments/15', + stop_url: '/root/review-apps/environments/15/stop', + metrics_url: '/root/review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/review-apps/environments/15/metrics', + external_url: 'http://gitlab.com.', + external_url_formatted: 'gitlab', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], + }; - return new Component({ - el: document.createElement('div'), - propsData: { deployment: { ...deploymentMockData } }, - }); -}; - -describe('Deployment component', () => { let vm; - beforeEach(() => { - vm = createComponent(); - }); - afterEach(() => { vm.$destroy(); }); - describe('computed', () => { + describe('', () => { + beforeEach(() => { + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); + }); + describe('deployTimeago', () => { it('return formatted date', () => { const readable = getTimeago().format(deploymentMockData.deployed_at); @@ -111,9 +105,7 @@ describe('Deployment component', () => { expect(vm.hasDeploymentMeta).toEqual(false); }); }); - }); - describe('methods', () => { describe('stopEnvironment', () => { const url = '/foo/bar'; const returnPromise = () => @@ -152,42 +144,33 @@ describe('Deployment component', () => { expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled(); }); }); - }); - - describe('template', () => { - let el; - - beforeEach(() => { - vm = createComponent(deploymentMockData); - el = vm.$el; - }); it('renders deployment name', () => { - expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual( + expect(vm.$el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual( deploymentMockData.url, ); - expect(el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name); + expect(vm.$el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name); }); it('renders external URL', () => { - expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual( + expect(vm.$el.querySelector('.js-deploy-url').getAttribute('href')).toEqual( deploymentMockData.external_url, ); - expect(el.querySelector('.js-deploy-url').innerText).toContain('View app'); + expect(vm.$el.querySelector('.js-deploy-url').innerText).toContain('View app'); }); it('renders stop button', () => { - expect(el.querySelector('.btn')).not.toBeNull(); + expect(vm.$el.querySelector('.btn')).not.toBeNull(); }); it('renders deployment time', () => { - expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago); + expect(vm.$el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago); }); it('renders metrics component', () => { - expect(el.querySelector('.js-mr-memory-usage')).not.toBeNull(); + expect(vm.$el.querySelector('.js-mr-memory-usage')).not.toBeNull(); }); }); @@ -196,8 +179,7 @@ describe('Deployment component', () => { window.gon = window.gon || {}; window.gon.features = window.gon.features || {}; window.gon.features.ciEnvironmentsStatusChanges = true; - - vm = createComponent(deploymentMockData); + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); }); afterEach(() => { @@ -216,7 +198,7 @@ describe('Deployment component', () => { window.gon.features = window.gon.features || {}; window.gon.features.ciEnvironmentsStatusChanges = false; - vm = createComponent(deploymentMockData); + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); }); afterEach(() => { @@ -228,4 +210,44 @@ describe('Deployment component', () => { expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull(); }); }); + + describe('deployment status', () => { + describe('running', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'running' }), + }); + }); + + it('renders information about running deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to'); + }); + }); + + describe('success', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'success' }), + }); + }); + + it('renders information about finished deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deployed to'); + }); + }); + + describe('failed', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'failed' }), + }); + }); + + it('renders information about finished deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain( + 'Failed to deploy to', + ); + }); + }); + }); }); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index d1a064b9f4d..27b6c91e154 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -189,7 +189,7 @@ describe('mrWidgetOptions', () => { it('should fetch deployments', done => { spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }])); - vm.fetchDeployments(); + vm.fetchPreMergeDeployments(); setTimeout(() => { expect(vm.service.fetchDeployments).toHaveBeenCalled(); @@ -454,6 +454,7 @@ describe('mrWidgetOptions', () => { deployed_at: '2017-03-22T22:44:42.258Z', deployed_at_formatted: 'Mar 22, 2017 10:44pm', changes, + status: 'success' }; beforeEach(done => { @@ -486,4 +487,189 @@ describe('mrWidgetOptions', () => { ).toEqual(changes.length); }); }); + + describe('pipeline for target branch after merge', () => { + describe('with information for target branch pipeline', () => { + beforeEach(done => { + vm.mr.state = 'merged'; + vm.mr.mergePipeline = { + id: 127, + user: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', + status_tooltip_html: null, + path: '/root', + }, + active: true, + coverage: null, + source: 'push', + created_at: '2018-10-22T11:41:35.186Z', + updated_at: '2018-10-22T11:41:35.433Z', + path: '/root/ci-web-terminal/pipelines/127', + flags: { + latest: true, + stuck: true, + auto_devops: false, + yaml_errors: false, + retryable: false, + cancelable: true, + failure_reason: false, + }, + details: { + status: { + icon: 'status_pending', + text: 'pending', + label: 'pending', + group: 'pending', + tooltip: 'pending', + has_details: true, + details_path: '/root/ci-web-terminal/pipelines/127', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png', + }, + duration: null, + finished_at: null, + stages: [ + { + name: 'test', + title: 'test: pending', + status: { + icon: 'status_pending', + text: 'pending', + label: 'pending', + group: 'pending', + tooltip: 'pending', + has_details: true, + details_path: '/root/ci-web-terminal/pipelines/127#test', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png', + }, + path: '/root/ci-web-terminal/pipelines/127#test', + dropdown_path: '/root/ci-web-terminal/pipelines/127/stage.json?stage=test', + }, + ], + artifacts: [], + manual_actions: [], + scheduled_actions: [], + }, + ref: { + name: 'master', + path: '/root/ci-web-terminal/commits/master', + tag: false, + branch: true, + }, + commit: { + id: 'aa1939133d373c94879becb79d91828a892ee319', + short_id: 'aa193913', + title: "Merge branch 'master-test' into 'master'", + created_at: '2018-10-22T11:41:33.000Z', + parent_ids: [ + '4622f4dd792468993003caf2e3be978798cbe096', + '76598df914cdfe87132d0c3c40f80db9fa9396a4', + ], + message: + "Merge branch 'master-test' into 'master'\n\nUpdate .gitlab-ci.yml\n\nSee merge request root/ci-web-terminal!1", + author_name: 'Administrator', + author_email: 'admin@example.com', + authored_date: '2018-10-22T11:41:33.000Z', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + committed_date: '2018-10-22T11:41:33.000Z', + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', + status_tooltip_html: null, + path: '/root', + }, + author_gravatar_url: null, + commit_url: + 'http://localhost:3000/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319', + commit_path: '/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319', + }, + cancel_path: '/root/ci-web-terminal/pipelines/127/cancel', + }; + vm.$nextTick(done); + }); + + it('renders pipeline block', () => { + expect(vm.$el.querySelector('.js-post-merge-pipeline')).not.toBeNull(); + }); + + describe('with post merge deployments', () => { + beforeEach(done => { + vm.mr.postMergeDeployments = [{ + id: 15, + name: 'review/diplo', + url: '/root/acets-review-apps/environments/15', + stop_url: '/root/acets-review-apps/environments/15/stop', + metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', + external_url: 'http://diplo.', + external_url_formatted: 'diplo.', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], + status: 'success' + }]; + + vm.$nextTick(done); + }); + + it('renders post deployment information', () => { + expect(vm.$el.querySelector('.js-post-deployment')).not.toBeNull(); + }); + }); + }); + + describe('without information for target branch pipeline', () => { + beforeEach(done => { + vm.mr.state = 'merged'; + + vm.$nextTick(done); + }); + + it('does not render pipeline block', () => { + expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull(); + }); + }); + + describe('when state is not merged', () => { + beforeEach(done => { + vm.mr.state = 'archived'; + + vm.$nextTick(done); + }); + + it('does not render pipeline block', () => { + expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull(); + }); + + it('does not render post deployment information', () => { + expect(vm.$el.querySelector('.js-post-deployment')).toBeNull(); + }); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js index 9914c0b70f3..67752c1c455 100644 --- a/spec/javascripts/vue_shared/components/file_row_spec.js +++ b/spec/javascripts/vue_shared/components/file_row_spec.js @@ -71,4 +71,40 @@ describe('RepoFile', () => { expect(vm.$el.querySelector('.file-row-name').style.marginLeft).toBe('32px'); }); + + describe('outputText', () => { + beforeEach(done => { + createComponent({ + file: { + ...file(), + path: 'app/assets/index.js', + }, + level: 0, + }); + + vm.displayTextKey = 'path'; + + vm.$nextTick(done); + }); + + it('returns text if truncateStart is 0', done => { + vm.truncateStart = 0; + + vm.$nextTick(() => { + expect(vm.outputText).toBe('app/assets/index.js'); + + done(); + }); + }); + + it('returns text truncated at start', done => { + vm.truncateStart = 5; + + vm.$nextTick(() => { + expect(vm.outputText).toBe('...ssets/index.js'); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/gl_countdown_spec.js b/spec/javascripts/vue_shared/components/gl_countdown_spec.js new file mode 100644 index 00000000000..929ffe219f4 --- /dev/null +++ b/spec/javascripts/vue_shared/components/gl_countdown_spec.js @@ -0,0 +1,77 @@ +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import Vue from 'vue'; +import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; + +describe('GlCountdown', () => { + const Component = Vue.extend(GlCountdown); + let vm; + let now = '2000-01-01T00:00:00Z'; + + beforeEach(() => { + spyOn(Date, 'now').and.callFake(() => new Date(now).getTime()); + jasmine.clock().install(); + }); + + afterEach(() => { + vm.$destroy(); + jasmine.clock().uninstall(); + }); + + describe('when there is time remaining', () => { + beforeEach(done => { + vm = mountComponent(Component, { + endDateString: '2000-01-01T01:02:03Z', + }); + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('displays remaining time', () => { + expect(vm.$el).toContainText('01:02:03'); + }); + + it('updates remaining time', done => { + now = '2000-01-01T00:00:01Z'; + jasmine.clock().tick(1000); + + Vue.nextTick() + .then(() => { + expect(vm.$el).toContainText('01:02:02'); + done(); + }) + .catch(done.fail); + }); + }); + + describe('when there is no time remaining', () => { + beforeEach(done => { + vm = mountComponent(Component, { + endDateString: '1900-01-01T00:00:00Z', + }); + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('displays 00:00:00', () => { + expect(vm.$el).toContainText('00:00:00'); + }); + }); + + describe('when an invalid date is passed', () => { + it('throws a validation error', () => { + spyOn(Vue.config, 'warnHandler').and.stub(); + vm = mountComponent(Component, { + endDateString: 'this is invalid', + }); + + expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1); + const [errorMessage] = Vue.config.warnHandler.calls.argsFor(0); + + expect(errorMessage).toMatch(/^Invalid prop: .* "endDateString"/); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js index 026a0c7ea09..3483b7d387d 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -11,16 +11,6 @@ describe('collapsedGroupedDatePicker', () => { }); }); - it('should render toggle sidebar if showToggleSidebar', (done) => { - expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined(); - - vm.showToggleSidebar = false; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull(); - done(); - }); - }); - describe('toggleCollapse events', () => { beforeEach((done) => { spyOn(vm, 'toggleSidebar'); @@ -28,12 +18,6 @@ describe('collapsedGroupedDatePicker', () => { Vue.nextTick(done); }); - it('should emit when sidebar is toggled', () => { - vm.$el.querySelector('.gutter-toggle').click(); - - expect(vm.toggleSidebar).toHaveBeenCalled(); - }); - it('should emit when collapsed-calendar-icon is clicked', () => { vm.$el.querySelector('.sidebar-collapsed-icon').click(); @@ -92,5 +76,11 @@ describe('collapsedGroupedDatePicker', () => { expect(icons.length).toEqual(1); expect(icons[0].innerText.trim()).toEqual('None'); }); + + it('should have tooltip as `Start and due date`', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + + expect(icons[0].dataset.originalTitle).toBe('Start and due date'); + }); }); }); diff --git a/spec/javascripts/vue_shared/directives/tooltip_spec.js b/spec/javascripts/vue_shared/directives/tooltip_spec.js index 305d2fd5af4..1d516a280b0 100644 --- a/spec/javascripts/vue_shared/directives/tooltip_spec.js +++ b/spec/javascripts/vue_shared/directives/tooltip_spec.js @@ -13,24 +13,45 @@ describe('Tooltip directive', () => { describe('with a single tooltip', () => { beforeEach(() => { - const SomeComponent = Vue.extend({ + setFixtures('<div id="dummy-element"></div>'); + vm = new Vue({ + el: '#dummy-element', directives: { tooltip, }, - template: ` - <div - v-tooltip - title="foo"> - </div> - `, + data() { + return { + tooltip: 'some text', + }; + }, + template: '<div v-tooltip :title="tooltip"></div>', }); - - vm = new SomeComponent().$mount(); }); it('should have tooltip plugin applied', () => { expect($(vm.$el).data('bs.tooltip')).toBeDefined(); }); + + it('displays the title as tooltip', () => { + $(vm.$el).tooltip('show'); + const tooltipElement = document.querySelector('.tooltip-inner'); + + expect(tooltipElement.innerText).toContain('some text'); + }); + + it('updates a visible tooltip', done => { + $(vm.$el).tooltip('show'); + const tooltipElement = document.querySelector('.tooltip-inner'); + + vm.tooltip = 'other text'; + + Vue.nextTick() + .then(() => { + expect(tooltipElement).toContainText('other text'); + done(); + }) + .catch(done.fail); + }); }); describe('with multiple tooltips', () => { diff --git a/spec/lib/api/helpers/custom_validators_spec.rb b/spec/lib/api/helpers/custom_validators_spec.rb new file mode 100644 index 00000000000..41e6fb47b11 --- /dev/null +++ b/spec/lib/api/helpers/custom_validators_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe API::Helpers::CustomValidators do + let(:scope) do + Struct.new(:opts) do + def full_name(attr_name) + attr_name + end + end + end + + describe API::Helpers::CustomValidators::Absence do + subject do + described_class.new(['test'], {}, false, scope.new) + end + + context 'empty param' do + it 'does not raise a validation error' do + expect_no_validation_error({}) + end + end + + context 'invalid parameters' do + it 'should raise a validation error' do + expect_validation_error({ 'test' => 'some_value' }) + end + end + end + + describe API::Helpers::CustomValidators::IntegerNoneAny do + subject do + described_class.new(['test'], {}, false, scope.new) + end + + context 'valid parameters' do + it 'does not raise a validation error' do + expect_no_validation_error({ 'test' => 2 }) + expect_no_validation_error({ 'test' => 100 }) + expect_no_validation_error({ 'test' => 'None' }) + expect_no_validation_error({ 'test' => 'Any' }) + expect_no_validation_error({ 'test' => 'none' }) + expect_no_validation_error({ 'test' => 'any' }) + end + end + + context 'invalid parameters' do + it 'should raise a validation error' do + expect_validation_error({ 'test' => 'some_other_string' }) + end + end + end + + def expect_no_validation_error(params) + expect { validate_test_param!(params) }.not_to raise_error + end + + def expect_validation_error(params) + expect { validate_test_param!(params) }.to raise_error(Grape::Exceptions::Validation) + end + + def validate_test_param!(params) + subject.validate_param!('test', params) + end +end diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index 4d5081b0a75..e5999a1c509 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -282,6 +282,21 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do expect(pipeline_status.status).to eq(status) expect(pipeline_status.ref).to eq(ref) end + + context 'when status is empty string' do + before do + Gitlab::Redis::Cache.with do |redis| + redis.mapped_hmset(cache_key, + { sha: sha, status: '', ref: ref }) + end + end + + it 'reads the status as nil' do + pipeline_status.load_from_cache + + expect(pipeline_status.status).to eq(nil) + end + end end describe '#has_cache?' do diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 4df426c54ae..81804ba5c76 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -10,13 +10,16 @@ describe Gitlab::Checks::ChangeAccess do let(:ref) { 'refs/heads/master' } let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } } let(:protocol) { 'ssh' } + let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT } + let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) } subject(:change_access) do described_class.new( changes, project: project, user_access: user_access, - protocol: protocol + protocol: protocol, + logger: logger ) end @@ -30,6 +33,19 @@ describe Gitlab::Checks::ChangeAccess do end end + context 'when time limit was reached' do + it 'raises a TimeoutError' do + logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout) + access = described_class.new(changes, + project: project, + user_access: user_access, + protocol: protocol, + logger: logger) + + expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError) + end + end + context 'when the user is not allowed to push to the repo' do it 'raises an error' do expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb index ec22e3a198e..0488720cec8 100644 --- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb +++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Gitlab::Checks::LfsIntegrity do include ProjectForksHelper + let!(:time_left) { 50 } let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:newrev) do @@ -15,7 +16,7 @@ describe Gitlab::Checks::LfsIntegrity do operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects") end - subject { described_class.new(project, newrev) } + subject { described_class.new(project, newrev, time_left) } describe '#objects_missing?' do let(:blob_object) { repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } diff --git a/spec/lib/gitlab/checks/timed_logger_spec.rb b/spec/lib/gitlab/checks/timed_logger_spec.rb new file mode 100644 index 00000000000..0ed3940c038 --- /dev/null +++ b/spec/lib/gitlab/checks/timed_logger_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Checks::TimedLogger do + let!(:timeout) { 50.seconds } + let!(:start) { Time.now } + let!(:ref) { "bar" } + let!(:logger) { described_class.new(start_time: start, timeout: timeout) } + let!(:log_messages) do + { + foo: "Foo message..." + } + end + + before do + logger.append_message("Checking ref: #{ref}") + end + + describe '#log_timed' do + it 'logs message' do + Timecop.freeze(start + 30.seconds) do + logger.log_timed(log_messages[:foo], start) { bar_check } + end + + expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (30000.0ms)") + end + + context 'when time limit was reached' do + it 'cancels action' do + Timecop.freeze(start + 50.seconds) do + expect do + logger.log_timed(log_messages[:foo], start) do + bar_check + end + end.to raise_error(described_class::TimeoutError) + end + + expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled)") + end + + it 'cancels action with time elapsed if work was performed' do + Timecop.freeze(start + 30.seconds) do + expect do + logger.log_timed(log_messages[:foo], start) do + grpc_check + end + end.to raise_error(described_class::TimeoutError) + + expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled after 30000.0ms)") + end + end + end + end + + def bar_check + 2 + 2 + end + + def grpc_check + raise GRPC::DeadlineExceeded + end +end diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb index c5e7ab959b2..d035df7e0c2 100644 --- a/spec/lib/gitlab/git/lfs_changes_spec.rb +++ b/spec/lib/gitlab/git/lfs_changes_spec.rb @@ -15,5 +15,9 @@ describe Gitlab::Git::LfsChanges do it 'limits new_objects using object_limit' do expect(subject.new_pointers(object_limit: 1)).to eq([]) end + + it 'times out if given a small dynamic timeout' do + expect { subject.new_pointers(dynamic_timeout: 0.001) }.to raise_error(GRPC::DeadlineExceeded) + end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index e7da5565c26..a417ef77c9e 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -934,6 +934,16 @@ describe Gitlab::GitAccess do # There is still an N+1 query with protected branches expect { access.check('git-receive-pack', changes) }.not_to exceed_query_limit(control_count).with_threshold(1) end + + it 'raises TimeoutError when #check_single_change_access raises a timeout error' do + message = "Push operation timed out\n\nTiming information for debugging purposes:\nRunning checks for ref: wow" + + expect_next_instance_of(Gitlab::Checks::ChangeAccess) do |check| + expect(check).to receive(:exec).and_raise(Gitlab::Checks::TimedLogger::TimeoutError) + end + + expect { access.check('git-receive-pack', changes) }.to raise_error(described_class::TimeoutError, message) + end end end diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb index 0385dd762c2..1e583f4cee2 100644 --- a/spec/lib/gitlab/identifier_spec.rb +++ b/spec/lib/gitlab/identifier_spec.rb @@ -11,11 +11,8 @@ describe Gitlab::Identifier do describe '#identify' do context 'without an identifier' do - it 'identifies the user using a commit' do - expect(identifier).to receive(:identify_using_commit) - .with(project, '123') - - identifier.identify('', project, '123') + it 'returns nil' do + expect(identifier.identify('')).to be nil end end @@ -24,7 +21,7 @@ describe Gitlab::Identifier do expect(identifier).to receive(:identify_using_user) .with("user-#{user.id}") - identifier.identify("user-#{user.id}", project, '123') + identifier.identify("user-#{user.id}") end end @@ -33,49 +30,11 @@ describe Gitlab::Identifier do expect(identifier).to receive(:identify_using_ssh_key) .with("key-#{key.id}") - identifier.identify("key-#{key.id}", project, '123') + identifier.identify("key-#{key.id}") end end end - describe '#identify_using_commit' do - it "returns the User for an existing commit author's Email address" do - commit = double(:commit, author: user, author_email: user.email) - - expect(project).to receive(:commit).with('123').and_return(commit) - - expect(identifier.identify_using_commit(project, '123')).to eq(user) - end - - it 'returns nil when no user could be found' do - allow(project).to receive(:commit).with('123').and_return(nil) - - expect(identifier.identify_using_commit(project, '123')).to be_nil - end - - it 'returns nil when the commit does not have an author Email' do - commit = double(:commit, author_email: nil) - - expect(project).to receive(:commit).with('123').and_return(commit) - - expect(identifier.identify_using_commit(project, '123')).to be_nil - end - - it 'caches the found users per Email' do - commit = double(:commit, author: user, author_email: user.email) - - expect(project).to receive(:commit).with('123').twice.and_return(commit) - - 2.times do - expect(identifier.identify_using_commit(project, '123')).to eq(user) - end - end - - it 'returns nil if the project & ref are not present' do - expect(identifier.identify_using_commit(nil, nil)).to be_nil - end - end - describe '#identify_using_user' do it 'returns the User for an existing ID in the identifier' do found = identifier.identify_using_user("user-#{user.id}") diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 53c5a4e7c94..eed4135d8a2 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -6,104 +6,63 @@ describe Gitlab::Kubernetes::KubeClient do include KubernetesHelpers let(:api_url) { 'https://kubernetes.example.com/prefix' } - let(:api_groups) { ['api', 'apis/rbac.authorization.k8s.io'] } - let(:api_version) { 'v1' } let(:kubeclient_options) { { auth_options: { bearer_token: 'xyz' } } } - let(:client) { described_class.new(api_url, api_groups, api_version, kubeclient_options) } + let(:client) { described_class.new(api_url, kubeclient_options) } before do stub_kubeclient_discover(api_url) end - describe '#hashed_clients' do - subject { client.hashed_clients } - - it 'has keys from api groups' do - expect(subject.keys).to match_array api_groups - end - - it 'has values of Kubeclient::Client' do - expect(subject.values).to all(be_an_instance_of Kubeclient::Client) - end - end - - describe '#clients' do - subject { client.clients } - - it 'is not empty' do - is_expected.to be_present - end - - it 'is an array of Kubeclient::Client objects' do - is_expected.to all(be_an_instance_of Kubeclient::Client) - end - - it 'has each API group url' do - expected_urls = api_groups.map { |group| "#{api_url}/#{group}" } - - expect(subject.map(&:api_endpoint).map(&:to_s)).to match_array(expected_urls) + shared_examples 'a Kubeclient' do + it 'is a Kubeclient::Client' do + is_expected.to be_an_instance_of Kubeclient::Client end it 'has the kubeclient options' do - subject.each do |client| - expect(client.auth_options).to eq({ bearer_token: 'xyz' }) - end - end - - it 'has the api_version' do - subject.each do |client| - expect(client.instance_variable_get(:@api_version)).to eq('v1') - end + expect(subject.auth_options).to eq({ bearer_token: 'xyz' }) end end describe '#core_client' do subject { client.core_client } - it 'is a Kubeclient::Client' do - is_expected.to be_an_instance_of Kubeclient::Client - end + it_behaves_like 'a Kubeclient' it 'has the core API endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/api\Z}) end + + it 'has the api_version' do + expect(subject.instance_variable_get(:@api_version)).to eq('v1') + end end describe '#rbac_client' do subject { client.rbac_client } - it 'is a Kubeclient::Client' do - is_expected.to be_an_instance_of Kubeclient::Client - end + it_behaves_like 'a Kubeclient' it 'has the RBAC API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/rbac.authorization.k8s.io\Z}) end + + it 'has the api_version' do + expect(subject.instance_variable_get(:@api_version)).to eq('v1') + end end describe '#extensions_client' do subject { client.extensions_client } - let(:api_groups) { ['apis/extensions'] } - - it 'is a Kubeclient::Client' do - is_expected.to be_an_instance_of Kubeclient::Client - end + it_behaves_like 'a Kubeclient' it 'has the extensions API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/extensions\Z}) end - end - describe '#discover!' do - it 'makes a discovery request for each API group' do - client.discover! - - api_groups.each do |api_group| - discovery_url = api_url + '/' + api_group + '/v1' - expect(WebMock).to have_requested(:get, discovery_url).once - end + it 'has the api_version' do + expect(subject.instance_variable_get(:@api_version)).to eq('v1beta1') end end @@ -156,21 +115,12 @@ describe Gitlab::Kubernetes::KubeClient do it 'responds to the method' do expect(client).to respond_to method end - - context 'no rbac client' do - let(:api_groups) { ['api'] } - - it 'throws an error' do - expect { client.public_send(method) }.to raise_error(Module::DelegationError) - end - end end end end describe 'extensions API group' do let(:api_groups) { ['apis/extensions'] } - let(:api_version) { 'v1beta1' } let(:extensions_client) { client.extensions_client } describe '#get_deployments' do @@ -181,22 +131,11 @@ describe Gitlab::Kubernetes::KubeClient do it 'responds to the method' do expect(client).to respond_to :get_deployments end - - context 'no extensions client' do - let(:api_groups) { ['api'] } - let(:api_version) { 'v1' } - - it 'throws an error' do - expect { client.get_deployments }.to raise_error(Module::DelegationError) - end - end end end describe 'non-entity methods' do it 'does not proxy for non-entity methods' do - expect(client.clients.first).to respond_to :proxy_url - expect(client).not_to respond_to :proxy_url end @@ -211,14 +150,6 @@ describe Gitlab::Kubernetes::KubeClient do it 'is delegated to the core client' do expect(client).to delegate_method(:get_pod_log).to(:core_client) end - - context 'when no core client' do - let(:api_groups) { ['apis/extensions'] } - - it 'throws an error' do - expect { client.get_pod_log('pod-name') }.to raise_error(Module::DelegationError) - end - end end describe '#watch_pod_log' do @@ -227,14 +158,6 @@ describe Gitlab::Kubernetes::KubeClient do it 'is delegated to the core client' do expect(client).to delegate_method(:watch_pod_log).to(:core_client) end - - context 'when no core client' do - let(:api_groups) { ['apis/extensions'] } - - it 'throws an error' do - expect { client.watch_pod_log('pod-name') }.to raise_error(Module::DelegationError) - end - end end describe 'methods that do not exist on any client' do diff --git a/spec/lib/gitlab/patch/draw_route_spec.rb b/spec/lib/gitlab/patch/draw_route_spec.rb new file mode 100644 index 00000000000..4009b903dc3 --- /dev/null +++ b/spec/lib/gitlab/patch/draw_route_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Patch::DrawRoute do + subject do + Class.new do + include Gitlab::Patch::DrawRoute + + def route_path(route_name) + File.expand_path("../../../../#{route_name}", __dir__) + end + end.new + end + + before do + allow(subject).to receive(:instance_eval) + end + + it 'evaluates CE only route' do + subject.draw(:help) + + expect(subject).to have_received(:instance_eval) + .with(File.read(subject.route_path('config/routes/help.rb'))) + .once + + expect(subject).to have_received(:instance_eval) + .once + end +end diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb new file mode 100644 index 00000000000..59ad4e5417e --- /dev/null +++ b/spec/models/board_group_recent_visit_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BoardGroupRecentVisit do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:board) { create(:board, group: group) } + + describe 'relationships' do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:board) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:group) } + it { is_expected.to validate_presence_of(:board) } + end + + describe '#visited' do + it 'creates a visit if one does not exists' do + expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) + end + + shared_examples 'was visited previously' do + let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago } + + it 'updates the timestamp' do + Timecop.freeze do + described_class.visited!(user, board) + + expect(described_class.count).to eq 1 + expect(described_class.first.updated_at).to be_like_time(Time.zone.now) + end + end + end + + it_behaves_like 'was visited previously' + + context 'when we try to create a visit that is not unique' do + before do + expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') + expect(described_class).to receive(:find_or_create_by).and_return(visit) + end + + it_behaves_like 'was visited previously' + end + end + + describe '#latest' do + it 'returns the most recent visited' do + board2 = create(:board, group: group) + board3 = create(:board, group: group) + + create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago + create :board_group_recent_visit, group: board2.group, board: board2, user: user, updated_at: 5.days.ago + recent = create :board_group_recent_visit, group: board3.group, board: board3, user: user, updated_at: 1.day.ago + + expect(described_class.latest(user, group)).to eq recent + end + end +end diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb new file mode 100644 index 00000000000..275581945fa --- /dev/null +++ b/spec/models/board_project_recent_visit_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BoardProjectRecentVisit do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:board) { create(:board, project: project) } + + describe 'relationships' do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:board) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:board) } + end + + describe '#visited' do + it 'creates a visit if one does not exists' do + expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) + end + + shared_examples 'was visited previously' do + let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago } + + it 'updates the timestamp' do + Timecop.freeze do + described_class.visited!(user, board) + + expect(described_class.count).to eq 1 + expect(described_class.first.updated_at).to be_like_time(Time.zone.now) + end + end + end + + it_behaves_like 'was visited previously' + + context 'when we try to create a visit that is not unique' do + before do + expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') + expect(described_class).to receive(:find_or_create_by).and_return(visit) + end + + it_behaves_like 'was visited previously' + end + end + + describe '#latest' do + it 'returns the most recent visited' do + board2 = create(:board, project: project) + board3 = create(:board, project: project) + + create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago + create :board_project_recent_visit, project: board2.project, board: board2, user: user, updated_at: 5.days.ago + recent = create :board_project_recent_visit, project: board3.project, board: board3, user: user, updated_at: 1.day.ago + + expect(described_class.latest(user, project)).to eq recent + end + end +end diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 23643d1c4d2..d5fb1a9d010 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -17,7 +17,7 @@ describe Clusters::Applications::Runner do let(:application) { create(:clusters_applications_runner, :scheduled, version: '0.1.30') } it 'updates the application version' do - expect(application.reload.version).to eq('0.1.34') + expect(application.reload.version).to eq('0.1.35') end end end @@ -45,7 +45,7 @@ describe Clusters::Applications::Runner do it 'should be initialized with 4 arguments' do expect(subject.name).to eq('runner') expect(subject.chart).to eq('runner/gitlab-runner') - expect(subject.version).to eq('0.1.34') + expect(subject.version).to eq('0.1.35') expect(subject).not_to be_rbac expect(subject.repository).to eq('https://charts.gitlab.io') expect(subject.files).to eq(gitlab_runner.files) @@ -63,7 +63,7 @@ describe Clusters::Applications::Runner do let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') } it 'should be initialized with the locked version' do - expect(subject.version).to eq('0.1.34') + expect(subject.version).to eq('0.1.35') end end end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index f5c4b0b66ae..c245e8df815 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -4,7 +4,10 @@ require 'spec_helper' describe Clusters::Cluster do it { is_expected.to belong_to(:user) } + it { is_expected.to have_many(:cluster_projects) } it { is_expected.to have_many(:projects) } + it { is_expected.to have_many(:cluster_groups) } + it { is_expected.to have_many(:groups) } it { is_expected.to have_one(:provider_gcp) } it { is_expected.to have_one(:platform_kubernetes) } it { is_expected.to have_one(:application_helm) } @@ -178,6 +181,53 @@ describe Clusters::Cluster do it { expect(cluster.update(enabled: false)).to be_truthy } end end + + describe 'cluster_type validations' do + let(:instance_cluster) { create(:cluster, :instance) } + let(:group_cluster) { create(:cluster, :group) } + let(:project_cluster) { create(:cluster, :project) } + + it 'validates presence' do + cluster = build(:cluster, :project, cluster_type: nil) + + expect(cluster).not_to be_valid + expect(cluster.errors.full_messages).to include("Cluster type can't be blank") + end + + context 'project_type cluster' do + it 'does not allow setting group' do + project_cluster.groups << build(:group) + + expect(project_cluster).not_to be_valid + expect(project_cluster.errors.full_messages).to include('Cluster cannot have groups assigned') + end + end + + context 'group_type cluster' do + it 'does not allow setting project' do + group_cluster.projects << build(:project) + + expect(group_cluster).not_to be_valid + expect(group_cluster.errors.full_messages).to include('Cluster cannot have projects assigned') + end + end + + context 'instance_type cluster' do + it 'does not allow setting group' do + instance_cluster.groups << build(:group) + + expect(instance_cluster).not_to be_valid + expect(instance_cluster.errors.full_messages).to include('Cluster cannot have groups assigned') + end + + it 'does not allow setting project' do + instance_cluster.projects << build(:project) + + expect(instance_cluster).not_to be_valid + expect(instance_cluster.errors.full_messages).to include('Cluster cannot have projects assigned') + end + end + end end describe '#provider' do @@ -229,6 +279,23 @@ describe Clusters::Cluster do end end + describe '#group' do + subject { cluster.group } + + context 'when cluster belongs to a group' do + let(:cluster) { create(:cluster, :group) } + let(:group) { cluster.groups.first } + + it { is_expected.to eq(group) } + end + + context 'when cluster does not belong to any group' do + let(:cluster) { create(:cluster) } + + it { is_expected.to be_nil } + end + end + describe '#applications' do set(:cluster) { create(:cluster) } diff --git a/spec/models/clusters/group_spec.rb b/spec/models/clusters/group_spec.rb new file mode 100644 index 00000000000..ba145342cb8 --- /dev/null +++ b/spec/models/clusters/group_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Group do + it { is_expected.to belong_to(:cluster) } + it { is_expected.to belong_to(:group) } +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index c65e0b81451..1de95d881a7 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -334,7 +334,7 @@ describe Environment do describe '#has_terminals?' do subject { environment.has_terminals? } - context 'when the enviroment is available' do + context 'when the environment is available' do context 'with a deployment service' do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do context 'and a deployment' do @@ -447,7 +447,7 @@ describe Environment do describe '#has_metrics?' do subject { environment.has_metrics? } - context 'when the enviroment is available' do + context 'when the environment is available' do context 'with a deployment service' do let(:project) { create(:prometheus_project) } @@ -456,8 +456,8 @@ describe Environment do it { is_expected.to be_truthy } end - context 'but no deployments' do - it { is_expected.to be_falsy } + context 'and no deployments' do + it { is_expected.to be_truthy } end end @@ -504,39 +504,6 @@ describe Environment do end end - describe '#has_metrics?' do - subject { environment.has_metrics? } - - context 'when the enviroment is available' do - context 'with a deployment service' do - let(:project) { create(:prometheus_project) } - - context 'and a deployment' do - let!(:deployment) { create(:deployment, environment: environment) } - it { is_expected.to be_truthy } - end - - context 'but no deployments' do - it { is_expected.to be_falsy } - end - end - - context 'without a monitoring service' do - it { is_expected.to be_falsy } - end - end - - context 'when the environment is unavailable' do - let(:project) { create(:prometheus_project) } - - before do - environment.stop - end - - it { is_expected.to be_falsy } - end - end - describe '#additional_metrics' do let(:project) { create(:prometheus_project) } subject { environment.additional_metrics } diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index f2eb263c98c..e7805d52d75 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -5,13 +5,15 @@ describe EnvironmentStatus do let(:environment) { deployment.environment} let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } + let(:sha) { deployment.sha } - subject(:environment_status) { described_class.new(environment, merge_request) } + subject(:environment_status) { described_class.new(environment, merge_request, sha) } it { is_expected.to delegate_method(:id).to(:environment) } it { is_expected.to delegate_method(:name).to(:environment) } it { is_expected.to delegate_method(:project).to(:environment) } it { is_expected.to delegate_method(:deployed_at).to(:deployment).as(:created_at) } + it { is_expected.to delegate_method(:status).to(:deployment) } describe '#project' do subject { environment_status.project } @@ -58,4 +60,32 @@ describe EnvironmentStatus do ) end end + + describe '.for_merge_request' do + let(:admin) { create(:admin) } + let(:pipeline) { create(:ci_pipeline, sha: sha) } + + it 'is based on merge_request.head_pipeline' do + expect(merge_request).to receive(:head_pipeline).and_return(pipeline) + expect(merge_request).not_to receive(:merge_pipeline) + + described_class.for_merge_request(merge_request, admin) + end + end + + describe '.after_merge_request' do + let(:admin) { create(:admin) } + let(:pipeline) { create(:ci_pipeline, sha: sha) } + + before do + merge_request.mark_as_merged! + end + + it 'is based on merge_request.merge_pipeline' do + expect(merge_request).to receive(:merge_pipeline).and_return(pipeline) + expect(merge_request).not_to receive(:head_pipeline) + + described_class.after_merge_request(merge_request, admin) + end + end end diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index ab58f5c5021..b6355455c1d 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -92,41 +92,6 @@ describe GlobalMilestone do end end - describe '.states_count' do - context 'when the projects have milestones' do - before do - create(:closed_milestone, title: 'Active Group Milestone', project: project3) - create(:active_milestone, title: 'Active Group Milestone', project: project1) - create(:active_milestone, title: 'Active Group Milestone', project: project2) - create(:closed_milestone, title: 'Closed Group Milestone', project: project1) - create(:closed_milestone, title: 'Closed Group Milestone', project: project2) - create(:closed_milestone, title: 'Closed Group Milestone', project: project3) - end - - it 'returns the quantity of global milestones in each possible state' do - expected_count = { opened: 1, closed: 2, all: 2 } - - count = described_class.states_count(Project.all) - - expect(count).to eq(expected_count) - end - end - - context 'when the projects do not have milestones' do - before do - project1 - end - - it 'returns 0 as the quantity of global milestones in each state' do - expected_count = { opened: 0, closed: 0, all: 0 } - - count = described_class.states_count(Project.all) - - expect(count).to eq(expected_count) - end - end - end - describe '#initialize' do let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 1bf8f89e126..571b160d901 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -19,6 +19,8 @@ describe Group do it { is_expected.to have_one(:chat_team) } it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') } it { is_expected.to have_many(:badges).class_name('GroupBadge') } + it { is_expected.to have_many(:cluster_groups).class_name('Clusters::Group') } + it { is_expected.to have_many(:clusters).class_name('Clusters::Cluster') } describe '#members & #requesters' do let(:requester) { create(:user) } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 666d7e69f89..c8943f2d86f 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1058,6 +1058,26 @@ describe MergeRequest do end end + describe '#merge_pipeline' do + it 'returns nil when not merged' do + expect(subject.merge_pipeline).to be_nil + end + + context 'when the MR is merged' do + let(:sha) { subject.target_project.commit.id } + let(:pipeline) { create(:ci_empty_pipeline, sha: sha, ref: subject.target_branch, project: subject.target_project) } + + before do + subject.mark_as_merged! + subject.update_attribute(:merge_commit_sha, pipeline.sha) + end + + it 'returns the post-merge pipeline' do + expect(subject.merge_pipeline).to eq(pipeline) + end + end + end + describe '#has_ci?' do let(:merge_request) { build_stubbed(:merge_request) } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 27d4e622710..d11eb46159e 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -348,4 +348,41 @@ describe Milestone do end end end + + describe '.states_count' do + context 'when the projects have milestones' do + before do + project_1 = create(:project) + project_2 = create(:project) + group_1 = create(:group) + group_2 = create(:group) + + create(:active_milestone, title: 'Active Group Milestone', project: project_1) + create(:closed_milestone, title: 'Closed Group Milestone', project: project_1) + create(:active_milestone, title: 'Active Group Milestone', project: project_2) + create(:closed_milestone, title: 'Closed Group Milestone', project: project_2) + create(:closed_milestone, title: 'Active Group Milestone', group: group_1) + create(:closed_milestone, title: 'Closed Group Milestone', group: group_1) + create(:closed_milestone, title: 'Active Group Milestone', group: group_2) + create(:closed_milestone, title: 'Closed Group Milestone', group: group_2) + end + + it 'returns the quantity of milestones in each possible state' do + expected_count = { opened: 5, closed: 6, all: 11 } + + count = described_class.states_count(Project.all, Group.all) + expect(count).to eq(expected_count) + end + end + + context 'when the projects do not have milestones' do + it 'returns 0 as the quantity of global milestones in each state' do + expected_count = { opened: 0, closed: 0, all: 0 } + + count = described_class.states_count([project]) + + expect(count).to eq(expected_count) + end + end + end end diff --git a/spec/models/ssh_host_key_spec.rb b/spec/models/ssh_host_key_spec.rb new file mode 100644 index 00000000000..75db43b3d56 --- /dev/null +++ b/spec/models/ssh_host_key_spec.rb @@ -0,0 +1,164 @@ +require 'spec_helper' + +describe SshHostKey do + using RSpec::Parameterized::TableSyntax + include ReactiveCachingHelpers + + let(:key1) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3UpyF2iLqy1d63M6k3jH1vuEnq/NWtE+o' \ + 'rJe1Xn7JoRbduKd6zpsJ0JhBGWgcQK0ph0aGW5PcudzzBSc+SlYfCc4GTaxDtmj41hW0o72m' \ + 'NiuDW3oKXXShOiVRde2ZOquH8Z865jGiZIC8BI/bXZD29IGUih0hPu7Rjp70VYiE+35QRf/p' \ + 'sD0Ddrz8QUIG3A/2dMzLI5F5ZORk3BIX2F3mJwJOvZxRhR/SqyphDMZ5eZ0EzqbFBCDE6HAB' \ + 'Woz9ck8RBGLvCIggmDHj3FmMLcQGMDiy6wKp7QdnBtxjCP6vtE6YPUM223AqsWt+9NTtCfB8' \ + 'YdNAH7YcHHOR1FgtSk1x' + end + + let(:key2) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLIp+4ciR2YO9f9rpldc7InNQw/TBUtcNb' \ + 'J2XR0rr15/5ytz7YM16xXG0Qjx576PNSmqs4gbTrvTuFZak+v1Jx/9deHRq/yqp9f+tv33+i' \ + 'aJGCQCX/+OVY7aWgV2R9YsS7XQ4mnv4XlOTEssib/rGAIT+ATd/GcdYSEOO+dh4O09/6O/jI' \ + 'MGSeP+NNetgn1nPCnLOjrXFZUnUtNDi6EEKeIlrliJjSb7Jr4f7gjvZnv4RskWHHFo8FgAAq' \ + 't0gOMT6EmKrnypBe2vLGSAXbtkXr01q6/DNPH+n9VA1LTV6v1KN/W5CN5tQV11wRSKiM8g5O' \ + 'Ebi86VjJRi2sOuYoXQU1' + end + + # Purposefully ordered so that `sort` will make changes + let(:known_hosts) do + <<~EOF + example.com #{key1} git@localhost + @revoked other.example.com #{key2} git@localhost + EOF + end + + let(:extra) { known_hosts + "foo\nbar\n" } + let(:reversed) { known_hosts.lines.reverse.join } + + let(:compare_host_keys) { nil } + + def stub_ssh_keyscan(args, status: true, stdout: "", stderr: "") + stdin = StringIO.new + stdout = double(:stdout, read: stdout) + stderr = double(:stderr, read: stderr) + wait_thr = double(:wait_thr, value: double(success?: status)) + + expect(Open3).to receive(:popen3).with({}, 'ssh-keyscan', *args).and_yield(stdin, stdout, stderr, wait_thr) + + stdin + end + + let(:project) { build(:project) } + + subject(:ssh_host_key) { described_class.new(project: project, url: 'ssh://example.com:2222', compare_host_keys: compare_host_keys) } + + describe '#fingerprints', :use_clean_rails_memory_store_caching do + it 'returns an array of indexed fingerprints when the cache is filled' do + stub_reactive_cache(ssh_host_key, known_hosts: known_hosts) + + expected = [key1, key2] + .map { |data| Gitlab::SSHPublicKey.new(data) } + .each_with_index + .map { |key, i| { bits: key.bits, fingerprint: key.fingerprint, type: key.type, index: i } } + + expect(ssh_host_key.fingerprints.as_json).to eq(expected) + end + + it 'returns an empty array when the cache is empty' do + expect(ssh_host_key.fingerprints).to eq([]) + end + end + + describe '#fingerprints', :use_clean_rails_memory_store_caching do + it 'returns an array of indexed fingerprints when the cache is filled' do + stub_reactive_cache(ssh_host_key, known_hosts: known_hosts) + + expect(ssh_host_key.fingerprints.as_json).to eq( + [ + { bits: 2048, fingerprint: Gitlab::SSHPublicKey.new(key1).fingerprint, type: :rsa, index: 0 }, + { bits: 2048, fingerprint: Gitlab::SSHPublicKey.new(key2).fingerprint, type: :rsa, index: 1 } + ] + ) + end + + it 'returns an empty array when the cache is empty' do + expect(ssh_host_key.fingerprints).to eq([]) + end + end + + describe '#host_keys_changed?' do + where(:known_hosts_a, :known_hosts_b, :result) do + known_hosts | extra | true + known_hosts | "foo\n" | true + known_hosts | '' | true + known_hosts | nil | true + known_hosts | known_hosts | false + reversed | known_hosts | false + extra | "foo\n" | true + '' | '' | false + nil | nil | false + '' | nil | false + end + + with_them do + let(:compare_host_keys) { known_hosts_b } + + subject { ssh_host_key.host_keys_changed? } + + context '(normal)' do + let(:compare_host_keys) { known_hosts_b } + + before do + expect(ssh_host_key).to receive(:known_hosts).and_return(known_hosts_a) + end + + it { is_expected.to eq(result) } + end + + # Comparisons should be symmetrical, so test the reverse too + context '(reversed)' do + let(:compare_host_keys) { known_hosts_a } + + before do + expect(ssh_host_key).to receive(:known_hosts).and_return(known_hosts_b) + end + + it { is_expected.to eq(result) } + end + end + end + + describe '#calculate_reactive_cache' do + subject(:cache) { ssh_host_key.calculate_reactive_cache } + + it 'writes the hostname to STDIN' do + stdin = stub_ssh_keyscan(%w[-T 5 -p 2222 -f-]) + + cache + + expect(stdin.string).to eq("example.com\n") + end + + context 'successful key scan' do + it 'stores the cleaned known_hosts data' do + stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stdout: "KEY 1\nKEY 1\n\n# comment\nKEY 2\n") + + is_expected.to eq(known_hosts: "KEY 1\nKEY 2\n") + end + end + + context 'failed key scan (exit code 1)' do + it 'returns a generic error' do + stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stdout: 'blarg', status: false) + + is_expected.to eq(error: 'Failed to detect SSH host keys') + end + end + + context 'failed key scan (exit code 0)' do + it 'returns a generic error' do + stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stderr: 'Unknown host') + + is_expected.to eq(error: 'Failed to detect SSH host keys') + end + end + end +end diff --git a/spec/rack_servers/configs/config.ru b/spec/rack_servers/configs/config.ru new file mode 100644 index 00000000000..63daeb9eec5 --- /dev/null +++ b/spec/rack_servers/configs/config.ru @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +app = proc do |env| + if env['REQUEST_METHOD'] == 'GET' + [200, {}, ["#{Process.pid}"]] + else + Process.kill(env['QUERY_STRING'], Process.pid) + [200, {}, ['Bye!']] + end +end + +run app diff --git a/spec/rack_servers/configs/puma.rb b/spec/rack_servers/configs/puma.rb new file mode 100644 index 00000000000..d6b6d83d648 --- /dev/null +++ b/spec/rack_servers/configs/puma.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Note: this file is used for testing puma in `spec/rack_servers/puma_spec.rb` only +# Note: as per the convention in `config/puma.example.development.rb`, +# this file will replace `/home/git` with the actual working directory + +directory '/home/git' +threads 1, 10 +queue_requests false +pidfile '/home/git/gitlab/tmp/pids/puma.pid' +bind 'unix:///home/git/gitlab/tmp/tests/puma.socket' +workers 1 +preload_app! +worker_timeout 60 + +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" +require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer" + +before_fork do + Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options +on_worker_boot do + Gitlab::Cluster::LifecycleEvents.do_worker_start + File.write('/home/git/gitlab/tmp/tests/puma-worker-ready', Process.pid) +end + +on_restart do + Gitlab::Cluster::LifecycleEvents.do_master_restart +end diff --git a/spec/rack_servers/puma_spec.rb b/spec/rack_servers/puma_spec.rb new file mode 100644 index 00000000000..431fab87857 --- /dev/null +++ b/spec/rack_servers/puma_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'fileutils' + +require 'excon' + +require 'spec_helper' + +describe 'Puma' do + before(:all) do + project_root = File.expand_path('../..', __dir__) + + config_lines = File.read('spec/rack_servers/configs/puma.rb') + .gsub('/home/git/gitlab', project_root) + .gsub('/home/git', project_root) + + config_path = File.join(project_root, "tmp/tests/puma.rb") + @socket_path = File.join(project_root, 'tmp/tests/puma.socket') + + File.write(config_path, config_lines) + + cmd = %W[puma -e test -C #{config_path} #{File.join(__dir__, 'configs/config.ru')}] + @puma_master_pid = spawn(*cmd) + wait_puma_boot!(@puma_master_pid, File.join(project_root, 'tmp/tests/puma-worker-ready')) + WebMock.allow_net_connect! + end + + %w[SIGQUIT SIGTERM SIGKILL].each do |signal| + it "has a worker that self-terminates on signal #{signal}" do + response = Excon.get('unix://', socket: @socket_path) + expect(response.status).to eq(200) + + worker_pid = response.body.to_i + expect(worker_pid).to be > 0 + + begin + Excon.post("unix://?#{signal}", socket: @socket_path) + rescue Excon::Error::Socket + # The connection may be closed abruptly + end + + expect(pid_gone?(worker_pid)).to eq(true) + end + end + + after(:all) do + begin + WebMock.disable_net_connect!(allow_localhost: true) + Process.kill('TERM', @puma_master_pid) + rescue Errno::ESRCH + end + end + + def wait_puma_boot!(master_pid, ready_file) + # We have seen the boot timeout after 2 minutes in CI so let's set it to 5 minutes. + timeout = 5 * 60 + timeout.times do + return if File.exist?(ready_file) + + pid = Process.waitpid(master_pid, Process::WNOHANG) + raise "puma failed to boot: #{$?}" unless pid.nil? + + sleep 1 + end + + raise "puma boot timed out after #{timeout} seconds" + end + + def pid_gone?(pid) + # Worker termination should take less than a second. That makes 10 + # seconds a generous timeout. + 10.times do + begin + Process.kill(0, pid) + rescue Errno::ESRCH + return true + end + + sleep 1 + end + + false + end +end diff --git a/spec/unicorn/unicorn_spec.rb b/spec/rack_servers/unicorn_spec.rb index a4cf479a339..6a02ebcd048 100644 --- a/spec/unicorn/unicorn_spec.rb +++ b/spec/rack_servers/unicorn_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fileutils' require 'excon' @@ -6,12 +8,16 @@ require 'spec_helper' describe 'Unicorn' do before(:all) do - config_lines = File.read('config/unicorn.rb.example').split("\n") + project_root = File.expand_path('../..', __dir__) + + config_lines = File.read('config/unicorn.rb.example') + .gsub('/home/git/gitlab', project_root) + .gsub('/home/git', project_root) + .split("\n") # Remove these because they make setup harder. config_lines = config_lines.reject do |line| %w[ - working_directory worker_processes listen pid @@ -26,33 +32,18 @@ describe 'Unicorn' do # predictable which process will handle our requests. config_lines << 'worker_processes 1' - @socket_path = File.join(Dir.pwd, 'tmp/tests/unicorn.socket') + @socket_path = File.join(project_root, 'tmp/tests/unicorn.socket') config_lines << "listen '#{@socket_path}'" - ready_file = 'tmp/tests/unicorn-worker-ready' + ready_file = File.join(project_root, 'tmp/tests/unicorn-worker-ready') FileUtils.rm_f(ready_file) after_fork_index = config_lines.index { |l| l.start_with?('after_fork') } config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)") - config_path = 'tmp/tests/unicorn.rb' + config_path = File.join(project_root, 'tmp/tests/unicorn.rb') File.write(config_path, config_lines.join("\n") + "\n") - rackup_path = 'tmp/tests/config.ru' - File.write(rackup_path, <<~EOS) - app = - proc do |env| - if env['REQUEST_METHOD'] == 'GET' - [200, {}, [Process.pid]] - else - Process.kill(env['QUERY_STRING'], Process.pid) - [200, {}, ['Bye!']] - end - end - - run app - EOS - - cmd = %W[unicorn -E test -c #{config_path} #{rackup_path}] + cmd = %W[unicorn -E test -c #{config_path} spec/rack_servers/configs/config.ru] @unicorn_master_pid = spawn(*cmd) wait_unicorn_boot!(@unicorn_master_pid, ready_file) WebMock.allow_net_connect! diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index e0b5b34f9c4..2ebcb787d06 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -494,6 +494,24 @@ describe API::Internal do end end + context 'request times out' do + context 'git push' do + it 'responds with a gateway timeout' do + personal_project = create(:project, namespace: user.namespace) + + expect_next_instance_of(Gitlab::GitAccess) do |access| + expect(access).to receive(:check).and_raise(Gitlab::GitAccess::TimeoutError, "Foo") + end + push(key, personal_project) + + expect(response).to have_gitlab_http_status(503) + expect(json_response['status']).to be_falsey + expect(json_response['message']).to eq("Foo") + expect(user.reload.last_activity_on).to be_nil + end + end + end + context "archived project" do before do project.add_developer(user) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9f6cf12f9a7..9cda39a569b 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -178,6 +178,24 @@ describe API::Issues do expect(first_issue['id']).to eq(issue2.id) end + it 'returns issues with no assignee' do + issue2 = create(:issue, author: user2, project: project) + + get api('/issues', user), assignee_id: 'None', scope: 'all' + + expect_paginated_array_response(size: 1) + expect(first_issue['id']).to eq(issue2.id) + end + + it 'returns issues with any assignee' do + # This issue without assignee should not be returned + create(:issue, author: user2, project: project) + + get api('/issues', user), assignee_id: 'Any', scope: 'all' + + expect_paginated_array_response(size: 3) + end + it 'returns issues reacted by the authenticated user by the given emoji' do issue2 = create(:issue, project: project, author: user, assignees: [user]) award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star') diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 07d19e3ad29..e4e0ca285e0 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -143,6 +143,23 @@ describe API::MergeRequests do expect_response_ordered_exactly(merge_request3) end + it 'returns an array of merge requests with no assignee' do + merge_request3 = create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + + get api('/merge_requests', user), assignee_id: 'None', scope: :all + + expect_response_ordered_exactly(merge_request3) + end + + it 'returns an array of merge requests with any assignee' do + # This MR with no assignee should not be returned + create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + + get api('/merge_requests', user), assignee_id: 'Any', scope: :all + + expect_response_contain_exactly(merge_request, merge_request2, merge_request_closed, merge_request_merged, merge_request_locked) + end + it 'returns an array of merge requests assigned to me' do merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 22e5a7c7174..62b6a3ce42e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -200,6 +200,24 @@ describe API::Projects do expect(json_response.first).to include 'statistics' end + it "does not include license by default" do + get api('/projects', user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first).not_to include('license', 'license_url') + end + + it "does not include license if requested" do + get api('/projects', user), license: true + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first).not_to include('license', 'license_url') + end + context 'when external issue tracker is enabled' do let!(:jira_service) { create(:jira_service, project: project) } @@ -994,6 +1012,26 @@ describe API::Projects do }) end + it "does not include license fields by default" do + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include('license', 'license_url') + end + + it 'includes license fields when requested' do + get api("/projects/#{project.id}", user), license: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response['license']).to eq({ + 'key' => project.repository.license.key, + 'name' => project.repository.license.name, + 'nickname' => project.repository.license.nickname, + 'html_url' => project.repository.license.url, + 'source_url' => project.repository.license.meta['source'] + }) + end + it "does not include statistics by default" do get api("/projects/#{project.id}", user) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 43ceb332cfb..909703a8d47 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -797,6 +797,24 @@ describe API::Runner, :clean_gitlab_redis_shared_state do it { expect(job).to be_runner_system_failure } end + + context 'when failure_reason is unrecognized value' do + before do + update_job(state: 'failed', failure_reason: 'what_is_this') + job.reload + end + + it { expect(job).to be_unknown_failure } + end + + context 'when failure_reason is job_execution_timeout' do + before do + update_job(state: 'failed', failure_reason: 'job_execution_timeout') + job.reload + end + + it { expect(job).to be_job_execution_timeout } + end end context 'when trace is given' do diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb index 6894c65d639..1b4d8b70aa6 100644 --- a/spec/serializers/environment_status_entity_spec.rb +++ b/spec/serializers/environment_status_entity_spec.rb @@ -9,7 +9,7 @@ describe EnvironmentStatusEntity do let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } - let(:environment_status) { EnvironmentStatus.new(environment, merge_request) } + let(:environment_status) { EnvironmentStatus.new(environment, merge_request, merge_request.diff_head_sha) } let(:entity) { described_class.new(environment_status, request: request) } subject { entity.as_json } @@ -26,6 +26,7 @@ describe EnvironmentStatusEntity do it { is_expected.to include(:deployed_at) } it { is_expected.to include(:deployed_at_formatted) } it { is_expected.to include(:changes) } + it { is_expected.to include(:status) } it { is_expected.not_to include(:stop_url) } it { is_expected.not_to include(:metrics_url) } diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index 5bf8aa7f23f..561421d5ac8 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -52,6 +52,40 @@ describe MergeRequestWidgetEntity do end end + describe 'merge_pipeline' do + it 'returns nil' do + expect(subject[:merge_pipeline]).to be_nil + end + + context 'when is merged' do + let(:resource) { create(:merged_merge_request, source_project: project, merge_commit_sha: project.commit.id) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.target_branch, sha: resource.merge_commit_sha) } + + before do + project.add_maintainer(user) + end + + it 'returns merge_pipeline' do + pipeline.reload + pipeline_payload = PipelineDetailsEntity + .represent(pipeline, request: request) + .as_json + + expect(subject[:merge_pipeline]).to eq(pipeline_payload) + end + + context 'when user cannot read pipelines on target project' do + before do + project.add_guest(user) + end + + it 'returns nil' do + expect(subject[:merge_pipeline]).to be_nil + end + end + end + end + describe 'metrics' do context 'when metrics record exists with merged data' do before do diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb new file mode 100644 index 00000000000..6baf7ac9deb --- /dev/null +++ b/spec/services/boards/visits/create_service_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Boards::Visits::CreateService do + describe '#execute' do + let(:user) { create(:user) } + + context 'when a project board' do + let(:project) { create(:project) } + let(:project_board) { create(:board, project: project) } + + subject(:service) { described_class.new(project_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute(project_board)).to eq nil + end + + it 'returns nil when database is read only' do + allow(Gitlab::Database).to receive(:read_only?) { true } + + expect(service.execute(project_board)).to eq nil + end + + it 'records the visit' do + expect(BoardProjectRecentVisit).to receive(:visited!).once + + service.execute(project_board) + end + end + + context 'when a group board' do + let(:group) { create(:group) } + let(:group_board) { create(:board, group: group) } + + subject(:service) { described_class.new(group_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute(group_board)).to eq nil + end + + it 'records the visit' do + expect(BoardGroupRecentVisit).to receive(:visited!).once + + service.execute(group_board) + end + end + end +end diff --git a/spec/services/boards/visits/latest_service_spec.rb b/spec/services/boards/visits/latest_service_spec.rb new file mode 100644 index 00000000000..e55d599e2cc --- /dev/null +++ b/spec/services/boards/visits/latest_service_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Boards::Visits::LatestService do + describe '#execute' do + let(:user) { create(:user) } + + context 'when a project board' do + let(:project) { create(:project) } + let(:project_board) { create(:board, project: project) } + + subject(:service) { described_class.new(project_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute).to eq nil + end + + it 'queries for most recent visit' do + expect(BoardProjectRecentVisit).to receive(:latest).once + + service.execute + end + end + + context 'when a group board' do + let(:group) { create(:group) } + let(:group_board) { create(:board, group: group) } + + subject(:service) { described_class.new(group_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute).to eq nil + end + + it 'queries for most recent visit' do + expect(BoardGroupRecentVisit).to receive(:latest).once + + service.execute + end + end + end +end diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb index 065d021db5e..b096f1fa4fb 100644 --- a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb @@ -16,7 +16,6 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do let(:kubeclient) do Gitlab::Kubernetes::KubeClient.new( api_url, - ['api', 'apis/rbac.authorization.k8s.io'], auth_options: { username: username, password: password } ) end diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb index c543de21d5b..2355827fa5a 100644 --- a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb @@ -11,7 +11,6 @@ describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do let(:kubeclient) do Gitlab::Kubernetes::KubeClient.new( api_url, - ['api', 'apis/rbac.authorization.k8s.io'], auth_options: { username: username, password: password } ) end diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb index 0d333d541c9..c76f6e6f77e 100644 --- a/spec/services/resource_events/merge_into_notes_service_spec.rb +++ b/spec/services/resource_events/merge_into_notes_service_spec.rb @@ -66,5 +66,14 @@ describe ResourceEvents::MergeIntoNotesService do expect(notes.count).to eq 1 expect(notes.first.discussion_id).to eq event.discussion_id end + + it "preloads the note author's status" do + event = create_event(created_at: time) + create(:user_status, user: event.user) + + notes = described_class.new(resource, user).execute + + expect(notes.first.author.association(:status)).to be_loaded + end end end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 71287f28171..71d72ff27e9 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -168,6 +168,8 @@ module TestEnv return end + FileUtils.mkdir_p("tmp/tests/second_storage") unless File.exist?("tmp/tests/second_storage") + spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s Bundler.with_original_env do raise "gitaly spawn failed" unless system(spawn_script) diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index cd6661f09a1..9176eb12b12 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -6,7 +6,7 @@ describe PostReceive do let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } let(:gl_repository) { "project-#{project.id}" } let(:key) { create(:key, user: project.owner) } - let(:key_id) { key.shell_id } + let!(:key_id) { key.shell_id } let(:project) do create(:project, :repository, auto_cancel_pending_pipelines: 'disabled') @@ -31,85 +31,108 @@ describe PostReceive do end describe "#process_project_changes" do - before do - allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + context 'empty changes' do + it "does not call any PushService but runs after project hooks" do + expect(GitPushService).not_to receive(:new) + expect(GitTagPushService).not_to receive(:new) + expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) } + + described_class.new.perform(gl_repository, key_id, "") + end end - context "branches" do - let(:changes) { "123456 789012 refs/heads/tést" } + context 'unidentified user' do + let!(:key_id) { "" } - it "calls GitTagPushService" do - expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) - expect_any_instance_of(GitTagPushService).not_to receive(:execute) - described_class.new.perform(gl_repository, key_id, base64_changes) + it 'returns false' do + expect(GitPushService).not_to receive(:new) + expect(GitTagPushService).not_to receive(:new) + + expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false end end - context "tags" do - let(:changes) { "123456 789012 refs/tags/tag" } + context 'with changes' do + before do + allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + end + + context "branches" do + let(:changes) { "123456 789012 refs/heads/tést" } - it "calls GitTagPushService" do - expect_any_instance_of(GitPushService).not_to receive(:execute) - expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) - described_class.new.perform(gl_repository, key_id, base64_changes) + it "calls GitPushService" do + expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + described_class.new.perform(gl_repository, key_id, base64_changes) + end end - end - context "merge-requests" do - let(:changes) { "123456 789012 refs/merge-requests/123" } + context "tags" do + let(:changes) { "123456 789012 refs/tags/tag" } - it "does not call any of the services" do - expect_any_instance_of(GitPushService).not_to receive(:execute) - expect_any_instance_of(GitTagPushService).not_to receive(:execute) - described_class.new.perform(gl_repository, key_id, base64_changes) + it "calls GitTagPushService" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) + described_class.new.perform(gl_repository, key_id, base64_changes) + end end - end - context "gitlab-ci.yml" do - let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" } + context "merge-requests" do + let(:changes) { "123456 789012 refs/merge-requests/123" } - subject { described_class.new.perform(gl_repository, key_id, base64_changes) } + it "does not call any of the services" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + described_class.new.perform(gl_repository, key_id, base64_changes) + end + end - context "creates a Ci::Pipeline for every change" do - before do - stub_ci_pipeline_to_return_yaml_file + context "gitlab-ci.yml" do + let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" } - allow_any_instance_of(Project) - .to receive(:commit) - .and_return(project.commit) + subject { described_class.new.perform(gl_repository, key_id, base64_changes) } - allow_any_instance_of(Repository) - .to receive(:branch_exists?) - .and_return(true) - end + context "creates a Ci::Pipeline for every change" do + before do + stub_ci_pipeline_to_return_yaml_file - it { expect { subject }.to change { Ci::Pipeline.count }.by(2) } - end + allow_any_instance_of(Project) + .to receive(:commit) + .and_return(project.commit) - context "does not create a Ci::Pipeline" do - before do - stub_ci_pipeline_yaml_file(nil) + allow_any_instance_of(Repository) + .to receive(:branch_exists?) + .and_return(true) + end + + it { expect { subject }.to change { Ci::Pipeline.count }.by(2) } end - it { expect { subject }.not_to change { Ci::Pipeline.count } } + context "does not create a Ci::Pipeline" do + before do + stub_ci_pipeline_yaml_file(nil) + end + + it { expect { subject }.not_to change { Ci::Pipeline.count } } + end end - end - context 'after project changes hooks' do - let(:changes) { '123456 789012 refs/heads/tést' } - let(:fake_hook_data) { Hash.new(event_name: 'repository_update') } + context 'after project changes hooks' do + let(:changes) { '123456 789012 refs/heads/tést' } + let(:fake_hook_data) { Hash.new(event_name: 'repository_update') } - before do - allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) - # silence hooks so we can isolate - allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) - allow_any_instance_of(GitPushService).to receive(:execute).and_return(true) - end + before do + allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) + # silence hooks so we can isolate + allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) + allow_any_instance_of(GitPushService).to receive(:execute).and_return(true) + end - it 'calls SystemHooksService' do - expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) + it 'calls SystemHooksService' do + expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - described_class.new.perform(gl_repository, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) + end end end end diff --git a/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml index c432be72163..02ec3e2d9fe 100644 --- a/vendor/prometheus/values.yaml +++ b/vendor/prometheus/values.yaml @@ -1,5 +1,7 @@ alertmanager: enabled: false + image: + tag: v0.15.2 kubeStateMetrics: enabled: true @@ -16,7 +18,7 @@ rbac: server: fullnameOverride: "prometheus-prometheus-server" image: - tag: v2.1.0 + tag: v2.4.3 serverFiles: alerts: {} diff --git a/yarn.lock b/yarn.lock index 544bd4a05bd..5da401c1d43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -616,11 +616,16 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.32.0": +"@gitlab-org/gitlab-svgs@^1.23.0": version "1.32.0" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.32.0.tgz#a65ab7724fa7d55be8e5cc9b2dbe3f0757432fd3" integrity sha512-L3o8dFUd2nSkVZBwh2hCJWzNzADJ3dTBZxamND8NLosZK9/ohNhccmsQOZGyMCUHaOzm4vifaaXkAXh04UtMKA== +"@gitlab-org/gitlab-svgs@^1.33.0": + version "1.33.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.33.0.tgz#068566e8ee00795f6f09f58236f08e1716f9f04a" + integrity sha512-8ajtUHk6gQ1xosL/CO5IzHSFM/t18hx5pfzQ3cd0VuQXcyR6QKGuXTLwbYdmJDYOw1Etoo5DqDWxPEClHyZpiA== + "@gitlab-org/gitlab-ui@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.8.0.tgz#dee33d78f68c91644273dbd51734b796108263ee" |