summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml104
-rw-r--r--.ruby-version2
-rw-r--r--Gemfile8
-rw-r--r--Gemfile.lock12
-rw-r--r--Gemfile.rails5.lock12
-rw-r--r--app/assets/javascripts/activities.js10
-rw-r--r--app/assets/javascripts/breadcrumb.js15
-rw-r--r--app/assets/javascripts/build_artifacts.js18
-rw-r--r--app/assets/javascripts/ci_variable_list/ajax_variable_list.js56
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js37
-rw-r--r--app/assets/javascripts/ci_variable_list/native_form_variable_list.js5
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js8
-rw-r--r--app/assets/javascripts/comment_type_toggle.js56
-rw-r--r--app/assets/javascripts/commit/image_file.js320
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js10
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue122
-rw-r--r--app/assets/javascripts/commit_merge_requests.js7
-rw-r--r--app/assets/javascripts/commits.js38
-rw-r--r--app/assets/javascripts/commons/bootstrap.js12
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js24
-rw-r--r--app/assets/javascripts/contextual_sidebar.js7
-rw-r--r--app/assets/javascripts/create_item_dropdown.js13
-rw-r--r--app/assets/javascripts/create_label.js63
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue6
-rw-r--r--app/assets/javascripts/deploy_keys/service/index.js9
-rw-r--r--app/assets/javascripts/diff.js14
-rw-r--r--app/assets/javascripts/diffs/components/app.vue18
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue4
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue116
-rw-r--r--app/assets/javascripts/diffs/store/actions.js24
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js94
-rw-r--r--app/assets/javascripts/dropzone_input.js41
-rw-r--r--app/assets/javascripts/due_date_select.js3
-rw-r--r--app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js55
-rw-r--r--app/assets/javascripts/experimental_flags.js2
-rw-r--r--app/assets/javascripts/files_comment_button.js12
-rw-r--r--app/assets/javascripts/filterable_list.js25
-rw-r--r--app/assets/javascripts/flash.js42
-rw-r--r--app/assets/javascripts/fly_out_nav.js52
-rw-r--r--app/assets/javascripts/gl_field_error.js3
-rw-r--r--app/assets/javascripts/gl_field_errors.js14
-rw-r--r--app/assets/javascripts/gl_form.js19
-rw-r--r--app/assets/javascripts/group_avatar.js5
-rw-r--r--app/assets/javascripts/group_label_subscription.js10
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue72
-rw-r--r--app/assets/javascripts/groups/components/item_stats_value.vue86
-rw-r--r--app/assets/javascripts/groups/new_group_child.js22
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js23
-rw-r--r--app/assets/javascripts/groups/transfer_dropdown.js2
-rw-r--r--app/assets/javascripts/groups_select.js9
-rw-r--r--app/assets/javascripts/helpers/avatar_helper.js4
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js6
-rw-r--r--app/assets/javascripts/image_diff/init_discussion_tab.js3
-rw-r--r--app/assets/javascripts/image_diff/replaced_image_diff.js15
-rw-r--r--app/assets/javascripts/importer_status.js122
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js2
-rw-r--r--app/assets/javascripts/init_notes.js8
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js5
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js5
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js10
-rw-r--r--app/assets/javascripts/issuable_form.js34
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue6
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue11
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue11
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue1
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue16
-rw-r--r--app/assets/javascripts/jobs/store/getters.js11
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js2
-rw-r--r--app/assets/javascripts/jobs/store/state.js4
-rw-r--r--app/assets/javascripts/lib/utils/datefix.js28
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js118
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js63
-rw-r--r--app/assets/javascripts/member_expiration_date.js2
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue5
-rw-r--r--app/assets/javascripts/notes/stores/getters.js4
-rw-r--r--app/assets/javascripts/notes/stores/utils.js12
-rw-r--r--app/assets/javascripts/pager.js11
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js2
-rw-r--r--app/assets/javascripts/pages/admin/admin.js15
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/account_and_limits.js8
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js36
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue65
-rw-r--r--app/assets/javascripts/pages/admin/projects/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue139
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue187
-rw-r--r--app/assets/javascripts/pages/admin/users/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/users/new/index.js6
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js15
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue169
-rw-r--r--app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js16
-rw-r--r--app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js16
-rw-r--r--app/assets/javascripts/pages/profiles/index.js7
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/branches/new/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js22
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js91
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js221
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js64
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js3
-rw-r--r--app/assets/javascripts/pages/projects/init_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue124
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/network/network.js10
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js22
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue102
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue46
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/charts/index.js59
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/index/index.js66
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/init_pipelines.js7
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue114
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue36
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/constants.js6
-rw-r--r--app/assets/javascripts/pages/projects/shared/project_avatar.js5
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js3
-rw-r--r--app/assets/javascripts/pages/search/show/search.js25
-rw-r--r--app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js10
-rw-r--r--app/assets/javascripts/pages/users/index.js6
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue192
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_countdown.vue49
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue2
-rw-r--r--app/assets/stylesheets/framework/common.scss10
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss1
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/pages/diff.scss14
-rw-r--r--app/controllers/projects/mirrors_controller.rb16
-rw-r--r--app/helpers/groups_helper.rb9
-rw-r--r--app/models/clusters/platforms/kubernetes.rb6
-rw-r--r--app/models/project_services/kubernetes_service.rb6
-rw-r--r--app/models/ssh_host_key.rb130
-rw-r--r--app/serializers/build_details_entity.rb1
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb7
-rw-r--r--app/views/instance_statistics/conversational_development_index/_disabled.html.haml2
-rw-r--r--app/views/layouts/_flash.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml14
-rw-r--r--app/views/projects/tree/_tree_commit_column.html.haml2
-rw-r--r--app/views/shared/notifications/_button.html.haml4
-rwxr-xr-xbin/secpick14
-rwxr-xr-xbin/web5
-rwxr-xr-xbin/web_puma63
-rw-r--r--changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml5
-rw-r--r--changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml5
-rw-r--r--changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml5
-rw-r--r--changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml5
-rw-r--r--changelogs/unreleased/53013-duplicate-escape.yml5
-rw-r--r--changelogs/unreleased/53023-endless-scroll-loader-is-visible-on-user-profile-overview-page.yml4
-rw-r--r--changelogs/unreleased/53055-combine-date-util-functions.yml5
-rw-r--r--changelogs/unreleased/53070-fix-enable-usage-ping-link.yml5
-rw-r--r--changelogs/unreleased/53133-jobs-list.yml5
-rw-r--r--changelogs/unreleased/an-multithreading.yml5
-rw-r--r--changelogs/unreleased/gt-add-transparent-background-to-markdown-header-tabs.yml5
-rw-r--r--changelogs/unreleased/jramsay-42673-commit-tooltip.yml5
-rw-r--r--changelogs/unreleased/mr-file-list.yml5
-rw-r--r--config/initializers/7_prometheus_metrics.rb22
-rw-r--r--config/initializers/8_metrics.rb4
-rw-r--r--config/initializers/active_record_lifecycle.rb23
-rw-r--r--config/initializers/macos.rb13
-rw-r--r--config/initializers/rbtrace.rb9
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/puma.example.development.rb77
-rw-r--r--config/routes/project.rb1
-rw-r--r--config/unicorn.rb.example37
-rw-r--r--config/unicorn.rb.example.development67
-rw-r--r--danger/specs/Dangerfile6
-rw-r--r--doc/administration/pages/index.md27
-rw-r--r--doc/administration/pages/source.md38
-rw-r--r--doc/ci/runners/README.md2
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/feature_flags.md5
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/topics/autodevops/index.md115
-rw-r--r--doc/update/11.3-to-11.4.md6
-rw-r--r--doc/update/11.4-to-11.5.md404
-rw-r--r--doc/user/project/repository/branches/index.md9
-rw-r--r--lib/gitlab/ci/templates/Android.gitlab-ci.yml62
-rw-r--r--lib/gitlab/cluster/lifecycle_events.rb99
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_initializer.rb32
-rw-r--r--lib/gitlab/ee_compat_check.rb10
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb63
-rw-r--r--lib/tasks/gitlab/ldap.rake2
-rw-r--r--lib/tasks/haml-lint.rake11
-rw-r--r--locale/gitlab.pot6
-rw-r--r--package.json2
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/factory/README.md369
-rw-r--r--qa/qa/factory/base.rb79
-rw-r--r--qa/qa/factory/dependency.rb28
-rw-r--r--qa/qa/factory/product.rb33
-rw-r--r--qa/qa/factory/repository/project_push.rb11
-rw-r--r--qa/qa/factory/repository/wiki_push.rb10
-rw-r--r--qa/qa/factory/resource/branch.rb10
-rw-r--r--qa/qa/factory/resource/deploy_key.rb22
-rw-r--r--qa/qa/factory/resource/deploy_token.rb20
-rw-r--r--qa/qa/factory/resource/file.rb8
-rw-r--r--qa/qa/factory/resource/fork.rb25
-rw-r--r--qa/qa/factory/resource/group.rb8
-rw-r--r--qa/qa/factory/resource/issue.rb17
-rw-r--r--qa/qa/factory/resource/kubernetes_cluster.rb19
-rw-r--r--qa/qa/factory/resource/label.rb16
-rw-r--r--qa/qa/factory/resource/merge_request.rb42
-rw-r--r--qa/qa/factory/resource/merge_request_from_fork.rb21
-rw-r--r--qa/qa/factory/resource/personal_access_token.rb8
-rw-r--r--qa/qa/factory/resource/project.rb29
-rw-r--r--qa/qa/factory/resource/project_imported_from_github.rb8
-rw-r--r--qa/qa/factory/resource/project_milestone.rb15
-rw-r--r--qa/qa/factory/resource/runner.rb10
-rw-r--r--qa/qa/factory/resource/sandbox.rb8
-rw-r--r--qa/qa/factory/resource/secret_variable.rb10
-rw-r--r--qa/qa/factory/resource/ssh_key.rb12
-rw-r--r--qa/qa/factory/resource/user.rb11
-rw-r--r--qa/qa/factory/resource/wiki.rb8
-rw-r--r--qa/qa/factory/settings/hashed_storage.rb8
-rw-r--r--qa/qa/git/repository.rb8
-rw-r--r--qa/qa/runtime/logger.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb4
-rw-r--r--qa/spec/factory/base_spec.rb143
-rw-r--r--qa/spec/factory/dependency_spec.rb79
-rw-r--r--qa/spec/factory/product_spec.rb66
-rw-r--r--qa/spec/page/logging_spec.rb6
-rw-r--r--qa/spec/runtime/logger_spec.rb6
-rwxr-xr-xscripts/review_apps/review-apps.sh37
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb63
-rw-r--r--spec/features/projects/jobs_spec.rb61
-rw-r--r--spec/fixtures/api/schemas/job/job_details.json3
-rw-r--r--spec/javascripts/diffs/components/tree_list_spec.js72
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js9
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js70
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js22
-rw-r--r--spec/javascripts/flash_spec.js4
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js150
-rw-r--r--spec/javascripts/jobs/components/sidebar_spec.js5
-rw-r--r--spec/javascripts/jobs/store/getters_spec.js30
-rw-r--r--spec/javascripts/jobs/store/mutations_spec.js4
-rw-r--r--spec/javascripts/lib/utils/datefix_spec.js27
-rw-r--r--spec/javascripts/lib/utils/datetime_utility_spec.js (renamed from spec/javascripts/datetime_utility_spec.js)178
-rw-r--r--spec/javascripts/pretty_time_spec.js135
-rw-r--r--spec/javascripts/vue_shared/components/file_row_spec.js36
-rw-r--r--spec/javascripts/vue_shared/components/gl_countdown_spec.js77
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb113
-rw-r--r--spec/models/ssh_host_key_spec.rb164
-rw-r--r--spec/rack_servers/configs/config.ru12
-rw-r--r--spec/rack_servers/configs/puma.rb32
-rw-r--r--spec/rack_servers/puma_spec.rb84
-rw-r--r--spec/rack_servers/unicorn_spec.rb (renamed from spec/unicorn/unicorn_spec.rb)33
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb1
-rw-r--r--spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb1
-rw-r--r--spec/support/helpers/test_env.rb2
-rw-r--r--yarn.lock7
260 files changed, 5380 insertions, 3100 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/.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/Gemfile b/Gemfile
index 64d87baf697..d8c996cf6c4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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)
diff --git a/bin/web b/bin/web
index ecd0bbd10b0..06ff7c39296 100755
--- a/bin/web
+++ b/bin/web
@@ -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/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/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/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/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/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/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/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/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/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/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/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..4b2a6ccc7e4 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -2031,3 +2031,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/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/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/autodevops/index.md b/doc/topics/autodevops/index.md
index c60d25eda1b..4d4832184e2 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,37 @@ 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. Check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
+1. Go to your project's **Settings > CI/CD > Auto DevOps**.
+1. Check the **Default to Auto DevOps pipeline** checkbox.
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
+ 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.
-Once saved, an Auto DevOps pipeline will be triggered on the default branch.
+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,16 +242,16 @@ 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)`.
+### Disable Auto DevOps at the project level
+
+1. Go to your project's **Settings > CI/CD > Auto DevOps**.
+1. Uncheck the **Default to Auto DevOps pipeline** checkbox.
+1. Click **Save changes** for the changes to take effect.
+
### Deployment strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0.
@@ -299,8 +319,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 +332,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 +351,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 +367,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 +380,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 +405,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 +425,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 +443,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/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/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/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/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/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..26270595c6a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3599,6 +3599,9 @@ msgstr ""
msgid "List available repositories"
msgstr ""
+msgid "List view"
+msgstr ""
+
msgid "List your Bitbucket Server repositories"
msgstr ""
@@ -6519,6 +6522,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",
diff --git a/qa/qa.rb b/qa/qa.rb
index 36a37dbb270..f760dd972a7 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -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
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/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/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/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/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/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('&lt;img src=x onerror=alert(document.domain)&gt; - 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/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/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_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/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/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/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/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/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"