summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2018-10-08 10:40:10 +0100
committerFilipa Lacerda <filipa@gitlab.com>2018-10-08 10:40:10 +0100
commitfa875ba7a9441df6827ef1d6b05405c66ee0c579 (patch)
tree23d0cf911c9bf6a73fec9bb1f3de1bf61bedeacd /app
parentecefe090460687a078e3d1aacf621fd5bff07fb5 (diff)
parent838c1076694d24d180e19625d663749c8b5c1a1c (diff)
downloadgitlab-ce-fa875ba7a9441df6827ef1d6b05405c66ee0c579.tar.gz
Merge branch 'master' into 42611-removed-branch-link
* master: (1252 commits) Render log artifact files in GitLab Check disabled_services when finding a service Fix invalid parent path on group settings page Backport CE changes for: [Frontend only] Batch comments on merge requests Add button to insert table in markdown editor Update GITALY_SERVER_VERSION Updates Laravel.gitlab-ci.yml template Update operations metrics empty state Fix LFS uploaded images not being rendered Prepare admin/projects/show view to allow EE specific feature Add timed incremental rollout to Auto DevOps Update spec comment to point to correct issue Fix documentation for variables Document Security and Licence Management features permissions Fix time dependent jobs spec Use a CTE to remove the query timeout Backport changes from gitlab-ee!7538 Fix CE to EE merge (backport) Add changelog entry Refactor Feature.flipper method ...
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/auth_buttons/auth0_64.pngbin0 -> 1815 bytes
-rw-r--r--app/assets/images/auth_buttons/azure_64.pngbin695 -> 199 bytes
-rw-r--r--app/assets/images/auth_buttons/bitbucket_64.pngbin2161 -> 1299 bytes
-rw-r--r--app/assets/images/auth_buttons/google_64.pngbin4366 -> 1625 bytes
-rw-r--r--app/assets/images/auth_buttons/jwt_64.pngbin0 -> 2457 bytes
-rw-r--r--app/assets/images/auth_buttons/shibboleth_64.pngbin0 -> 2993 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_scheduled.icobin0 -> 5430 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_scheduled.pngbin0 -> 1072 bytes
-rw-r--r--app/assets/images/cluster_app_logos/elasticsearch.pngbin0 -> 796 bytes
-rw-r--r--app/assets/images/cluster_app_logos/gitlab.pngbin0 -> 1757 bytes
-rw-r--r--app/assets/images/cluster_app_logos/helm.pngbin0 -> 1438 bytes
-rw-r--r--app/assets/images/cluster_app_logos/jeager.pngbin0 -> 2619 bytes
-rw-r--r--app/assets/images/cluster_app_logos/jupyterhub.pngbin0 -> 895 bytes
-rw-r--r--app/assets/images/cluster_app_logos/kubernetes.pngbin0 -> 1437 bytes
-rw-r--r--app/assets/images/cluster_app_logos/meltano.pngbin0 -> 580 bytes
-rw-r--r--app/assets/images/cluster_app_logos/prometheus.pngbin0 -> 923 bytes
-rw-r--r--app/assets/javascripts/api.js55
-rw-r--r--app/assets/javascripts/awards_handler.js32
-rw-r--r--app/assets/javascripts/badges/components/badge.vue6
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue109
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue8
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue14
-rw-r--r--app/assets/javascripts/behaviors/index.js6
-rw-r--r--app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js19
-rw-r--r--app/assets/javascripts/behaviors/markdown/highlight_current_user.js17
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js4
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js (renamed from app/assets/javascripts/preview_markdown.js)0
-rw-r--r--app/assets/javascripts/behaviors/shortcuts.js35
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js (renamed from app/assets/javascripts/shortcuts.js)6
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js (renamed from app/assets/javascripts/shortcuts_blob.js)2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js (renamed from app/assets/javascripts/shortcuts_find_file.js)0
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js (renamed from app/assets/javascripts/shortcuts_issuable.js)4
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js (renamed from app/assets/javascripts/shortcuts_navigation.js)2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js (renamed from app/assets/javascripts/shortcuts_network.js)0
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js (renamed from app/assets/javascripts/shortcuts_wiki.js)2
-rw-r--r--app/assets/javascripts/blob/3d_viewer/index.js4
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js42
-rw-r--r--app/assets/javascripts/blob/template_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js4
-rw-r--r--app/assets/javascripts/blob/template_selectors/dockerfile_selector.js4
-rw-r--r--app/assets/javascripts/blob/template_selectors/gitignore_selector.js4
-rw-r--r--app/assets/javascripts/blob/template_selectors/license_selector.js4
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js3
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js24
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue10
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue18
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue9
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue18
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue8
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue6
-rw-r--r--app/assets/javascripts/boards/index.js17
-rw-r--r--app/assets/javascripts/boards/models/list.js7
-rw-r--r--app/assets/javascripts/boards/utils/query_data.js21
-rw-r--r--app/assets/javascripts/build_variables.js10
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js45
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js5
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue137
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue439
-rw-r--r--app/assets/javascripts/clusters/components/gcp_signup_offer.js27
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue4
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js17
-rw-r--r--app/assets/javascripts/commons/polyfills.js2
-rw-r--r--app/assets/javascripts/commons/polyfills/element.js23
-rw-r--r--app/assets/javascripts/commons/polyfills/svg.js5
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue6
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue6
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue10
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js22
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js9
-rw-r--r--app/assets/javascripts/diffs/components/app.vue90
-rw-r--r--app/assets/javascripts/diffs/components/changed_files.vue171
-rw-r--r--app/assets/javascripts/diffs/components/changed_files_dropdown.vue126
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue129
-rw-r--r--app/assets/javascripts/diffs/components/commit_widget.vue40
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue128
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue10
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue10
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue60
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue14
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue8
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue39
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue46
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue37
-rw-r--r--app/assets/javascripts/diffs/components/file_row_stats.vue30
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue18
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue13
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue25
-rw-r--r--app/assets/javascripts/diffs/components/no_changes.vue2
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue54
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue124
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue43
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue101
-rw-r--r--app/assets/javascripts/diffs/constants.js3
-rw-r--r--app/assets/javascripts/diffs/mixins/changed_files.js38
-rw-r--r--app/assets/javascripts/diffs/store/actions.js118
-rw-r--r--app/assets/javascripts/diffs/store/getters.js59
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js9
-rw-r--r--app/assets/javascripts/diffs/store/modules/index.js4
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js146
-rw-r--r--app/assets/javascripts/diffs/store/utils.js138
-rw-r--r--app/assets/javascripts/dispatcher.js89
-rw-r--r--app/assets/javascripts/dropzone_input.js25
-rw-r--r--app/assets/javascripts/environments/components/container.vue6
-rw-r--r--app/assets/javascripts/environments/components/empty_state.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue4
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue6
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue9
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue4
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue2
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue8
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js2
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js2
-rw-r--r--app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js21
-rw-r--r--app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js8
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js28
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js44
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js142
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js54
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js77
-rw-r--r--app/assets/javascripts/filtered_search/null_dropdown.js9
-rw-r--r--app/assets/javascripts/fly_out_nav.js4
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue6
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js40
-rw-r--r--app/assets/javascripts/gl_form.js2
-rw-r--r--app/assets/javascripts/groups/components/app.vue84
-rw-r--r--app/assets/javascripts/groups/components/group_folder.vue7
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue16
-rw-r--r--app/assets/javascripts/groups/components/groups.vue91
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue11
-rw-r--r--app/assets/javascripts/groups/constants.js24
-rw-r--r--app/assets/javascripts/groups/groups_filterable_list.js64
-rw-r--r--app/assets/javascripts/groups/index.js31
-rw-r--r--app/assets/javascripts/header.js55
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue8
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue79
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue77
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue25
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue61
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue11
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue6
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue2
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue14
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue104
-rw-r--r--app/assets/javascripts/ide/components/file_templates/bar.vue80
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue123
-rw-r--r--app/assets/javascripts/ide/components/ide.vue21
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue18
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue6
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue6
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue6
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue47
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue139
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue6
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue6
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue6
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue14
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue227
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue42
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue2
-rw-r--r--app/assets/javascripts/ide/constants.js8
-rw-r--r--app/assets/javascripts/ide/index.js38
-rw-r--r--app/assets/javascripts/ide/stores/actions.js26
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js31
-rw-r--r--app/assets/javascripts/ide/stores/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/mutations.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/actions.js47
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/mutations.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pane/actions.js30
-rw-r--r--app/assets/javascripts/ide/stores/modules/pane/getters.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/pane/index.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/pane/mutation_types.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/pane/mutations.js19
-rw-r--r--app/assets/javascripts/ide/stores/modules/pane/state.js5
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js23
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js3
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js8
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js6
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue1
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue19
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue3
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue5
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue5
-rw-r--r--app/assets/javascripts/issue_show/index.js7
-rw-r--r--app/assets/javascripts/job.js27
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue68
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue72
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue8
-rw-r--r--app/assets/javascripts/jobs/components/environments_block.vue68
-rw-r--r--app/assets/javascripts/jobs/components/erased_block.vue49
-rw-r--r--app/assets/javascripts/jobs/components/header.vue97
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue114
-rw-r--r--app/assets/javascripts/jobs/components/job_log.vue4
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue76
-rw-r--r--app/assets/javascripts/jobs/components/jobs_container.vue30
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue297
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue241
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue116
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue6
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue28
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js60
-rw-r--r--app/assets/javascripts/jobs/job_details_mediator.js67
-rw-r--r--app/assets/javascripts/jobs/services/job_service.js11
-rw-r--r--app/assets/javascripts/jobs/store/actions.js187
-rw-r--r--app/assets/javascripts/jobs/store/getters.js53
-rw-r--r--app/assets/javascripts/jobs/store/index.js15
-rw-r--r--app/assets/javascripts/jobs/store/mutation_types.js29
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js95
-rw-r--r--app/assets/javascripts/jobs/store/state.js40
-rw-r--r--app/assets/javascripts/jobs/stores/job_store.js11
-rw-r--r--app/assets/javascripts/labels_select.js4
-rw-r--r--app/assets/javascripts/lazy_loader.js107
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js110
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js21
-rw-r--r--app/assets/javascripts/lib/utils/logoutput_behaviours.js17
-rw-r--r--app/assets/javascripts/lib/utils/navigation_utility.js (renamed from app/assets/javascripts/shortcuts_dashboard_navigation.js)2
-rw-r--r--app/assets/javascripts/lib/utils/scroll_utils.js24
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js30
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js10
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js4
-rw-r--r--app/assets/javascripts/main.js16
-rw-r--r--app/assets/javascripts/merge_request_tabs.js19
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue39
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue65
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue4
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js15
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js127
-rw-r--r--app/assets/javascripts/mr_notes/stores/index.js17
-rw-r--r--app/assets/javascripts/notebook/index.vue4
-rw-r--r--app/assets/javascripts/notes.js67
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue15
-rw-r--r--app/assets/javascripts/notes/components/diff_file_header.vue94
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue50
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue47
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue24
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue29
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue11
-rw-r--r--app/assets/javascripts/notes/stores/actions.js87
-rw-r--r--app/assets/javascripts/notes/stores/getters.js21
-rw-r--r--app/assets/javascripts/notes/stores/index.js12
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js4
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js40
-rw-r--r--app/assets/javascripts/notes/stores/utils.js21
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js8
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js62
-rw-r--r--app/assets/javascripts/pages/admin/runners/index.js10
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue5
-rw-r--r--app/assets/javascripts/pages/constants.js1
-rw-r--r--app/assets/javascripts/pages/dashboard/groups/index/index.js4
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js12
-rw-r--r--app/assets/javascripts/pages/groups/boards/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js7
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/show/group_tabs.js136
-rw-r--r--app/assets/javascripts/pages/groups/show/index.js14
-rw-r--r--app/assets/javascripts/pages/ide/index.js10
-rw-r--r--app/assets/javascripts/pages/instance_statistics/cohorts/index.js3
-rw-r--r--app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js13
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/activity/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/browse/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/file/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/boards/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commits/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/find_file/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js2
-rw-r--r--app/assets/javascripts/pages/projects/network/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue2
-rw-r--r--app/assets/javascripts/pages/projects/project.js64
-rw-r--r--app/assets/javascripts/pages/projects/settings/badges/index/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue52
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/tree/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue69
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js14
-rw-r--r--app/assets/javascripts/pages/root/index.js5
-rw-r--r--app/assets/javascripts/pages/snippets/form.js1
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js26
-rw-r--r--app/assets/javascripts/pages/users/user_overview_block.js42
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js84
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue4
-rw-r--r--app/assets/javascripts/performance_bar/components/simple_metric.vue37
-rw-r--r--app/assets/javascripts/persistent_user_callout.js34
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue42
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue16
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue6
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue2
-rw-r--r--app/assets/javascripts/projects/project_import_gitlab_project.js16
-rw-r--r--app/assets/javascripts/projects/project_new.js29
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue6
-rw-r--r--app/assets/javascripts/read_more.js41
-rw-r--r--app/assets/javascripts/registry/components/app.vue6
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue14
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue2
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue4
-rw-r--r--app/assets/javascripts/reports/components/report_issues.vue4
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue2
-rw-r--r--app/assets/javascripts/reports/components/summary_row.vue4
-rw-r--r--app/assets/javascripts/reports/store/mutations.js1
-rw-r--r--app/assets/javascripts/search_autocomplete.js6
-rw-r--r--app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js21
-rw-r--r--app/assets/javascripts/set_status_modal/event_hub.js3
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue33
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue241
-rw-r--r--app/assets/javascripts/shared/milestones/form.js1
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue42
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue4
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js10
-rw-r--r--app/assets/javascripts/usage_ping_consent.js30
-rw-r--r--app/assets/javascripts/users_select.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue60
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue)2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/bar_chart.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue (renamed from app/assets/javascripts/ide/components/changed_file_icon.vue)30
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue237
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_icon.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_links.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue8
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js2
-rw-r--r--app/assets/stylesheets/application.scss2
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss1
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/avatar.scss3
-rw-r--r--app/assets/stylesheets/framework/awards.scss4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss24
-rw-r--r--app/assets/stylesheets/framework/calendar.scss12
-rw-r--r--app/assets/stylesheets/framework/common.scss127
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss10
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss12
-rw-r--r--app/assets/stylesheets/framework/emojis.scss10
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/filters.scss13
-rw-r--r--app/assets/stylesheets/framework/gfm.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss36
-rw-r--r--app/assets/stylesheets/framework/icons.scss1
-rw-r--r--app/assets/stylesheets/framework/jquery.scss15
-rw-r--r--app/assets/stylesheets/framework/layout.scss19
-rw-r--r--app/assets/stylesheets/framework/lists.scss1
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss66
-rw-r--r--app/assets/stylesheets/framework/mobile.scss13
-rw-r--r--app/assets/stylesheets/framework/modal.scss13
-rw-r--r--app/assets/stylesheets/framework/read_more.scss13
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss2
-rw-r--r--app/assets/stylesheets/framework/selects.scss4
-rw-r--r--app/assets/stylesheets/framework/snippets.scss2
-rw-r--r--app/assets/stylesheets/framework/toggle.scss8
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss99
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss1
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/notify.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss204
-rw-r--r--app/assets/stylesheets/page_bundles/xterm.scss (renamed from app/assets/stylesheets/pages/xterm.scss)2
-rw-r--r--app/assets/stylesheets/pages/admin.scss4
-rw-r--r--app/assets/stylesheets/pages/boards.scss12
-rw-r--r--app/assets/stylesheets/pages/branches.scss4
-rw-r--r--app/assets/stylesheets/pages/builds.scss23
-rw-r--r--app/assets/stylesheets/pages/clusters.scss57
-rw-r--r--app/assets/stylesheets/pages/commits.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss72
-rw-r--r--app/assets/stylesheets/pages/environments.scss4
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/graph.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss60
-rw-r--r--app/assets/stylesheets/pages/help.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss13
-rw-r--r--app/assets/stylesheets/pages/login.scss29
-rw-r--r--app/assets/stylesheets/pages/members.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss15
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss78
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss1
-rw-r--r--app/assets/stylesheets/pages/profile.scss14
-rw-r--r--app/assets/stylesheets/pages/projects.scss247
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/settings.scss4
-rw-r--r--app/assets/stylesheets/pages/status.scss1
-rw-r--r--app/assets/stylesheets/pages/todos.scss4
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss2
-rw-r--r--app/assets/stylesheets/performance_bar.scss9
-rw-r--r--app/controllers/abuse_reports_controller.rb4
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb4
-rw-r--r--app/controllers/admin/appearances_controller.rb2
-rw-r--r--app/controllers/admin/application_controller.rb2
-rw-r--r--app/controllers/admin/application_settings_controller.rb62
-rw-r--r--app/controllers/admin/applications_controller.rb6
-rw-r--r--app/controllers/admin/background_jobs_controller.rb2
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb4
-rw-r--r--app/controllers/admin/dashboard_controller.rb4
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb2
-rw-r--r--app/controllers/admin/gitaly_servers_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb4
-rw-r--r--app/controllers/admin/health_check_controller.rb2
-rw-r--r--app/controllers/admin/hook_logs_controller.rb2
-rw-r--r--app/controllers/admin/hooks_controller.rb2
-rw-r--r--app/controllers/admin/identities_controller.rb4
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb6
-rw-r--r--app/controllers/admin/impersonations_controller.rb2
-rw-r--r--app/controllers/admin/jobs_controller.rb4
-rw-r--r--app/controllers/admin/keys_controller.rb4
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/admin/logs_controller.rb5
-rw-r--r--app/controllers/admin/projects_controller.rb6
-rw-r--r--app/controllers/admin/requests_profiles_controller.rb2
-rw-r--r--app/controllers/admin/runner_projects_controller.rb2
-rw-r--r--app/controllers/admin/runners_controller.rb13
-rw-r--r--app/controllers/admin/services_controller.rb4
-rw-r--r--app/controllers/admin/spam_logs_controller.rb4
-rw-r--r--app/controllers/admin/system_info_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb4
-rw-r--r--app/controllers/application_controller.rb46
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/boards/application_controller.rb2
-rw-r--r--app/controllers/boards/issues_controller.rb4
-rw-r--r--app/controllers/boards/lists_controller.rb2
-rw-r--r--app/controllers/ci/lints_controller.rb2
-rw-r--r--app/controllers/concerns/accepts_pending_invitations.rb2
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb4
-rw-r--r--app/controllers/concerns/boards_responses.rb2
-rw-r--r--app/controllers/concerns/checks_collaboration.rb2
-rw-r--r--app/controllers/concerns/continue_params.rb2
-rw-r--r--app/controllers/concerns/controller_with_cross_project_access_check.rb2
-rw-r--r--app/controllers/concerns/creates_commit.rb6
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb2
-rw-r--r--app/controllers/concerns/diff_for_path.rb2
-rw-r--r--app/controllers/concerns/enforces_two_factor_authentication.rb4
-rw-r--r--app/controllers/concerns/group_tree.rb6
-rw-r--r--app/controllers/concerns/hooks_execution.rb2
-rw-r--r--app/controllers/concerns/internal_redirect.rb8
-rw-r--r--app/controllers/concerns/invalid_utf8_error_handler.rb27
-rw-r--r--app/controllers/concerns/issuable_actions.rb5
-rw-r--r--app/controllers/concerns/issuable_collections.rb18
-rw-r--r--app/controllers/concerns/issues_action.rb2
-rw-r--r--app/controllers/concerns/issues_calendar.rb4
-rw-r--r--app/controllers/concerns/labels_as_hash.rb28
-rw-r--r--app/controllers/concerns/lfs_request.rb2
-rw-r--r--app/controllers/concerns/members_presentation.rb4
-rw-r--r--app/controllers/concerns/membership_actions.rb4
-rw-r--r--app/controllers/concerns/merge_requests_action.rb2
-rw-r--r--app/controllers/concerns/milestone_actions.rb2
-rw-r--r--app/controllers/concerns/notes_actions.rb51
-rw-r--r--app/controllers/concerns/oauth_applications.rb2
-rw-r--r--app/controllers/concerns/params_backward_compatibility.rb2
-rw-r--r--app/controllers/concerns/preview_markdown.rb2
-rw-r--r--app/controllers/concerns/renders_blob.rb2
-rw-r--r--app/controllers/concerns/renders_commits.rb2
-rw-r--r--app/controllers/concerns/renders_member_access.rb4
-rw-r--r--app/controllers/concerns/renders_notes.rb6
-rw-r--r--app/controllers/concerns/repository_settings_redirect.rb2
-rw-r--r--app/controllers/concerns/requires_whitelisted_monitoring_client.rb2
-rw-r--r--app/controllers/concerns/routable_actions.rb2
-rw-r--r--app/controllers/concerns/send_file_upload.rb18
-rw-r--r--app/controllers/concerns/service_params.rb2
-rw-r--r--app/controllers/concerns/snippets_actions.rb2
-rw-r--r--app/controllers/concerns/spammable_actions.rb2
-rw-r--r--app/controllers/concerns/todos_actions.rb2
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb2
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb2
-rw-r--r--app/controllers/concerns/uploads_actions.rb6
-rw-r--r--app/controllers/concerns/with_performance_bar.rb8
-rw-r--r--app/controllers/concerns/workhorse_request.rb2
-rw-r--r--app/controllers/confirmations_controller.rb4
-rw-r--r--app/controllers/dashboard/application_controller.rb2
-rw-r--r--app/controllers/dashboard/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/labels_controller.rb2
-rw-r--r--app/controllers/dashboard/milestones_controller.rb4
-rw-r--r--app/controllers/dashboard/projects_controller.rb6
-rw-r--r--app/controllers/dashboard/snippets_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb4
-rw-r--r--app/controllers/dashboard_controller.rb4
-rw-r--r--app/controllers/explore/application_controller.rb2
-rw-r--r--app/controllers/explore/groups_controller.rb2
-rw-r--r--app/controllers/explore/projects_controller.rb6
-rw-r--r--app/controllers/explore/snippets_controller.rb2
-rw-r--r--app/controllers/google_api/authorizations_controller.rb2
-rw-r--r--app/controllers/graphql_controller.rb2
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups/avatars_controller.rb2
-rw-r--r--app/controllers/groups/boards_controller.rb2
-rw-r--r--app/controllers/groups/children_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/groups/labels_controller.rb16
-rw-r--r--app/controllers/groups/milestones_controller.rb2
-rw-r--r--app/controllers/groups/runners_controller.rb2
-rw-r--r--app/controllers/groups/settings/badges_controller.rb13
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb9
-rw-r--r--app/controllers/groups/shared_projects_controller.rb2
-rw-r--r--app/controllers/groups/uploads_controller.rb2
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb18
-rw-r--r--app/controllers/health_check_controller.rb2
-rw-r--r--app/controllers/health_controller.rb2
-rw-r--r--app/controllers/help_controller.rb2
-rw-r--r--app/controllers/ide_controller.rb2
-rw-r--r--app/controllers/import/base_controller.rb6
-rw-r--r--app/controllers/import/bitbucket_controller.rb4
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb2
-rw-r--r--app/controllers/import/fogbugz_controller.rb4
-rw-r--r--app/controllers/import/gitea_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb6
-rw-r--r--app/controllers/import/gitlab_controller.rb4
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb10
-rw-r--r--app/controllers/import/google_code_controller.rb4
-rw-r--r--app/controllers/import/manifest_controller.rb8
-rw-r--r--app/controllers/instance_statistics/cohorts_controller.rb6
-rw-r--r--app/controllers/instance_statistics/conversational_development_index_controller.rb2
-rw-r--r--app/controllers/invites_controller.rb8
-rw-r--r--app/controllers/jwt_controller.rb2
-rw-r--r--app/controllers/koding_controller.rb2
-rw-r--r--app/controllers/ldap/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/metrics_controller.rb2
-rw-r--r--app/controllers/notification_settings_controller.rb2
-rw-r--r--app/controllers/oauth/applications_controller.rb4
-rw-r--r--app/controllers/oauth/authorizations_controller.rb2
-rw-r--r--app/controllers/oauth/authorized_applications_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb9
-rw-r--r--app/controllers/passwords_controller.rb4
-rw-r--r--app/controllers/profiles/accounts_controller.rb4
-rw-r--r--app/controllers/profiles/active_sessions_controller.rb2
-rw-r--r--app/controllers/profiles/application_controller.rb2
-rw-r--r--app/controllers/profiles/avatars_controller.rb2
-rw-r--r--app/controllers/profiles/chat_names_controller.rb2
-rw-r--r--app/controllers/profiles/emails_controller.rb2
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb2
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/profiles/notifications_controller.rb4
-rw-r--r--app/controllers/profiles/passwords_controller.rb2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb4
-rw-r--r--app/controllers/profiles/preferences_controller.rb2
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb4
-rw-r--r--app/controllers/profiles/u2f_registrations_controller.rb2
-rw-r--r--app/controllers/profiles_controller.rb6
-rw-r--r--app/controllers/projects/application_controller.rb5
-rw-r--r--app/controllers/projects/artifacts_controller.rb10
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb6
-rw-r--r--app/controllers/projects/avatars_controller.rb2
-rw-r--r--app/controllers/projects/badges_controller.rb2
-rw-r--r--app/controllers/projects/blame_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb6
-rw-r--r--app/controllers/projects/boards_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb4
-rw-r--r--app/controllers/projects/build_artifacts_controller.rb6
-rw-r--r--app/controllers/projects/builds_controller.rb2
-rw-r--r--app/controllers/projects/ci/lints_controller.rb2
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb4
-rw-r--r--app/controllers/projects/clusters_controller.rb8
-rw-r--r--app/controllers/projects/commit_controller.rb6
-rw-r--r--app/controllers/projects/commits_controller.rb4
-rw-r--r--app/controllers/projects/compare_controller.rb4
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb2
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb2
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb4
-rw-r--r--app/controllers/projects/deploy_tokens_controller.rb2
-rw-r--r--app/controllers/projects/deployments_controller.rb6
-rw-r--r--app/controllers/projects/discussions_controller.rb4
-rw-r--r--app/controllers/projects/environments_controller.rb6
-rw-r--r--app/controllers/projects/find_file_controller.rb2
-rw-r--r--app/controllers/projects/forks_controller.rb6
-rw-r--r--app/controllers/projects/git_http_client_controller.rb2
-rw-r--r--app/controllers/projects/git_http_controller.rb2
-rw-r--r--app/controllers/projects/graphs_controller.rb2
-rw-r--r--app/controllers/projects/group_links_controller.rb2
-rw-r--r--app/controllers/projects/hook_logs_controller.rb2
-rw-r--r--app/controllers/projects/hooks_controller.rb3
-rw-r--r--app/controllers/projects/imports_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb6
-rw-r--r--app/controllers/projects/jobs_controller.rb13
-rw-r--r--app/controllers/projects/labels_controller.rb19
-rw-r--r--app/controllers/projects/lfs_api_controller.rb4
-rw-r--r--app/controllers/projects/lfs_locks_api_controller.rb2
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb6
-rw-r--r--app/controllers/projects/mattermosts_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests/conflicts_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb8
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb16
-rw-r--r--app/controllers/projects/merge_requests_controller.rb11
-rw-r--r--app/controllers/projects/milestones_controller.rb11
-rw-r--r--app/controllers/projects/mirrors_controller.rb2
-rw-r--r--app/controllers/projects/network_controller.rb2
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/controllers/projects/pages_controller.rb4
-rw-r--r--app/controllers/projects/pages_domains_controller.rb4
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb4
-rw-r--r--app/controllers/projects/pipelines_controller.rb6
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb4
-rw-r--r--app/controllers/projects/prometheus/metrics_controller.rb2
-rw-r--r--app/controllers/projects/protected_branches_controller.rb2
-rw-r--r--app/controllers/projects/protected_refs_controller.rb2
-rw-r--r--app/controllers/projects/protected_tags_controller.rb2
-rw-r--r--app/controllers/projects/raw_controller.rb2
-rw-r--r--app/controllers/projects/refs_controller.rb67
-rw-r--r--app/controllers/projects/registry/application_controller.rb2
-rw-r--r--app/controllers/projects/registry/repositories_controller.rb18
-rw-r--r--app/controllers/projects/registry/tags_controller.rb2
-rw-r--r--app/controllers/projects/releases_controller.rb4
-rw-r--r--app/controllers/projects/repositories_controller.rb2
-rw-r--r--app/controllers/projects/runner_projects_controller.rb2
-rw-r--r--app/controllers/projects/runners_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb2
-rw-r--r--app/controllers/projects/settings/badges_controller.rb13
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb9
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb2
-rw-r--r--app/controllers/projects/settings/repository_controller.rb4
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb11
-rw-r--r--app/controllers/projects/templates_controller.rb2
-rw-r--r--app/controllers/projects/todos_controller.rb2
-rw-r--r--app/controllers/projects/tree_controller.rb2
-rw-r--r--app/controllers/projects/triggers_controller.rb2
-rw-r--r--app/controllers/projects/uploads_controller.rb2
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb28
-rw-r--r--app/controllers/registrations_controller.rb2
-rw-r--r--app/controllers/root_controller.rb2
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/controllers/sent_notifications_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb4
-rw-r--r--app/controllers/sherlock/application_controller.rb2
-rw-r--r--app/controllers/sherlock/file_samples_controller.rb2
-rw-r--r--app/controllers/sherlock/queries_controller.rb2
-rw-r--r--app/controllers/sherlock/transactions_controller.rb2
-rw-r--r--app/controllers/snippets/notes_controller.rb4
-rw-r--r--app/controllers/snippets_controller.rb6
-rw-r--r--app/controllers/uploads_controller.rb2
-rw-r--r--app/controllers/user_callouts_controller.rb4
-rw-r--r--app/controllers/users/terms_controller.rb2
-rw-r--r--app/controllers/users_controller.rb15
-rw-r--r--app/finders/access_requests_finder.rb2
-rw-r--r--app/finders/admin/projects_finder.rb8
-rw-r--r--app/finders/admin/runners_finder.rb62
-rw-r--r--app/finders/autocomplete/users_finder.rb4
-rw-r--r--app/finders/branches_finder.rb2
-rw-r--r--app/finders/clusters_finder.rb2
-rw-r--r--app/finders/concerns/created_at_filter.rb2
-rw-r--r--app/finders/concerns/custom_attributes_filter.rb4
-rw-r--r--app/finders/concerns/finder_methods.rb6
-rw-r--r--app/finders/concerns/finder_with_cross_project_access.rb4
-rw-r--r--app/finders/contributed_projects_finder.rb4
-rw-r--r--app/finders/environments_finder.rb4
-rw-r--r--app/finders/events_finder.rb15
-rw-r--r--app/finders/fork_projects_finder.rb4
-rw-r--r--app/finders/group_descendants_finder.rb24
-rw-r--r--app/finders/group_finder.rb4
-rw-r--r--app/finders/group_labels_finder.rb29
-rw-r--r--app/finders/group_members_finder.rb4
-rw-r--r--app/finders/group_projects_finder.rb4
-rw-r--r--app/finders/groups_finder.rb10
-rw-r--r--app/finders/issuable_finder.rb59
-rw-r--r--app/finders/issues_finder.rb18
-rw-r--r--app/finders/joined_groups_finder.rb21
-rw-r--r--app/finders/labels_finder.rb37
-rw-r--r--app/finders/license_template_finder.rb42
-rw-r--r--app/finders/members_finder.rb8
-rw-r--r--app/finders/merge_request_target_project_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb27
-rw-r--r--app/finders/milestones_finder.rb8
-rw-r--r--app/finders/notes_finder.rb14
-rw-r--r--app/finders/personal_access_tokens_finder.rb4
-rw-r--r--app/finders/personal_projects_finder.rb4
-rw-r--r--app/finders/pipeline_schedules_finder.rb4
-rw-r--r--app/finders/pipelines_finder.rb22
-rw-r--r--app/finders/projects_finder.rb17
-rw-r--r--app/finders/runner_jobs_finder.rb4
-rw-r--r--app/finders/snippets_finder.rb16
-rw-r--r--app/finders/tags_finder.rb2
-rw-r--r--app/finders/template_finder.rb42
-rw-r--r--app/finders/todos_finder.rb27
-rw-r--r--app/finders/union_finder.rb10
-rw-r--r--app/finders/user_finder.rb2
-rw-r--r--app/finders/user_recent_events_finder.rb27
-rw-r--r--app/finders/users_finder.rb8
-rw-r--r--app/graphql/functions/base_function.rb2
-rw-r--r--app/graphql/functions/echo.rb2
-rw-r--r--app/graphql/gitlab_schema.rb2
-rw-r--r--app/graphql/mutations/concerns/mutations/resolves_project.rb2
-rw-r--r--app/graphql/mutations/merge_requests/base.rb2
-rw-r--r--app/graphql/resolvers/base_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/resolves_pipelines.rb2
-rw-r--r--app/graphql/resolvers/full_path_resolver.rb2
-rw-r--r--app/graphql/resolvers/merge_request_pipelines_resolver.rb2
-rw-r--r--app/graphql/resolvers/merge_request_resolver.rb4
-rw-r--r--app/graphql/resolvers/project_pipelines_resolver.rb2
-rw-r--r--app/graphql/resolvers/project_resolver.rb2
-rw-r--r--app/graphql/types/base_enum.rb2
-rw-r--r--app/graphql/types/base_field.rb2
-rw-r--r--app/graphql/types/base_input_object.rb2
-rw-r--r--app/graphql/types/base_interface.rb2
-rw-r--r--app/graphql/types/base_object.rb2
-rw-r--r--app/graphql/types/base_scalar.rb2
-rw-r--r--app/graphql/types/base_union.rb2
-rw-r--r--app/graphql/types/ci/pipeline_status_enum.rb2
-rw-r--r--app/graphql/types/ci/pipeline_type.rb2
-rw-r--r--app/graphql/types/merge_request_type.rb2
-rw-r--r--app/graphql/types/permission_types/base_permission_type.rb2
-rw-r--r--app/graphql/types/permission_types/ci/pipeline.rb2
-rw-r--r--app/graphql/types/permission_types/merge_request.rb2
-rw-r--r--app/graphql/types/permission_types/project.rb4
-rw-r--r--app/graphql/types/project_type.rb2
-rw-r--r--app/graphql/types/query_type.rb2
-rw-r--r--app/graphql/types/time_type.rb2
-rw-r--r--app/helpers/accounts_helper.rb2
-rw-r--r--app/helpers/active_sessions_helper.rb2
-rw-r--r--app/helpers/appearances_helper.rb2
-rw-r--r--app/helpers/application_helper.rb32
-rw-r--r--app/helpers/application_settings_helper.rb23
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/auto_devops_helper.rb4
-rw-r--r--app/helpers/avatars_helper.rb29
-rw-r--r--app/helpers/award_emoji_helper.rb2
-rw-r--r--app/helpers/blame_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb40
-rw-r--r--app/helpers/boards_helper.rb6
-rw-r--r--app/helpers/branches_helper.rb2
-rw-r--r--app/helpers/breadcrumbs_helper.rb2
-rw-r--r--app/helpers/broadcast_messages_helper.rb7
-rw-r--r--app/helpers/builds_helper.rb10
-rw-r--r--app/helpers/button_helper.rb13
-rw-r--r--app/helpers/calendar_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb13
-rw-r--r--app/helpers/clusters_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/compare_helper.rb2
-rw-r--r--app/helpers/components_helper.rb2
-rw-r--r--app/helpers/conversational_development_index_helper.rb2
-rw-r--r--app/helpers/cookies_helper.rb9
-rw-r--r--app/helpers/count_helper.rb2
-rw-r--r--app/helpers/dashboard_helper.rb25
-rw-r--r--app/helpers/defer_script_tag_helper.rb2
-rw-r--r--app/helpers/deploy_tokens_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb13
-rw-r--r--app/helpers/dropdowns_helper.rb11
-rw-r--r--app/helpers/emails_helper.rb6
-rw-r--r--app/helpers/emoji_helper.rb2
-rw-r--r--app/helpers/environment_helper.rb4
-rw-r--r--app/helpers/environments_helper.rb2
-rw-r--r--app/helpers/events_helper.rb20
-rw-r--r--app/helpers/explore_helper.rb2
-rw-r--r--app/helpers/external_wiki_helper.rb2
-rw-r--r--app/helpers/favicon_helper.rb2
-rw-r--r--app/helpers/form_helper.rb2
-rw-r--r--app/helpers/git_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb2
-rw-r--r--app/helpers/graph_helper.rb6
-rw-r--r--app/helpers/groups_helper.rb25
-rw-r--r--app/helpers/hooks_helper.rb2
-rw-r--r--app/helpers/icons_helper.rb22
-rw-r--r--app/helpers/import_helper.rb2
-rw-r--r--app/helpers/instance_configuration_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb28
-rw-r--r--app/helpers/issues_helper.rb22
-rw-r--r--app/helpers/javascript_helper.rb2
-rw-r--r--app/helpers/kerberos_spnego_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb22
-rw-r--r--app/helpers/lazy_image_tag_helper.rb8
-rw-r--r--app/helpers/markup_helper.rb41
-rw-r--r--app/helpers/mattermost_helper.rb2
-rw-r--r--app/helpers/members_helper.rb16
-rw-r--r--app/helpers/merge_requests_helper.rb16
-rw-r--r--app/helpers/milestones_helper.rb20
-rw-r--r--app/helpers/milestones_routing_helper.rb2
-rw-r--r--app/helpers/mirror_helper.rb2
-rw-r--r--app/helpers/namespaces_helper.rb8
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/helpers/notes_helper.rb6
-rw-r--r--app/helpers/notifications_helper.rb2
-rw-r--r--app/helpers/numbers_helper.rb4
-rw-r--r--app/helpers/page_layout_helper.rb14
-rw-r--r--app/helpers/pagination_helper.rb2
-rw-r--r--app/helpers/performance_bar_helper.rb2
-rw-r--r--app/helpers/pipeline_schedules_helper.rb2
-rw-r--r--app/helpers/preferences_helper.rb2
-rw-r--r--app/helpers/profiles_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb41
-rw-r--r--app/helpers/repository_languages_helper.rb5
-rw-r--r--app/helpers/rss_helper.rb2
-rw-r--r--app/helpers/runners_helper.rb2
-rw-r--r--app/helpers/safe_params_helper.rb4
-rw-r--r--app/helpers/search_helper.rb6
-rw-r--r--app/helpers/selects_helper.rb28
-rw-r--r--app/helpers/sentry_helper.rb2
-rw-r--r--app/helpers/services_helper.rb4
-rw-r--r--app/helpers/sidekiq_helper.rb2
-rw-r--r--app/helpers/snippets_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb35
-rw-r--r--app/helpers/storage_health_helper.rb2
-rw-r--r--app/helpers/storage_helper.rb2
-rw-r--r--app/helpers/submodule_helper.rb2
-rw-r--r--app/helpers/system_note_helper.rb5
-rw-r--r--app/helpers/tab_helper.rb22
-rw-r--r--app/helpers/tags_helper.rb7
-rw-r--r--app/helpers/time_helper.rb16
-rw-r--r--app/helpers/todos_helper.rb11
-rw-r--r--app/helpers/tree_helper.rb10
-rw-r--r--app/helpers/triggers_helper.rb2
-rw-r--r--app/helpers/user_callouts_helper.rb7
-rw-r--r--app/helpers/users_helper.rb4
-rw-r--r--app/helpers/version_check_helper.rb23
-rw-r--r--app/helpers/visibility_level_helper.rb12
-rw-r--r--app/helpers/webpack_helper.rb2
-rw-r--r--app/helpers/wiki_helper.rb8
-rw-r--r--app/helpers/workhorse_helper.rb2
-rw-r--r--app/mailers/emails/auto_devops.rb24
-rw-r--r--app/mailers/emails/issues.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb14
-rw-r--r--app/mailers/emails/profile.rb4
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/mailers/previews/notify_preview.rb10
-rw-r--r--app/mailers/repository_check_mailer.rb2
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/application_setting.rb37
-rw-r--r--app/models/badge.rb2
-rw-r--r--app/models/blob_viewer/gitlab_ci_yml.rb8
-rw-r--r--app/models/blob_viewer/package_json.rb3
-rw-r--r--app/models/ci/artifact_blob.rb2
-rw-r--r--app/models/ci/build.rb147
-rw-r--r--app/models/ci/job_artifact.rb67
-rw-r--r--app/models/ci/pipeline.rb52
-rw-r--r--app/models/ci/pipeline_variable.rb4
-rw-r--r--app/models/ci/runner.rb62
-rw-r--r--app/models/ci/stage.rb5
-rw-r--r--app/models/ci/trigger_request.rb2
-rw-r--r--app/models/clusters/applications/helm.rb3
-rw-r--r--app/models/clusters/applications/ingress.rb1
-rw-r--r--app/models/clusters/applications/jupyter.rb6
-rw-r--r--app/models/clusters/applications/prometheus.rb3
-rw-r--r--app/models/clusters/applications/runner.rb1
-rw-r--r--app/models/clusters/cluster.rb1
-rw-r--r--app/models/clusters/concerns/application_status.rb24
-rw-r--r--app/models/clusters/platforms/kubernetes.rb27
-rw-r--r--app/models/commit.rb7
-rw-r--r--app/models/commit_status.rb15
-rw-r--r--app/models/concerns/avatarable.rb2
-rw-r--r--app/models/concerns/bulk_member_access_load.rb6
-rw-r--r--app/models/concerns/cacheable_attributes.rb6
-rw-r--r--app/models/concerns/case_sensitivity.rb45
-rw-r--r--app/models/concerns/diff_positionable_note.rb81
-rw-r--r--app/models/concerns/from_union.rb51
-rw-r--r--app/models/concerns/has_status.rb19
-rw-r--r--app/models/concerns/issuable.rb7
-rw-r--r--app/models/concerns/mentionable.rb13
-rw-r--r--app/models/concerns/mentionable/reference_regexes.rb12
-rw-r--r--app/models/concerns/noteable.rb19
-rw-r--r--app/models/concerns/project_services_loggable.rb28
-rw-r--r--app/models/concerns/protected_branch_access.rb11
-rw-r--r--app/models/concerns/protected_ref.rb10
-rw-r--r--app/models/concerns/protected_ref_access.rb18
-rw-r--r--app/models/concerns/protected_tag_access.rb3
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb10
-rw-r--r--app/models/concerns/subscribable.rb8
-rw-r--r--app/models/concerns/triggerable_hooks.rb7
-rw-r--r--app/models/container_repository.rb4
-rw-r--r--app/models/dashboard_group_milestone.rb6
-rw-r--r--app/models/deploy_key.rb2
-rw-r--r--app/models/diff_note.rb70
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/epic.rb4
-rw-r--r--app/models/event.rb21
-rw-r--r--app/models/global_milestone.rb6
-rw-r--r--app/models/group.rb38
-rw-r--r--app/models/hooks/active_hook_filter.rb16
-rw-r--r--app/models/hooks/service_hook.rb2
-rw-r--r--app/models/hooks/web_hook.rb49
-rw-r--r--app/models/instance_configuration.rb4
-rw-r--r--app/models/issue.rb18
-rw-r--r--app/models/key.rb8
-rw-r--r--app/models/label.rb14
-rw-r--r--app/models/label_link.rb2
-rw-r--r--app/models/label_note.rb97
-rw-r--r--app/models/legacy_diff_note.rb6
-rw-r--r--app/models/license_template.rb10
-rw-r--r--app/models/member.rb10
-rw-r--r--app/models/members/project_member.rb2
-rw-r--r--app/models/merge_request.rb30
-rw-r--r--app/models/merge_request_diff.rb2
-rw-r--r--app/models/milestone.rb5
-rw-r--r--app/models/namespace.rb21
-rw-r--r--app/models/network/commit.rb2
-rw-r--r--app/models/network/graph.rb4
-rw-r--r--app/models/note.rb43
-rw-r--r--app/models/pages_domain.rb2
-rw-r--r--app/models/project.rb150
-rw-r--r--app/models/project_authorization.rb8
-rw-r--r--app/models/project_auto_devops.rb20
-rw-r--r--app/models/project_feature.rb51
-rw-r--r--app/models/project_import_state.rb2
-rw-r--r--app/models/project_services/asana_service.rb4
-rw-r--r--app/models/project_services/chat_message/merge_message.rb9
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/irker_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb9
-rw-r--r--app/models/project_services/kubernetes_service.rb26
-rw-r--r--app/models/project_services/slash_commands_service.rb4
-rw-r--r--app/models/project_wiki.rb9
-rw-r--r--app/models/prometheus_metric.rb89
-rw-r--r--app/models/protected_ref_matcher.rb56
-rw-r--r--app/models/ref_matcher.rb46
-rw-r--r--app/models/repository.rb43
-rw-r--r--app/models/resource_label_event.rb101
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/site_statistic.rb2
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/models/system_note_metadata.rb7
-rw-r--r--app/models/todo.rb1
-rw-r--r--app/models/user.rb141
-rw-r--r--app/models/user_callout.rb4
-rw-r--r--app/models/wiki_page.rb11
-rw-r--r--app/policies/application_setting/term_policy.rb2
-rw-r--r--app/policies/ci/runner_policy.rb2
-rw-r--r--app/policies/deploy_key_policy.rb2
-rw-r--r--app/policies/issuable_policy.rb1
-rw-r--r--app/policies/issue_policy.rb4
-rw-r--r--app/policies/project_policy.rb14
-rw-r--r--app/presenters/ci/build_presenter.rb4
-rw-r--r--app/presenters/commit_status_presenter.rb11
-rw-r--r--app/presenters/conversational_development_index/metric_presenter.rb2
-rw-r--r--app/presenters/merge_request_presenter.rb4
-rw-r--r--app/presenters/project_presenter.rb158
-rw-r--r--app/presenters/projects/settings/deploy_keys_presenter.rb8
-rw-r--r--app/serializers/build_action_entity.rb5
-rw-r--r--app/serializers/build_details_entity.rb61
-rw-r--r--app/serializers/commit_entity.rb40
-rw-r--r--app/serializers/detailed_status_entity.rb (renamed from app/serializers/status_entity.rb)16
-rw-r--r--app/serializers/diff_file_entity.rb10
-rw-r--r--app/serializers/diff_line_entity.rb14
-rw-r--r--app/serializers/diff_line_parallel_entity.rb6
-rw-r--r--app/serializers/diff_line_serializer.rb5
-rw-r--r--app/serializers/diff_viewer_entity.rb7
-rw-r--r--app/serializers/diffs_entity.rb13
-rw-r--r--app/serializers/discussion_entity.rb5
-rw-r--r--app/serializers/environment_serializer.rb2
-rw-r--r--app/serializers/group_child_entity.rb2
-rw-r--r--app/serializers/group_entity.rb2
-rw-r--r--app/serializers/job_entity.rb11
-rw-r--r--app/serializers/job_group_entity.rb2
-rw-r--r--app/serializers/merge_request_widget_entity.rb2
-rw-r--r--app/serializers/note_entity.rb10
-rw-r--r--app/serializers/pipeline_details_entity.rb1
-rw-r--r--app/serializers/pipeline_entity.rb2
-rw-r--r--app/serializers/pipeline_serializer.rb3
-rw-r--r--app/serializers/project_note_entity.rb4
-rw-r--r--app/serializers/runner_entity.rb7
-rw-r--r--app/serializers/stage_entity.rb18
-rw-r--r--app/serializers/trigger_variable_entity.rb7
-rw-r--r--app/services/application_settings/update_service.rb8
-rw-r--r--app/services/applications/create_service.rb2
-rw-r--r--app/services/boards/create_service.rb2
-rw-r--r--app/services/boards/issues/list_service.rb17
-rw-r--r--app/services/boards/issues/move_service.rb6
-rw-r--r--app/services/boards/lists/destroy_service.rb2
-rw-r--r--app/services/boards/lists/move_service.rb4
-rw-r--r--app/services/chat_names/find_user_service.rb2
-rw-r--r--app/services/ci/compare_test_reports_service.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb4
-rw-r--r--app/services/ci/enqueue_build_service.rb8
-rw-r--r--app/services/ci/ensure_stage_service.rb2
-rw-r--r--app/services/ci/extract_sections_from_build_trace_service.rb2
-rw-r--r--app/services/ci/fetch_kubernetes_token_service.rb75
-rw-r--r--app/services/ci/process_build_service.rb45
-rw-r--r--app/services/ci/process_pipeline_service.rb42
-rw-r--r--app/services/ci/register_job_service.rb12
-rw-r--r--app/services/ci/retry_build_service.rb2
-rw-r--r--app/services/ci/run_scheduled_build_service.rb13
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb6
-rw-r--r--app/services/clusters/applications/install_service.rb8
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb50
-rw-r--r--app/services/clusters/gcp/kubernetes.rb13
-rw-r--r--app/services/clusters/gcp/kubernetes/create_service_account_service.rb51
-rw-r--r--app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb30
-rw-r--r--app/services/clusters/gcp/provision_service.rb4
-rw-r--r--app/services/cohorts_service.rb2
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb2
-rw-r--r--app/services/create_release_service.rb2
-rw-r--r--app/services/delete_merged_branches_service.rb2
-rw-r--r--app/services/emails/base_service.rb2
-rw-r--r--app/services/emails/create_service.rb7
-rw-r--r--app/services/files/base_service.rb6
-rw-r--r--app/services/files/multi_service.rb2
-rw-r--r--app/services/git_push_service.rb2
-rw-r--r--app/services/groups/destroy_service.rb2
-rw-r--r--app/services/groups/transfer_service.rb4
-rw-r--r--app/services/import_export_clean_up_service.rb2
-rw-r--r--app/services/issuable/bulk_update_service.rb2
-rw-r--r--app/services/issuable/common_system_notes_service.rb9
-rw-r--r--app/services/issuable_base_service.rb17
-rw-r--r--app/services/issues/base_service.rb2
-rw-r--r--app/services/issues/move_service.rb17
-rw-r--r--app/services/issues/referenced_merge_requests_service.rb6
-rw-r--r--app/services/issues/related_branches_service.rb26
-rw-r--r--app/services/issues/reopen_service.rb2
-rw-r--r--app/services/issues/update_service.rb4
-rw-r--r--app/services/labels/find_or_create_service.rb2
-rw-r--r--app/services/labels/promote_service.rb23
-rw-r--r--app/services/labels/transfer_service.rb24
-rw-r--r--app/services/lfs/file_transformer.rb2
-rw-r--r--app/services/lfs/lock_file_service.rb2
-rw-r--r--app/services/lfs/locks_finder_service.rb2
-rw-r--r--app/services/lfs/unlock_file_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb8
-rw-r--r--app/services/merge_requests/build_service.rb20
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb7
-rw-r--r--app/services/merge_requests/create_service.rb2
-rw-r--r--app/services/merge_requests/delete_non_latest_diffs_service.rb2
-rw-r--r--app/services/merge_requests/refresh_service.rb64
-rw-r--r--app/services/merge_requests/reload_diffs_service.rb12
-rw-r--r--app/services/milestones/promote_service.rb8
-rw-r--r--app/services/milestones/update_service.rb2
-rw-r--r--app/services/notes/build_service.rb6
-rw-r--r--app/services/notification_recipient_service.rb18
-rw-r--r--app/services/notification_service.rb6
-rw-r--r--app/services/preview_markdown_service.rb6
-rw-r--r--app/services/projects/auto_devops/disable_service.rb41
-rw-r--r--app/services/projects/autocomplete_service.rb33
-rw-r--r--app/services/projects/base_move_relations_service.rb2
-rw-r--r--app/services/projects/batch_forks_count_service.rb2
-rw-r--r--app/services/projects/batch_open_issues_count_service.rb2
-rw-r--r--app/services/projects/container_repository/destroy_service.rb15
-rw-r--r--app/services/projects/create_service.rb6
-rw-r--r--app/services/projects/destroy_service.rb25
-rw-r--r--app/services/projects/detect_repository_languages_service.rb6
-rw-r--r--app/services/projects/enable_deploy_key_service.rb2
-rw-r--r--app/services/projects/forks_count_service.rb2
-rw-r--r--app/services/projects/gitlab_projects_import_service.rb2
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb4
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb2
-rw-r--r--app/services/projects/lfs_pointers/lfs_import_service.rb2
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb2
-rw-r--r--app/services/projects/move_deploy_keys_projects_service.rb2
-rw-r--r--app/services/projects/move_forks_service.rb6
-rw-r--r--app/services/projects/move_lfs_objects_projects_service.rb2
-rw-r--r--app/services/projects/move_notification_settings_service.rb2
-rw-r--r--app/services/projects/move_project_authorizations_service.rb2
-rw-r--r--app/services/projects/move_project_group_links_service.rb2
-rw-r--r--app/services/projects/move_project_members_service.rb2
-rw-r--r--app/services/projects/open_issues_count_service.rb4
-rw-r--r--app/services/projects/propagate_service_template.rb4
-rw-r--r--app/services/projects/transfer_service.rb2
-rw-r--r--app/services/projects/unlink_fork_service.rb2
-rw-r--r--app/services/projects/update_pages_configuration_service.rb8
-rw-r--r--app/services/projects/update_remote_mirror_service.rb4
-rw-r--r--app/services/projects/update_service.rb12
-rw-r--r--app/services/quick_actions/interpret_service.rb70
-rw-r--r--app/services/quick_actions/target_service.rb4
-rw-r--r--app/services/resource_events/change_labels_service.rb3
-rw-r--r--app/services/resource_events/merge_into_notes_service.rb57
-rw-r--r--app/services/search/group_service.rb2
-rw-r--r--app/services/search_service.rb4
-rw-r--r--app/services/spam_check_service.rb2
-rw-r--r--app/services/submit_usage_ping_service.rb1
-rw-r--r--app/services/system_note_service.rb59
-rw-r--r--app/services/tags/destroy_service.rb2
-rw-r--r--app/services/todo_service.rb10
-rw-r--r--app/services/todos/destroy/base_service.rb4
-rw-r--r--app/services/todos/destroy/confidential_issue_service.rb6
-rw-r--r--app/services/todos/destroy/entity_leave_service.rb18
-rw-r--r--app/services/todos/destroy/group_private_service.rb4
-rw-r--r--app/services/todos/destroy/private_features_service.rb4
-rw-r--r--app/services/todos/destroy/project_private_service.rb4
-rw-r--r--app/services/update_release_service.rb2
-rw-r--r--app/services/users/build_service.rb6
-rw-r--r--app/services/users/last_push_event_service.rb2
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb4
-rw-r--r--app/services/users/respond_to_terms_service.rb2
-rw-r--r--app/services/wikis/create_attachment_service.rb79
-rw-r--r--app/uploaders/avatar_uploader.rb4
-rw-r--r--app/uploaders/file_uploader.rb10
-rw-r--r--app/uploaders/gitlab_uploader.rb10
-rw-r--r--app/uploaders/job_artifact_uploader.rb19
-rw-r--r--app/uploaders/legacy_artifact_uploader.rb2
-rw-r--r--app/uploaders/lfs_object_uploader.rb2
-rw-r--r--app/uploaders/namespace_file_uploader.rb22
-rw-r--r--app/uploaders/records_uploads.rb6
-rw-r--r--app/uploaders/uploader_helper.rb27
-rw-r--r--app/validators/branch_filter_validator.rb37
-rw-r--r--app/validators/js_regex_validator.rb2
-rw-r--r--app/validators/url_validator.rb14
-rw-r--r--app/validators/variable_duplicates_validator.rb2
-rw-r--r--app/views/abuse_reports/new.html.haml2
-rw-r--r--app/views/admin/appearances/_form.html.haml8
-rw-r--r--app/views/admin/appearances/preview_sign_in.html.haml2
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml5
-rw-r--r--app/views/admin/application_settings/_background_jobs.html.haml27
-rw-r--r--app/views/admin/application_settings/_diff_limits.html.haml16
-rw-r--r--app/views/admin/application_settings/_influx.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml4
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml4
-rw-r--r--app/views/admin/application_settings/_signin.html.haml2
-rw-r--r--app/views/admin/application_settings/_usage.html.haml31
-rw-r--r--app/views/admin/application_settings/ci_cd.html.haml26
-rw-r--r--app/views/admin/application_settings/integrations.html.haml31
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml50
-rw-r--r--app/views/admin/application_settings/network.html.haml36
-rw-r--r--app/views/admin/application_settings/preferences.html.haml58
-rw-r--r--app/views/admin/application_settings/reporting.html.haml36
-rw-r--r--app/views/admin/application_settings/repository.html.haml36
-rw-r--r--app/views/admin/application_settings/show.html.haml309
-rw-r--r--app/views/admin/applications/_form.html.haml6
-rw-r--r--app/views/admin/applications/index.html.haml2
-rw-r--r--app/views/admin/applications/show.html.haml21
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml4
-rw-r--r--app/views/admin/dashboard/index.html.haml6
-rw-r--r--app/views/admin/deploy_keys/edit.html.haml2
-rw-r--r--app/views/admin/deploy_keys/index.html.haml2
-rw-r--r--app/views/admin/deploy_keys/new.html.haml2
-rw-r--r--app/views/admin/groups/_form.html.haml4
-rw-r--r--app/views/admin/groups/index.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/hooks/edit.html.haml2
-rw-r--r--app/views/admin/hooks/index.html.haml2
-rw-r--r--app/views/admin/identities/_form.html.haml2
-rw-r--r--app/views/admin/identities/index.html.haml2
-rw-r--r--app/views/admin/labels/_form.html.haml2
-rw-r--r--app/views/admin/labels/index.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml4
-rw-r--r--app/views/admin/runners/_runner.html.haml127
-rw-r--r--app/views/admin/runners/_sort_dropdown.html.haml11
-rw-r--r--app/views/admin/runners/index.html.haml171
-rw-r--r--app/views/admin/services/_form.html.haml2
-rw-r--r--app/views/admin/users/_form.html.haml4
-rw-r--r--app/views/admin/users/index.html.haml2
-rw-r--r--app/views/admin/users/projects.html.haml2
-rw-r--r--app/views/award_emoji/_awards_block.html.haml1
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml6
-rw-r--r--app/views/ci/runner/_how_to_setup_shared_runner.html.haml3
-rw-r--r--app/views/ci/runner/_how_to_setup_specific_runner.html.haml26
-rw-r--r--app/views/dashboard/_groups_head.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/dashboard/_snippets_head.html.haml2
-rw-r--r--app/views/dashboard/activity.html.haml3
-rw-r--r--app/views/dashboard/groups/index.html.haml3
-rw-r--r--app/views/dashboard/issues.atom.builder2
-rw-r--r--app/views/dashboard/issues.html.haml3
-rw-r--r--app/views/dashboard/merge_requests.html.haml3
-rw-r--r--app/views/dashboard/projects/index.html.haml3
-rw-r--r--app/views/dashboard/projects/starred.html.haml3
-rw-r--r--app/views/dashboard/snippets/index.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml3
-rw-r--r--app/views/devise/passwords/edit.html.haml6
-rw-r--r--app/views/devise/sessions/_new_base.html.haml10
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml2
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml6
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml27
-rw-r--r--app/views/devise/shared/_signup_box.html.haml17
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml6
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml4
-rw-r--r--app/views/discussions/_diff_discussion.html.haml3
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/discussions/_parallel_diff_discussion.html.haml12
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml2
-rw-r--r--app/views/doorkeeper/applications/index.html.haml5
-rw-r--r--app/views/doorkeeper/applications/show.html.haml19
-rw-r--r--app/views/doorkeeper/authorized_applications/_delete_form.html.haml1
-rw-r--r--app/views/errors/precondition_failed.html.haml8
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/events/_event_push.atom.haml2
-rw-r--r--app/views/events/_event_scope.html.haml2
-rw-r--r--app/views/events/event/_common.html.haml2
-rw-r--r--app/views/events/event/_created_project.html.haml4
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/events/event/_private.html.haml10
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml3
-rw-r--r--app/views/explore/projects/index.html.haml3
-rw-r--r--app/views/explore/projects/starred.html.haml3
-rw-r--r--app/views/explore/projects/trending.html.haml3
-rw-r--r--app/views/groups/_archived_projects.html.haml8
-rw-r--r--app/views/groups/_children.html.haml4
-rw-r--r--app/views/groups/_group_admin_settings.html.haml6
-rw-r--r--app/views/groups/_shared_projects.html.haml8
-rw-r--r--app/views/groups/_subgroups_and_projects.html.haml8
-rw-r--r--app/views/groups/edit.html.haml12
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml2
-rw-r--r--app/views/groups/issues.atom.builder2
-rw-r--r--app/views/groups/labels/index.html.haml33
-rw-r--r--app/views/groups/milestones/_form.html.haml4
-rw-r--r--app/views/groups/milestones/index.html.haml2
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/runners/_group_runners.html.haml4
-rw-r--r--app/views/groups/settings/_advanced.html.haml4
-rw-r--r--app/views/groups/settings/_permissions.html.haml2
-rw-r--r--app/views/groups/show.html.haml47
-rw-r--r--app/views/help/index.html.haml3
-rw-r--r--app/views/help/instance_configuration/_gitlab_pages.html.haml2
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/import/fogbugz/new.html.haml2
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml2
-rw-r--r--app/views/import/gitea/new.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml11
-rw-r--r--app/views/import/google_code/new.html.haml2
-rw-r--r--app/views/import/google_code/new_user_map.html.haml2
-rw-r--r--app/views/instance_statistics/cohorts/index.html.haml16
-rw-r--r--app/views/instance_statistics/conversational_development_index/_disabled.html.haml17
-rw-r--r--app/views/instance_statistics/conversational_development_index/index.html.haml7
-rw-r--r--app/views/issues/_issues_calendar.ics.ruby2
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/_search.html.haml4
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/explore.html.haml4
-rw-r--r--app/views/layouts/fullscreen.html.haml2
-rw-r--r--app/views/layouts/group_settings.html.haml4
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml7
-rw-r--r--app/views/layouts/header/_default.html.haml24
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml1
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml56
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml12
-rw-r--r--app/views/layouts/nav/sidebar/_instance_statistics.html.haml23
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml23
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml9
-rw-r--r--app/views/notify/_failed_builds.html.haml32
-rw-r--r--app/views/notify/autodevops_disabled_email.html.haml49
-rw-r--r--app/views/notify/autodevops_disabled_email.text.erb20
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/new_merge_request_email.text.erb2
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml35
-rw-r--r--app/views/profiles/emails/index.html.haml10
-rw-r--r--app/views/profiles/gpg_keys/_form.html.haml2
-rw-r--r--app/views/profiles/keys/_form.html.haml8
-rw-r--r--app/views/profiles/keys/_key_details.html.haml2
-rw-r--r--app/views/profiles/passwords/edit.html.haml2
-rw-r--r--app/views/profiles/passwords/new.html.haml4
-rw-r--r--app/views/profiles/preferences/show.html.haml2
-rw-r--r--app/views/profiles/show.html.haml77
-rw-r--r--app/views/profiles/two_factor_auths/_codes.html.haml6
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml6
-rw-r--r--app/views/projects/_commit_button.html.haml2
-rw-r--r--app/views/projects/_flash_messages.html.haml1
-rw-r--r--app/views/projects/_fork_suggestion.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml84
-rw-r--r--app/views/projects/_import_project_pane.html.haml26
-rw-r--r--app/views/projects/_md_preview.html.haml19
-rw-r--r--app/views/projects/_merge_request_merge_method_settings.html.haml1
-rw-r--r--app/views/projects/_new_project_fields.html.haml43
-rw-r--r--app/views/projects/_project_templates.html.haml2
-rw-r--r--app/views/projects/_readme.html.haml2
-rw-r--r--app/views/projects/_stat_anchor_list.html.haml2
-rw-r--r--app/views/projects/_wiki.html.haml2
-rw-r--r--app/views/projects/blob/_blob.html.haml1
-rw-r--r--app/views/projects/blob/_new_dir.html.haml2
-rw-r--r--app/views/projects/blob/_upload.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/preview.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_markup.html.haml6
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/branches/new.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml30
-rw-r--r--app/views/projects/buttons/_star.html.haml30
-rw-r--r--app/views/projects/ci/builds/_build.html.haml22
-rw-r--r--app/views/projects/clusters/_banner.html.haml23
-rw-r--r--app/views/projects/clusters/_gcp_signup_offer_banner.html.haml4
-rw-r--r--app/views/projects/clusters/_integration_form.html.haml20
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml9
-rw-r--r--app/views/projects/clusters/gcp/_show.html.haml8
-rw-r--r--app/views/projects/clusters/show.html.haml3
-rw-r--r--app/views/projects/clusters/user/_form.html.haml9
-rw-r--r--app/views/projects/clusters/user/_show.html.haml8
-rw-r--r--app/views/projects/commit/_change.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml6
-rw-r--r--app/views/projects/compare/_form.html.haml2
-rw-r--r--app/views/projects/default_branch/_show.html.haml21
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml2
-rw-r--r--app/views/projects/deploy_keys/edit.html.haml2
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml1
-rw-r--r--app/views/projects/diffs/_single_image_diff.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml4
-rw-r--r--app/views/projects/edit.html.haml27
-rw-r--r--app/views/projects/empty.html.haml20
-rw-r--r--app/views/projects/environments/_form.html.haml2
-rw-r--r--app/views/projects/environments/empty.html.haml18
-rw-r--r--app/views/projects/environments/show.html.haml9
-rw-r--r--app/views/projects/environments/terminal.html.haml2
-rw-r--r--app/views/projects/find_file/show.html.haml2
-rw-r--r--app/views/projects/forks/_fork_button.html.haml4
-rw-r--r--app/views/projects/forks/index.html.haml4
-rw-r--r--app/views/projects/hooks/_index.html.haml2
-rw-r--r--app/views/projects/hooks/edit.html.haml2
-rw-r--r--app/views/projects/imports/new.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml6
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml2
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/new.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml6
-rw-r--r--app/views/projects/jobs/_empty_state.html.haml18
-rw-r--r--app/views/projects/jobs/_empty_states.html.haml9
-rw-r--r--app/views/projects/jobs/_header.html.haml2
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml95
-rw-r--r--app/views/projects/jobs/index.html.haml2
-rw-r--r--app/views/projects/jobs/show.html.haml55
-rw-r--r--app/views/projects/labels/index.html.haml25
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml2
-rw-r--r--app/views/projects/merge_requests/_form.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml6
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/_nav_btns.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/new.html.haml3
-rw-r--r--app/views/projects/merge_requests/diffs/_commit_widget.html.haml4
-rw-r--r--app/views/projects/merge_requests/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/milestones/_form.html.haml4
-rw-r--r--app/views/projects/milestones/index.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-rw-r--r--app/views/projects/new.html.haml7
-rw-r--r--app/views/projects/notes/_actions.html.haml1
-rw-r--r--app/views/projects/pages/show.html.haml2
-rw-r--r--app/views/projects/pages_domains/edit.html.haml2
-rw-r--r--app/views/projects/pages_domains/new.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml2
-rw-r--r--app/views/projects/pipelines/new.html.haml2
-rw-r--r--app/views/projects/project_members/_new_project_group.html.haml (renamed from app/views/projects/project_members/_new_shared_group.html.haml)12
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/project_members/import.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml16
-rw-r--r--app/views/projects/project_templates/_built_in_templates.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_create_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_create_protected_tag.html.haml2
-rw-r--r--app/views/projects/releases/edit.html.haml2
-rw-r--r--app/views/projects/runners/_group_runners.html.haml2
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml30
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml2
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml10
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml12
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/settings/repository/show.html.haml1
-rw-r--r--app/views/projects/show.html.haml9
-rw-r--r--app/views/projects/snippets/_actions.html.haml2
-rw-r--r--app/views/projects/snippets/index.html.haml2
-rw-r--r--app/views/projects/tags/_tag.atom.builder19
-rw-r--r--app/views/projects/tags/index.atom.builder7
-rw-r--r--app/views/projects/tags/index.html.haml6
-rw-r--r--app/views/projects/tags/new.html.haml2
-rw-r--r--app/views/projects/tree/_tree_commit_column.html.haml2
-rw-r--r--app/views/projects/triggers/_form.html.haml2
-rw-r--r--app/views/projects/update.js.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml10
-rw-r--r--app/views/projects/wikis/_main_links.html.haml2
-rw-r--r--app/views/projects/wikis/_new.html.haml2
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml14
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml9
-rw-r--r--app/views/shared/_clone_panel.html.haml6
-rw-r--r--app/views/shared/_event_filter.html.haml12
-rw-r--r--app/views/shared/_label.html.haml2
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml1
-rw-r--r--app/views/shared/_mobile_clone_panel.html.haml13
-rw-r--r--app/views/shared/_new_project_item_select.html.haml4
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml2
-rw-r--r--app/views/shared/_ping_consent.html.haml12
-rw-r--r--app/views/shared/_recaptcha_form.html.haml2
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/_visibility_level.html.haml2
-rw-r--r--app/views/shared/_visibility_radios.html.haml2
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/shared/empty_states/_labels.html.haml2
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml2
-rw-r--r--app/views/shared/empty_states/_wikis.html.haml4
-rw-r--r--app/views/shared/groups/_empty_state.html.haml7
-rw-r--r--app/views/shared/groups/_search_form.html.haml4
-rw-r--r--app/views/shared/icons/_icon_status_scheduled.svg1
-rw-r--r--app/views/shared/icons/_icon_status_scheduled_borderless.svg1
-rw-r--r--app/views/shared/issuable/_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_board_create_list_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml22
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml7
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml3
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml33
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml3
-rw-r--r--app/views/shared/issuable/form/_title.html.haml2
-rw-r--r--app/views/shared/labels/_form.html.haml4
-rw-r--r--app/views/shared/labels/_nav.html.haml20
-rw-r--r--app/views/shared/labels/_sort_dropdown.html.haml9
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml4
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/notes/_comment_button.html.haml2
-rw-r--r--app/views/shared/notes/_edit_form.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/runners/_form.html.haml2
-rw-r--r--app/views/shared/runners/_runner_description.html.haml2
-rw-r--r--app/views/shared/snippets/_form.html.haml4
-rw-r--r--app/views/shared/web_hooks/_form.html.haml1
-rw-r--r--app/views/snippets/_actions.html.haml2
-rw-r--r--app/views/snippets/new.html.haml3
-rw-r--r--app/views/snippets/notes/_actions.html.haml1
-rw-r--r--app/views/u2f/_register.html.haml4
-rw-r--r--app/views/users/_overview.html.haml32
-rw-r--r--app/views/users/calendar_activities.html.haml42
-rw-r--r--app/views/users/show.html.haml38
-rw-r--r--app/workers/admin_email_worker.rb2
-rw-r--r--app/workers/all_queues.yml4
-rw-r--r--app/workers/archive_trace_worker.rb2
-rw-r--r--app/workers/authorized_projects_worker.rb2
-rw-r--r--app/workers/auto_devops/disable_worker.rb35
-rw-r--r--app/workers/build_coverage_worker.rb2
-rw-r--r--app/workers/build_finished_worker.rb2
-rw-r--r--app/workers/build_hooks_worker.rb2
-rw-r--r--app/workers/build_queue_worker.rb2
-rw-r--r--app/workers/build_success_worker.rb2
-rw-r--r--app/workers/build_trace_sections_worker.rb2
-rw-r--r--app/workers/ci/archive_traces_cron_worker.rb2
-rw-r--r--app/workers/ci/build_schedule_worker.rb19
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb2
-rw-r--r--app/workers/concerns/auto_devops_queue.rb9
-rw-r--r--app/workers/concerns/gitlab/github_import/rescheduling_methods.rb2
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb2
-rw-r--r--app/workers/concerns/new_issuable.rb4
-rw-r--r--app/workers/create_gpg_signature_worker.rb2
-rw-r--r--app/workers/delete_container_repository_worker.rb36
-rw-r--r--app/workers/delete_diff_files_worker.rb2
-rw-r--r--app/workers/detect_repository_languages_worker.rb2
-rw-r--r--app/workers/expire_build_artifacts_worker.rb2
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb4
-rw-r--r--app/workers/expire_job_cache_worker.rb2
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/refresh_import_jid_worker.rb2
-rw-r--r--app/workers/invalid_gpg_signature_update_worker.rb2
-rw-r--r--app/workers/issue_due_scheduler_worker.rb2
-rw-r--r--app/workers/mail_scheduler/issue_due_worker.rb2
-rw-r--r--app/workers/new_merge_request_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb2
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb4
-rw-r--r--app/workers/pages_domain_verification_worker.rb2
-rw-r--r--app/workers/pages_worker.rb2
-rw-r--r--app/workers/pipeline_hooks_worker.rb2
-rw-r--r--app/workers/pipeline_metrics_worker.rb4
-rw-r--r--app/workers/pipeline_notification_worker.rb2
-rw-r--r--app/workers/pipeline_process_worker.rb2
-rw-r--r--app/workers/pipeline_schedule_worker.rb2
-rw-r--r--app/workers/pipeline_success_worker.rb2
-rw-r--r--app/workers/pipeline_update_worker.rb2
-rw-r--r--app/workers/process_commit_worker.rb4
-rw-r--r--app/workers/project_cache_worker.rb2
-rw-r--r--app/workers/project_migrate_hashed_storage_worker.rb2
-rw-r--r--app/workers/project_service_worker.rb6
-rw-r--r--app/workers/propagate_service_template_worker.rb2
-rw-r--r--app/workers/prune_old_events_worker.rb2
-rw-r--r--app/workers/prune_web_hook_logs_worker.rb2
-rw-r--r--app/workers/reactive_caching_worker.rb2
-rw-r--r--app/workers/repository_check/batch_worker.rb6
-rw-r--r--app/workers/repository_check/clear_worker.rb2
-rw-r--r--app/workers/repository_check/single_repository_worker.rb2
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb2
-rw-r--r--app/workers/stage_update_worker.rb2
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb32
-rw-r--r--app/workers/stuck_import_jobs_worker.rb8
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb6
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb2
-rw-r--r--app/workers/update_merge_requests_worker.rb2
1538 files changed, 15183 insertions, 7517 deletions
diff --git a/app/assets/images/auth_buttons/auth0_64.png b/app/assets/images/auth_buttons/auth0_64.png
new file mode 100644
index 00000000000..5ad59659380
--- /dev/null
+++ b/app/assets/images/auth_buttons/auth0_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png
index 85de7793440..168a9c81395 100644
--- a/app/assets/images/auth_buttons/azure_64.png
+++ b/app/assets/images/auth_buttons/azure_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/bitbucket_64.png b/app/assets/images/auth_buttons/bitbucket_64.png
index b3d022a5a70..0edf7f52a11 100644
--- a/app/assets/images/auth_buttons/bitbucket_64.png
+++ b/app/assets/images/auth_buttons/bitbucket_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/google_64.png b/app/assets/images/auth_buttons/google_64.png
index 720824230a5..389c1cd54ca 100644
--- a/app/assets/images/auth_buttons/google_64.png
+++ b/app/assets/images/auth_buttons/google_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/jwt_64.png b/app/assets/images/auth_buttons/jwt_64.png
new file mode 100644
index 00000000000..ca97ae47002
--- /dev/null
+++ b/app/assets/images/auth_buttons/jwt_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/shibboleth_64.png b/app/assets/images/auth_buttons/shibboleth_64.png
new file mode 100644
index 00000000000..d4c752f9400
--- /dev/null
+++ b/app/assets/images/auth_buttons/shibboleth_64.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_scheduled.ico b/app/assets/images/ci_favicons/canary/favicon_status_scheduled.ico
new file mode 100644
index 00000000000..5444b8e41dc
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_scheduled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_scheduled.png b/app/assets/images/ci_favicons/favicon_status_scheduled.png
new file mode 100644
index 00000000000..d198c255fdd
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_scheduled.png
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/elasticsearch.png b/app/assets/images/cluster_app_logos/elasticsearch.png
new file mode 100644
index 00000000000..96e9e0ff934
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/elasticsearch.png
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/gitlab.png b/app/assets/images/cluster_app_logos/gitlab.png
new file mode 100644
index 00000000000..cb2195fc6a2
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/gitlab.png
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/helm.png b/app/assets/images/cluster_app_logos/helm.png
new file mode 100644
index 00000000000..2989cae7b93
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/helm.png
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/jeager.png b/app/assets/images/cluster_app_logos/jeager.png
new file mode 100644
index 00000000000..be5bf2a0c9c
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/jeager.png
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/jupyterhub.png b/app/assets/images/cluster_app_logos/jupyterhub.png
new file mode 100644
index 00000000000..80c7343067f
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/jupyterhub.png
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/kubernetes.png b/app/assets/images/cluster_app_logos/kubernetes.png
new file mode 100644
index 00000000000..4d774909c10
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/kubernetes.png
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/meltano.png b/app/assets/images/cluster_app_logos/meltano.png
new file mode 100644
index 00000000000..7a2d82fbe27
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/meltano.png
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/prometheus.png b/app/assets/images/cluster_app_logos/prometheus.png
new file mode 100644
index 00000000000..a8663449b88
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/prometheus.png
Binary files differ
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index cd800d75f7a..3f7a1ef1bfc 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -15,13 +15,11 @@ const Api = {
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
- templatesPath: '/api/:version/templates/:key',
- licensePath: '/api/:version/templates/licenses/:key',
- gitignorePath: '/api/:version/templates/gitignores/:key',
- gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
- dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
+ projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key',
+ projectTemplatesPath: '/api/:version/projects/:id/templates/:type',
usersPath: '/api/:version/users.json',
+ userStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
@@ -195,29 +193,29 @@ const Api = {
return axios.get(url);
},
- // Return text for a specific license
- licenseText(key, data, callback) {
- const url = Api.buildUrl(Api.licensePath).replace(':key', key);
- return axios
- .get(url, {
- params: data,
- })
- .then(res => callback(res.data));
- },
+ projectTemplate(id, type, key, options, callback) {
+ const url = Api.buildUrl(this.projectTemplatePath)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':type', type)
+ .replace(':key', encodeURIComponent(key));
- gitignoreText(key, callback) {
- const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
- return axios.get(url).then(({ data }) => callback(data));
- },
+ return axios.get(url, { params: options }).then(res => {
+ if (callback) callback(res.data);
- gitlabCiYml(key, callback) {
- const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
- return axios.get(url).then(({ data }) => callback(data));
+ return res;
+ });
},
- dockerfileYml(key, callback) {
- const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
- return axios.get(url).then(({ data }) => callback(data));
+ projectTemplates(id, type, params = {}, callback) {
+ const url = Api.buildUrl(this.projectTemplatesPath)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':type', type);
+
+ return axios.get(url, { params }).then(res => {
+ if (callback) callback(res.data);
+
+ return res;
+ });
},
issueTemplate(namespacePath, projectPath, key, type, callback) {
@@ -266,10 +264,13 @@ const Api = {
});
},
- templates(key, params = {}) {
- const url = Api.buildUrl(this.templatesPath).replace(':key', key);
+ postUserStatus({ emoji, message }) {
+ const url = Api.buildUrl(this.userStatusPath);
- return axios.get(url, { params });
+ return axios.put(url, {
+ emoji,
+ message,
+ });
},
buildUrl(url) {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 5b0c4285339..cace8bb9dba 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -42,10 +42,11 @@ export class AwardsHandler {
}
bindEvents() {
+ const $parentEl = this.targetContainerEl ? $(this.targetContainerEl) : $(document);
// If the user shows intent let's pre-build the menu
this.registerEventListener(
'one',
- $(document),
+ $parentEl,
'mouseenter focus',
this.toggleButtonSelector,
'mouseenter focus',
@@ -58,7 +59,7 @@ export class AwardsHandler {
}
},
);
- this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => {
+ this.registerEventListener('on', $parentEl, 'click', this.toggleButtonSelector, e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
@@ -76,7 +77,7 @@ export class AwardsHandler {
});
const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`;
- this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => {
+ this.registerEventListener('on', $parentEl, 'click', emojiButtonSelector, e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
@@ -168,7 +169,8 @@ export class AwardsHandler {
</div>
`;
- document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup);
+ const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
+ targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
this.addRemainingEmojiMenuCategories();
this.setupSearch();
@@ -250,6 +252,12 @@ export class AwardsHandler {
}
positionMenu($menu, $addBtn) {
+ if (this.targetContainerEl) {
+ return $menu.css({
+ top: `${$addBtn.outerHeight()}px`,
+ });
+ }
+
const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element
// So we position the element absolute in the body
@@ -424,9 +432,7 @@ export class AwardsHandler {
users = origTitle.trim().split(FROM_SENTENCE_REGEX);
}
users.unshift('You');
- return awardBlock
- .attr('title', this.toSentence(users))
- .tooltip('_fixTitle');
+ return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle');
}
createAwardButtonForVotesBlock(votesBlock, emojiName) {
@@ -609,13 +615,11 @@ export class AwardsHandler {
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
- awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
- Emoji => {
- const awardsHandler = new AwardsHandler(Emoji);
- awardsHandler.bindEvents();
- return awardsHandler;
- },
- );
+ awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => {
+ const awardsHandler = new AwardsHandler(Emoji);
+ awardsHandler.bindEvents();
+ return awardsHandler;
+ });
}
return awardsHandlerPromise;
}
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 155c348286c..97232d7f783 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -1,13 +1,11 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'Badge',
components: {
Icon,
- LoadingIcon,
Tooltip,
},
directives: {
@@ -80,7 +78,7 @@ export default {
/>
</a>
- <loading-icon
+ <gl-loading-icon
v-show="isLoading"
:inline="true"
/>
@@ -105,8 +103,8 @@ export default {
</div>
<button
- v-tooltip
v-show="hasError"
+ v-tooltip
:title="s__('Badges|Reload badge image')"
class="btn btn-transparent btn-sm text-primary"
type="button"
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 7a13f74c570..aff7c4180e3 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -4,7 +4,6 @@ import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
@@ -15,7 +14,6 @@ export default {
components: {
Badge,
LoadingButton,
- LoadingIcon,
},
props: {
isEditing: {
@@ -23,6 +21,11 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ wasValidated: false,
+ };
+ },
computed: {
...mapState([
'badgeInAddForm',
@@ -39,16 +42,6 @@ export default {
return this.badgeInAddForm;
},
- canSubmit() {
- return (
- this.badge !== null &&
- this.badge.imageUrl &&
- this.badge.imageUrl.trim() !== '' &&
- this.badge.linkUrl &&
- this.badge.linkUrl.trim() !== '' &&
- !this.isSaving
- );
- },
helpText() {
const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
.map(placeholder => `<code>%{${placeholder}}</code>`)
@@ -93,11 +86,18 @@ export default {
});
},
},
- submitButtonLabel() {
- if (this.isEditing) {
- return s__('Badges|Save changes');
- }
- return s__('Badges|Add badge');
+ badgeImageUrlExample() {
+ const exampleUrl =
+ 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/badge.svg';
+ return sprintf(s__('Badges|e.g. %{exampleUrl}'), {
+ exampleUrl,
+ });
+ },
+ badgeLinkUrlExample() {
+ const exampleUrl = 'https://example.gitlab.com/%{project_path}';
+ return sprintf(s__('Badges|e.g. %{exampleUrl}'), {
+ exampleUrl,
+ });
},
},
methods: {
@@ -109,7 +109,9 @@ export default {
this.stopEditing();
},
onSubmit() {
- if (!this.canSubmit) {
+ const form = this.$el;
+ if (!form.checkValidity()) {
+ this.wasValidated = true;
return Promise.resolve();
}
@@ -117,6 +119,7 @@ export default {
return this.saveBadge()
.then(() => {
createFlash(s__('Badges|The badge was saved.'), 'notice');
+ this.wasValidated = false;
})
.catch(error => {
createFlash(
@@ -129,6 +132,7 @@ export default {
return this.addBadge()
.then(() => {
createFlash(s__('Badges|A new badge was added.'), 'notice');
+ this.wasValidated = false;
})
.catch(error => {
createFlash(
@@ -138,47 +142,58 @@ export default {
});
},
},
- badgeImageUrlPlaceholder:
- 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg',
- badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}',
};
</script>
<template>
<form
- class="prepend-top-default append-bottom-default"
+ :class="{ 'was-validated': wasValidated }"
+ class="prepend-top-default append-bottom-default needs-validation"
+ novalidate
@submit.prevent.stop="onSubmit"
>
<div class="form-group">
- <label for="badge-link-url">{{ s__('Badges|Link') }}</label>
+ <label
+ for="badge-link-url"
+ class="label-bold"
+ >{{ s__('Badges|Link') }}</label>
+ <p v-html="helpText"></p>
<input
id="badge-link-url"
v-model="linkUrl"
- :placeholder="$options.badgeLinkUrlPlaceholder"
- type="text"
+ type="URL"
class="form-control"
+ required
@input="debouncedPreview"
/>
- <span
- class="form-text text-muted"
- v-html="helpText"
- ></span>
+ <div class="invalid-feedback">
+ {{ s__('Badges|Please fill in a valid URL') }}
+ </div>
+ <span class="form-text text-muted">
+ {{ badgeLinkUrlExample }}
+ </span>
</div>
<div class="form-group">
- <label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
+ <label
+ for="badge-image-url"
+ class="label-bold"
+ >{{ s__('Badges|Badge image URL') }}</label>
+ <p v-html="helpText"></p>
<input
id="badge-image-url"
v-model="imageUrl"
- :placeholder="$options.badgeImageUrlPlaceholder"
- type="text"
+ type="URL"
class="form-control"
+ required
@input="debouncedPreview"
/>
- <span
- class="form-text text-muted"
- v-html="helpText"
- ></span>
+ <div class="invalid-feedback">
+ {{ s__('Badges|Please fill in a valid URL') }}
+ </div>
+ <span class="form-text text-muted">
+ {{ badgeImageUrlExample }}
+ </span>
</div>
<div class="form-group">
@@ -190,7 +205,7 @@ export default {
:link-url="renderedLinkUrl"
/>
<p v-show="isRendering">
- <loading-icon
+ <gl-loading-icon
:inline="true"
/>
</p>
@@ -200,20 +215,32 @@ export default {
>{{ s__('Badges|No image to preview') }}</p>
</div>
- <div class="row-content-block">
+ <div
+ v-if="isEditing"
+ class="row-content-block"
+ >
<loading-button
- :disabled="!canSubmit"
:loading="isSaving"
- :label="submitButtonLabel"
+ :label="s__('Badges|Save changes')"
type="submit"
container-class="btn btn-success"
/>
<button
- v-if="isEditing"
class="btn btn-cancel"
type="button"
@click="onCancel"
>{{ __('Cancel') }}</button>
</div>
+ <div
+ v-else
+ class="form-group"
+ >
+ <loading-button
+ :loading="isSaving"
+ :label="s__('Badges|Add badge')"
+ type="submit"
+ container-class="btn btn-success"
+ />
+ </div>
</form>
</template>
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index 268968b63b3..359d3e10380 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -1,6 +1,5 @@
<script>
import { mapState } from 'vuex';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import BadgeListRow from './badge_list_row.vue';
import { GROUP_BADGE } from '../constants';
@@ -8,7 +7,6 @@ export default {
name: 'BadgeList',
components: {
BadgeListRow,
- LoadingIcon,
},
computed: {
...mapState(['badges', 'isLoading', 'kind']),
@@ -28,13 +26,13 @@ export default {
{{ s__('Badges|Your badges') }}
<span
v-show="!isLoading"
- class="badge"
+ class="badge badge-pill"
>{{ badges.length }}</span>
</div>
- <loading-icon
+ <gl-loading-icon
v-show="isLoading"
+ :size="2"
class="card-body"
- size="2"
/>
<div
v-if="hasNoBadges"
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index 98aa00af0d7..5d16ba3ce6d 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -2,7 +2,6 @@
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
@@ -11,7 +10,6 @@ export default {
components: {
Badge,
Icon,
- LoadingIcon,
},
props: {
badge: {
@@ -43,13 +41,13 @@ export default {
<badge
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
- class="table-section section-30"
+ class="table-section section-40"
/>
- <span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
- <div class="table-section section-10">
- <span class="badge">{{ badgeKindText }}</span>
+ <span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span>
+ <div class="table-section section-15">
+ <span class="badge badge-pill">{{ badgeKindText }}</span>
</div>
- <div class="table-section section-10 table-button-footer">
+ <div class="table-section section-15 table-button-footer">
<div
v-if="canEditBadge"
class="table-action-buttons">
@@ -79,7 +77,7 @@ export default {
name="remove"
/>
</button>
- <loading-icon
+ <gl-loading-icon
v-show="badge.isDeleting"
:inline="true"
/>
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 84fef4d8b4f..8c4eccc34a3 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,15 +1,19 @@
import './autosize';
import './bind_in_out';
import './markdown/render_gfm';
+import initGFMInput from './markdown/gfm_auto_complete';
import initCopyAsGFM from './markdown/copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
import './requires_input';
+import initPageShortcuts from './shortcuts';
import './toggler_behavior';
-import '../preview_markdown';
+import './preview_markdown';
installGlEmojiElement();
+initGFMInput();
initCopyAsGFM();
initCopyToClipboard();
+initPageShortcuts();
diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
new file mode 100644
index 00000000000..a303e504cc7
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
@@ -0,0 +1,19 @@
+import $ from 'jquery';
+import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
+import GfmAutoComplete from '~/gfm_auto_complete';
+
+export default function initGFMInput() {
+ $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
+ const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
+ const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
+
+ gfm.setup($(el), {
+ emojis: true,
+ members: enableGFM,
+ issues: enableGFM,
+ milestones: enableGFM,
+ mergeRequests: enableGFM,
+ labels: enableGFM,
+ });
+ });
+}
diff --git a/app/assets/javascripts/behaviors/markdown/highlight_current_user.js b/app/assets/javascripts/behaviors/markdown/highlight_current_user.js
new file mode 100644
index 00000000000..6208b3f0032
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/highlight_current_user.js
@@ -0,0 +1,17 @@
+/**
+ * Highlights the current user in existing elements with a user ID data attribute.
+ *
+ * @param elements DOM elements that represent user mentions
+ */
+export default function highlightCurrentUser(elements) {
+ const currentUserId = gon && gon.current_user_id;
+ if (!currentUserId) {
+ return;
+ }
+
+ elements.forEach(element => {
+ if (parseInt(element.dataset.user, 10) === currentUserId) {
+ element.classList.add('current-user');
+ }
+ });
+}
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index dbff2bd4b10..a2d4331b6d1 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -2,8 +2,9 @@ import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
+import highlightCurrentUser from './highlight_current_user';
-// Render Gitlab flavoured Markdown
+// Render GitLab flavoured Markdown
//
// Delegates to syntax highlight and render math & mermaid diagrams.
//
@@ -11,6 +12,7 @@ $.fn.renderGFM = function renderGFM() {
syntaxHighlight(this.find('.js-syntax-highlight'));
renderMath(this.find('.js-render-math'));
renderMermaid(this.find('.js-render-mermaid'));
+ highlightCurrentUser(this.find('.gfm-project_member').get());
return this;
};
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 0964baf8954..0964baf8954 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js
new file mode 100644
index 00000000000..7987a533ae5
--- /dev/null
+++ b/app/assets/javascripts/behaviors/shortcuts.js
@@ -0,0 +1,35 @@
+import Shortcuts from './shortcuts/shortcuts';
+
+export default function initPageShortcuts() {
+ const { page } = document.body.dataset;
+ const pagesWithCustomShortcuts = [
+ 'projects:activity',
+ 'projects:artifacts:browse',
+ 'projects:artifacts:file',
+ 'projects:blame:show',
+ 'projects:blob:show',
+ 'projects:commit:show',
+ 'projects:commits:show',
+ 'projects:find_file:show',
+ 'projects:issues:edit',
+ 'projects:issues:index',
+ 'projects:issues:new',
+ 'projects:issues:show',
+ 'projects:merge_requests:creations:diffs',
+ 'projects:merge_requests:creations:new',
+ 'projects:merge_requests:edit',
+ 'projects:merge_requests:index',
+ 'projects:merge_requests:show',
+ 'projects:network:show',
+ 'projects:show',
+ 'projects:tree:show',
+ 'groups:show',
+ ];
+
+ // the pages above have their own shortcuts sub-classes instantiated elsewhere
+ // TODO: replace this whitelist with something more automated/maintainable
+ if (page && !pagesWithCustomShortcuts.includes(page)) {
+ return new Shortcuts();
+ }
+ return false;
+}
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 99c71d6524a..6719bfd6d22 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
-import axios from './lib/utils/axios_utils';
-import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility';
-import findAndFollowLink from './shortcuts_dashboard_navigation';
+import axios from '../../lib/utils/axios_utils';
+import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
+import findAndFollowLink from '../../lib/utils/navigation_utility';
const defaultStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = (e, element, combo) => {
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
index 908b9cab93d..052e33b4a2b 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
@@ -1,5 +1,5 @@
import Mousetrap from 'mousetrap';
-import { getLocationHash, visitUrl } from './lib/utils/url_utility';
+import { getLocationHash, visitUrl } from '../../lib/utils/url_utility';
import Shortcuts from './shortcuts';
const defaults = {
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
index 8658081c6c2..8658081c6c2 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index e9451be31fd..5e48bf5a35c 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import Mousetrap from 'mousetrap';
import _ from 'underscore';
-import Sidebar from './right_sidebar';
+import Sidebar from '../../right_sidebar';
import Shortcuts from './shortcuts';
-import { CopyAsGFM } from './behaviors/markdown/copy_as_gfm';
+import { CopyAsGFM } from '../markdown/copy_as_gfm';
export default class ShortcutsIssuable extends Shortcuts {
constructor(isMergeRequest) {
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
index 6b595764bc5..fa9b2c9f755 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
@@ -1,5 +1,5 @@
import Mousetrap from 'mousetrap';
-import findAndFollowLink from './shortcuts_dashboard_navigation';
+import findAndFollowLink from '../../lib/utils/navigation_utility';
import Shortcuts from './shortcuts';
export default class ShortcutsNavigation extends Shortcuts {
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
index a88c280fa3b..a88c280fa3b 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
index 41865dcf4ba..8b7e6a56d25 100644
--- a/app/assets/javascripts/shortcuts_wiki.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
@@ -1,6 +1,6 @@
import Mousetrap from 'mousetrap';
import ShortcutsNavigation from './shortcuts_navigation';
-import findAndFollowLink from './shortcuts_dashboard_navigation';
+import findAndFollowLink from '../../lib/utils/navigation_utility';
export default class ShortcutsWiki extends ShortcutsNavigation {
constructor() {
diff --git a/app/assets/javascripts/blob/3d_viewer/index.js b/app/assets/javascripts/blob/3d_viewer/index.js
index 68d4ddad551..1bdf1aeb76c 100644
--- a/app/assets/javascripts/blob/3d_viewer/index.js
+++ b/app/assets/javascripts/blob/3d_viewer/index.js
@@ -29,12 +29,12 @@ export default class Renderer {
this.scene.add(this.camera);
- // Setup the viewer
+ // Set up the viewer
this.setupRenderer();
this.setupGrid();
this.setupLight();
- // Setup OrbitControls
+ // Set up OrbitControls
this.controls = new OrbitControls(
this.camera,
this.renderer.domElement,
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index ff1cbcad145..addacf29f1e 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this */
+import Api from '~/api';
import $ from 'jquery';
import Flash from '../flash';
@@ -9,9 +9,10 @@ import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector';
export default class FileTemplateMediator {
- constructor({ editor, currentAction }) {
+ constructor({ editor, currentAction, projectId }) {
this.editor = editor;
this.currentAction = currentAction;
+ this.projectId = projectId;
this.initTemplateSelectors();
this.initTemplateTypeSelector();
@@ -33,15 +34,14 @@ export default class FileTemplateMediator {
initTemplateTypeSelector() {
this.typeSelector = new FileTemplateTypeSelector({
mediator: this,
- dropdownData: this.templateSelectors
- .map((templateSelector) => {
- const cfg = templateSelector.config;
-
- return {
- name: cfg.name,
- key: cfg.key,
- };
- }),
+ dropdownData: this.templateSelectors.map(templateSelector => {
+ const cfg = templateSelector.config;
+
+ return {
+ name: cfg.name,
+ key: cfg.key,
+ };
+ }),
});
}
@@ -89,7 +89,7 @@ export default class FileTemplateMediator {
}
listenForPreviewMode() {
- this.$navLinks.on('click', 'a', (e) => {
+ this.$navLinks.on('click', 'a', e => {
const urlPieces = e.target.href.split('#');
const hash = urlPieces[1];
if (hash === 'preview') {
@@ -105,7 +105,7 @@ export default class FileTemplateMediator {
e.preventDefault();
}
- this.templateSelectors.forEach((selector) => {
+ this.templateSelectors.forEach(selector => {
if (selector.config.key === item.key) {
selector.show();
} else {
@@ -126,8 +126,8 @@ export default class FileTemplateMediator {
selector.renderLoading();
// in case undo menu is already already there
this.destroyUndoMenu();
- this.fetchFileTemplate(selector.config.endpoint, query, data)
- .then((file) => {
+ this.fetchFileTemplate(selector.config.type, query, data)
+ .then(file => {
this.showUndoMenu();
this.setEditorContent(file);
this.setFilename(selector.config.name);
@@ -138,7 +138,7 @@ export default class FileTemplateMediator {
displayMatchedTemplateSelector() {
const currentInput = this.getFilename();
- this.templateSelectors.forEach((selector) => {
+ this.templateSelectors.forEach(selector => {
const match = selector.config.pattern.test(currentInput);
if (match) {
@@ -149,15 +149,11 @@ export default class FileTemplateMediator {
});
}
- fetchFileTemplate(apiCall, query, data) {
- return new Promise((resolve) => {
+ fetchFileTemplate(type, query, data = {}) {
+ return new Promise(resolve => {
const resolveFile = file => resolve(file);
- if (!data) {
- apiCall(query, resolveFile);
- } else {
- apiCall(query, data, resolveFile);
- }
+ Api.projectTemplate(this.projectId, type, query, data, resolveFile);
});
}
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index 9dfdb06007d..9db1fa70ffb 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -66,9 +66,6 @@ export default class TemplateSelector {
// be added by all subclasses.
}
- // To be implemented on the extending class
- // e.g. Api.gitlabCiYml(query.name, file => this.setEditorContent(file));
-
setEditorContent(file, { skipFocus } = {}) {
if (!file) return;
diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
index 9c41e429c8d..43f7aead8b9 100644
--- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
@@ -1,5 +1,3 @@
-import Api from '../../api';
-
import FileTemplateSelector from '../file_template_selector';
export default class BlobCiYamlSelector extends FileTemplateSelector {
@@ -9,7 +7,7 @@ export default class BlobCiYamlSelector extends FileTemplateSelector {
key: 'gitlab-ci-yaml',
name: '.gitlab-ci.yml',
pattern: /(.gitlab-ci.yml)/,
- endpoint: Api.gitlabCiYml,
+ type: 'gitlab_ci_ymls',
dropdown: '.js-gitlab-ci-yml-selector',
wrapper: '.js-gitlab-ci-yml-selector-wrap',
};
diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
index 45fb614fe00..4718b642617 100644
--- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
@@ -1,5 +1,3 @@
-import Api from '../../api';
-
import FileTemplateSelector from '../file_template_selector';
export default class DockerfileSelector extends FileTemplateSelector {
@@ -9,7 +7,7 @@ export default class DockerfileSelector extends FileTemplateSelector {
key: 'dockerfile',
name: 'Dockerfile',
pattern: /(Dockerfile)/,
- endpoint: Api.dockerfileYml,
+ type: 'dockerfiles',
dropdown: '.js-dockerfile-selector',
wrapper: '.js-dockerfile-selector-wrap',
};
diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
index a894953cc86..a8067ec5c84 100644
--- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
@@ -1,5 +1,3 @@
-import Api from '../../api';
-
import FileTemplateSelector from '../file_template_selector';
export default class BlobGitignoreSelector extends FileTemplateSelector {
@@ -9,7 +7,7 @@ export default class BlobGitignoreSelector extends FileTemplateSelector {
key: 'gitignore',
name: '.gitignore',
pattern: /(.gitignore)/,
- endpoint: Api.gitignoreText,
+ type: 'gitignores',
dropdown: '.js-gitignore-selector',
wrapper: '.js-gitignore-selector-wrap',
};
diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js
index b7c4da0f62e..ac1fe95eee5 100644
--- a/app/assets/javascripts/blob/template_selectors/license_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/license_selector.js
@@ -1,5 +1,3 @@
-import Api from '../../api';
-
import FileTemplateSelector from '../file_template_selector';
export default class BlobLicenseSelector extends FileTemplateSelector {
@@ -9,7 +7,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
key: 'license',
name: 'LICENSE',
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
- endpoint: Api.licenseText,
+ type: 'licenses',
dropdown: '.js-license-selector',
wrapper: '.js-license-selector-wrap',
};
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index a603d89b84a..4e4598870fa 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -15,8 +15,9 @@ export default () => {
const assetsPath = editBlobForm.data('assetsPrefix');
const blobLanguage = editBlobForm.data('blobLanguage');
const currentAction = $('.js-file-title').data('currentAction');
+ const projectId = editBlobForm.data('project-id');
- new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction);
+ new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction, projectId);
new NewCommitForm(editBlobForm);
}
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 82a3d494b67..ec2b130ab7d 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -7,11 +7,11 @@ import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator';
export default class EditBlob {
- constructor(assetsPath, aceMode, currentAction) {
+ constructor(assetsPath, aceMode, currentAction, projectId) {
this.configureAceEditor(aceMode, assetsPath);
this.initModePanesAndLinks();
this.initSoftWrap();
- this.initFileSelectors(currentAction);
+ this.initFileSelectors(currentAction, projectId);
}
configureAceEditor(aceMode, assetsPath) {
@@ -30,10 +30,11 @@ export default class EditBlob {
}
}
- initFileSelectors(currentAction) {
+ initFileSelectors(currentAction, projectId) {
this.fileTemplateMediator = new TemplateSelectorMediator({
currentAction,
editor: this.editor,
+ projectId,
});
}
@@ -60,14 +61,15 @@ export default class EditBlob {
if (paneId === '#preview') {
this.$toggleButton.hide();
- axios.post(currentLink.data('previewUrl'), {
- content: this.editor.getValue(),
- })
- .then(({ data }) => {
- currentPane.empty().append(data);
- currentPane.renderGFM();
- })
- .catch(() => createFlash(__('An error occurred previewing the blob')));
+ axios
+ .post(currentLink.data('previewUrl'), {
+ content: this.editor.getValue(),
+ })
+ .then(({ data }) => {
+ currentPane.empty().append(data);
+ currentPane.renderGFM();
+ })
+ .catch(() => createFlash(__('An error occurred previewing the blob')));
}
this.$toggleButton.show();
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index 286529b4d13..cde22725a89 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -83,7 +83,7 @@ export default {
right on the way to making the most of your board.
</p>
<button
- class="btn btn-create btn-inverted btn-block"
+ class="btn btn-success btn-inverted btn-block"
type="button"
@click.stop="addDefaultLists">
Add default lists
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index bfc8d9b03ad..7ddb22ad824 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -3,7 +3,6 @@ import Sortable from 'sortablejs';
import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
const Store = gl.issueBoards.BoardsStore;
@@ -12,7 +11,6 @@ export default {
components: {
boardCard,
boardNewIssue,
- loadingIcon,
},
props: {
groupId: {
@@ -217,7 +215,7 @@ export default {
v-if="loading"
class="board-list-loading text-center"
aria-label="Loading issues">
- <loading-icon />
+ <gl-loading-icon />
</div>
<board-new-issue
v-if="list.type !== 'closed' && showIssueForm"
@@ -233,19 +231,19 @@ export default {
<board-card
v-for="(issue, index) in issues"
ref="issue"
+ :key="issue.id"
:index="index"
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
- :disabled="disabled"
- :key="issue.id" />
+ :disabled="disabled" />
<li
v-if="showCount"
class="board-list-count text-center"
data-issue-id="-1">
- <loading-icon
+ <gl-loading-icon
v-show="list.loadingMore"
label="Loading more issues"
/>
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 1e3cd43d1f0..f7ce5128964 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+import { Button } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
@@ -10,6 +11,7 @@ export default {
name: 'BoardNewIssue',
components: {
ProjectSelect,
+ 'gl-button': Button,
},
props: {
groupId: {
@@ -110,9 +112,9 @@ export default {
Title
</label>
<input
+ :id="list.id + '-title'"
ref="input"
v-model="title"
- :id="list.id + '-title'"
class="form-control"
type="text"
name="issue_title"
@@ -123,21 +125,23 @@ export default {
:group-id="groupId"
/>
<div class="clearfix prepend-top-10">
- <button
+ <gl-button
ref="submit-button"
:disabled="disabled"
- class="btn btn-success float-left"
+ class="float-left"
+ variant="success"
type="submit"
>
Submit issue
- </button>
- <button
- class="btn btn-default float-right"
+ </gl-button>
+ <gl-button
+ class="float-right"
type="button"
+ variant="default"
@click="cancel"
>
Cancel
- </button>
+ </gl-button>
</div>
</form>
</div>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index d50641dc3a9..8b5536200e1 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -149,10 +149,11 @@
<a
:href="issue.path"
:title="issue.title"
- class="js-no-trigger">{{ issue.title }}</a>
+ class="js-no-trigger"
+ @mousemove.stop>{{ issue.title }}</a>
<span
v-if="issueId"
- class="board-card-number"
+ class="board-card-number append-right-5"
>
{{ issue.referencePath }}
</span>
@@ -170,8 +171,8 @@
tooltip-placement="bottom"
/>
<span
- v-tooltip
v-if="shouldRenderCounter"
+ v-tooltip
:title="assigneeCounterTooltip"
class="avatar-counter"
>
@@ -184,10 +185,10 @@
class="board-card-footer"
>
<button
- v-tooltip
v-for="label in issue.labels"
v-if="showLabel(label)"
:key="label.id"
+ v-tooltip
:style="labelStyle(label)"
:title="label.description"
class="badge color-label"
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 33e72a6782e..0c4c709324d 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -1,7 +1,6 @@
<script>
/* global ListIssue */
- import queryData from '~/boards/utils/query_data';
- import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import { urlParamsToObject } from '~/lib/utils/common_utils';
import ModalHeader from './header.vue';
import ModalList from './list.vue';
import ModalFooter from './footer.vue';
@@ -14,7 +13,6 @@
ModalHeader,
ModalList,
ModalFooter,
- loadingIcon,
},
props: {
newIssuePath: {
@@ -109,13 +107,11 @@
loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
- return gl.boardService
- .getBacklog(
- queryData(this.filter.path, {
- page: this.page,
- per: this.perPage,
- }),
- )
+ return gl.boardService.getBacklog({
+ ...urlParamsToObject(this.filter.path),
+ page: this.page,
+ per: this.perPage,
+ })
.then(res => res.data)
.then(data => {
if (clearIssues) {
@@ -169,7 +165,7 @@
class="add-issues-list text-center"
>
<div class="add-issues-list-loading">
- <loading-icon />
+ <gl-loading-icon />
</div>
</section>
<modal-footer/>
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
index 6a5a39099bd..4f23e5db35c 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -1,7 +1,11 @@
<script>
+import { Link } from '@gitlab-org/gitlab-ui';
import ModalStore from '../../stores/modal_store';
export default {
+ components: {
+ 'gl-link': Link,
+ },
data() {
return {
modal: ModalStore.store,
@@ -38,7 +42,7 @@ export default {
v-for="(list, i) in state.lists"
v-if="list.type == 'label'"
:key="i">
- <a
+ <gl-link
:class="{ 'is-active': list.id == selected.id }"
href="#"
role="button"
@@ -48,7 +52,7 @@ export default {
class="dropdown-label-box">
</span>
{{ list.title }}
- </a>
+ </gl-link>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index ef9844d5562..d4676914e02 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -2,14 +2,10 @@
import $ from 'jquery';
import _ from 'underscore';
import eventHub from '../eventhub';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
export default {
name: 'BoardProjectSelect',
- components: {
- loadingIcon,
- },
props: {
groupId: {
type: Number,
@@ -119,7 +115,7 @@ export default {
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
- <loading-icon />
+ <gl-loading-icon />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index bc263cbbfea..caa6ce84335 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -9,7 +9,7 @@ import '~/vue_shared/models/assignee';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
-import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first
+import sidebarEventHub from '~/sidebar/event_hub';
import './models/issue';
import './models/list';
import './models/milestone';
@@ -24,7 +24,8 @@ import './components/board';
import './components/board_sidebar';
import './components/new_list_dropdown';
import BoardAddIssuesModal from './components/modal/index.vue';
-import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first
+import '~/vue_shared/vue_resource_interceptor';
+import { NavigationType } from '~/lib/utils/common_utils';
export default () => {
const $boardApp = document.getElementById('board-app');
@@ -32,6 +33,16 @@ export default () => {
window.gl = window.gl || {};
+ // check for browser back and trigger a hard reload to circumvent browser caching.
+ window.addEventListener('pageshow', (event) => {
+ const isNavTypeBackForward = window.performance &&
+ window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
+
+ if (event.persisted || isNavTypeBackForward) {
+ window.location.reload();
+ }
+ });
+
if (gl.IssueBoardsApp) {
gl.IssueBoardsApp.$destroy(true);
}
@@ -229,7 +240,7 @@ export default () => {
template: `
<div class="board-extra-actions">
<button
- class="btn btn-create prepend-left-10"
+ class="btn btn-success prepend-left-10"
type="button"
data-placement="bottom"
ref="addIssuesButton"
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index ad473404c29..d416b76f0f4 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -4,7 +4,7 @@
import { __ } from '~/locale';
import ListLabel from '~/vue_shared/models/label';
import ListAssignee from '~/vue_shared/models/assignee';
-import queryData from '../utils/query_data';
+import { urlParamsToObject } from '~/lib/utils/common_utils';
const PER_PAGE = 20;
@@ -115,7 +115,10 @@ class List {
}
getIssues(emptyIssues = true) {
- const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
+ const data = {
+ ...urlParamsToObject(gl.issueBoards.BoardsStore.filter.path),
+ page: this.page,
+ };
if (this.label && data.label_name) {
data.label_name = data.label_name.filter(label => label !== this.label.title);
diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js
deleted file mode 100644
index 65315979df7..00000000000
--- a/app/assets/javascripts/boards/utils/query_data.js
+++ /dev/null
@@ -1,21 +0,0 @@
-export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => {
- if (filterParam === '') return dataParam;
-
- const data = dataParam;
- const paramSplit = filterParam.split('=');
- const paramKeyNormalized = paramSplit[0].replace('[]', '');
- const isArray = paramSplit[0].indexOf('[]');
- const value = decodeURIComponent(paramSplit[1].replace(/\+/g, ' '));
-
- if (isArray !== -1) {
- if (!data[paramKeyNormalized]) {
- data[paramKeyNormalized] = [];
- }
-
- data[paramKeyNormalized].push(value);
- } else {
- data[paramKeyNormalized] = value;
- }
-
- return data;
-}, extraData);
diff --git a/app/assets/javascripts/build_variables.js b/app/assets/javascripts/build_variables.js
deleted file mode 100644
index d398e4a4c83..00000000000
--- a/app/assets/javascripts/build_variables.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import $ from 'jquery';
-
-export default function handleRevealVariables() {
- $('.js-reveal-variables')
- .off('click')
- .on('click', function click() {
- $('.js-build-variables').toggle();
- $(this).hide();
- });
-}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 0fdf0c7a389..65e7cee7039 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,16 +1,12 @@
import Visibility from 'visibilityjs';
import Vue from 'vue';
+import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
-import {
- APPLICATION_STATUS,
- REQUEST_LOADING,
- REQUEST_SUCCESS,
- REQUEST_FAILURE,
-} from './constants';
+import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue';
@@ -66,6 +62,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token');
+ Clusters.initDismissableCallout();
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications();
@@ -108,6 +105,12 @@ export default class Clusters {
});
}
+ static initDismissableCallout() {
+ const callout = document.querySelector('.js-cluster-security-warning');
+
+ if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
+ }
+
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
@@ -129,7 +132,8 @@ export default class Clusters {
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
- this.service.fetchData()
+ this.service
+ .fetchData()
.then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError());
}
@@ -177,15 +181,21 @@ export default class Clusters {
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
const appTitles = Object.keys(newApplicationMap)
- .filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
- prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
- prevApplicationMap[appId].status !== null)
+ .filter(
+ appId =>
+ newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
+ prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
+ prevApplicationMap[appId].status !== null,
+ )
.map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) {
- const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), {
- appList: appTitles.join(', '),
- });
+ const text = sprintf(
+ s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
+ {
+ appList: appTitles.join(', '),
+ },
+ );
Flash(text, 'notice', this.successApplicationContainer);
}
}
@@ -218,13 +228,18 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null);
- this.service.installApplication(appId, data.params)
+ this.service
+ .installApplication(appId, data.params)
.then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
})
.catch(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
- this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed'));
+ this.store.updateAppProperty(
+ appId,
+ 'requestReason',
+ s__('ClusterIntegration|Request to begin installing failed'),
+ );
});
}
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
index 1e5c733d151..e32d507d1f7 100644
--- a/app/assets/javascripts/clusters/clusters_index.js
+++ b/app/assets/javascripts/clusters/clusters_index.js
@@ -1,14 +1,15 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
import setupToggleButtons from '~/toggle_buttons';
-import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+import PersistentUserCallout from '../persistent_user_callout';
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
- gcpSignupOffer();
+ const callout = document.querySelector('.gcp-signup-offer');
+ if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
// The empty state won't have a clusterList
if (clusterList) {
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 651f3b50236..0452729d3ff 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -2,6 +2,7 @@
/* eslint-disable vue/require-default-prop */
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
+ import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
APPLICATION_STATUS,
@@ -13,6 +14,7 @@
export default {
components: {
loadingButton,
+ identicon,
},
props: {
id: {
@@ -31,6 +33,16 @@
type: String,
required: false,
},
+ logoUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
status: {
type: String,
required: false,
@@ -60,6 +72,18 @@
isKnownStatus() {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
+ isInstalled() {
+ return (
+ this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.UPDATED
+ );
+ },
+ hasLogo() {
+ return !!this.logoUrl;
+ },
+ identiconId() {
+ // generate a deterministic integer id for the identicon background
+ return this.id.charCodeAt(0);
+ },
rowJsClass() {
return `js-cluster-application-row-${this.id}`;
},
@@ -128,37 +152,81 @@
<template>
<div
- :class="rowJsClass"
- class="gl-responsive-table-row gl-responsive-table-row-col-span"
+ :class="[
+ rowJsClass,
+ isInstalled && 'cluster-application-installed',
+ disabled && 'cluster-application-disabled'
+ ]"
+ class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span"
>
<div
class="gl-responsive-table-row-layout"
role="row"
>
- <a
- v-if="titleLink"
- :href="titleLink"
- target="blank"
- rel="noopener noreferrer"
+ <div
+ class="table-section append-right-8 section-align-top"
role="gridcell"
- class="table-section section-15 section-align-top js-cluster-application-title"
>
- {{ title }}
- </a>
- <span
- v-else
- class="table-section section-15 section-align-top js-cluster-application-title"
- >
- {{ title }}
- </span>
+ <img
+ v-if="hasLogo"
+ :src="logoUrl"
+ :alt="`${title} logo`"
+ class="cluster-application-logo avatar s40"
+ />
+ <identicon
+ v-else
+ :entity-id="identiconId"
+ :entity-name="title"
+ size-class="s40"
+ />
+ </div>
<div
- class="table-section section-wrap"
+ class="table-section cluster-application-description section-wrap"
role="gridcell"
>
+ <strong>
+ <a
+ v-if="titleLink"
+ :href="titleLink"
+ target="blank"
+ rel="noopener noreferrer"
+ class="js-cluster-application-title"
+ >
+ {{ title }}
+ </a>
+ <span
+ v-else
+ class="js-cluster-application-title"
+ >
+ {{ title }}
+ </span>
+ </strong>
<slot name="description"></slot>
+ <div
+ v-if="hasError || isUnknownStatus"
+ class="cluster-application-error text-danger prepend-top-10"
+ >
+ <p class="js-cluster-application-general-error-message append-bottom-0">
+ {{ generalErrorDescription }}
+ </p>
+ <ul v-if="statusReason || requestReason">
+ <li
+ v-if="statusReason"
+ class="js-cluster-application-status-error-message"
+ >
+ {{ statusReason }}
+ </li>
+ <li
+ v-if="requestReason"
+ class="js-cluster-application-request-error-message"
+ >
+ {{ requestReason }}
+ </li>
+ </ul>
+ </div>
</div>
<div
- :class="{ 'section-20': showManageButton, 'section-15': !showManageButton }"
+ :class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
class="table-section table-button-footer section-align-top"
role="gridcell"
>
@@ -168,6 +236,7 @@
>
<a
:href="manageLink"
+ :class="{ disabled: disabled }"
class="btn"
>
{{ manageButtonLabel }}
@@ -176,7 +245,7 @@
<div class="btn-group table-action-buttons">
<loading-button
:loading="installButtonLoading"
- :disabled="installButtonDisabled"
+ :disabled="disabled || installButtonDisabled"
:label="installButtonLabel"
class="js-cluster-application-install-button"
@click="installClicked"
@@ -184,35 +253,5 @@
</div>
</div>
</div>
- <div
- v-if="hasError || isUnknownStatus"
- class="gl-responsive-table-row-layout"
- role="row"
- >
- <div
- class="alert alert-danger alert-block append-bottom-0 clusters-error-alert"
- role="gridcell"
- >
- <div>
- <p class="js-cluster-application-general-error-message">
- {{ generalErrorDescription }}
- </p>
- <ul v-if="statusReason || requestReason">
- <li
- v-if="statusReason"
- class="js-cluster-application-status-error-message"
- >
- {{ statusReason }}
- </li>
- <li
- v-if="requestReason"
- class="js-cluster-application-request-error-message"
- >
- {{ requestReason }}
- </li>
- </ul>
- </div>
- </div>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index d708a9e595a..a1069985178 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,5 +1,14 @@
<script>
import _ from 'underscore';
+import helmInstallIllustration from '@gitlab-org/gitlab-svgs/illustrations/kubernetes-installation.svg';
+import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
+import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
+import helmLogo from 'images/cluster_app_logos/helm.png';
+import jeagerLogo from 'images/cluster_app_logos/jeager.png';
+import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
+import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
+import meltanoLogo from 'images/cluster_app_logos/meltano.png';
+import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
@@ -37,21 +46,21 @@ export default {
default: '',
},
},
+ data: () => ({
+ elasticsearchLogo,
+ gitlabLogo,
+ helmLogo,
+ jeagerLogo,
+ jupyterhubLogo,
+ kubernetesLogo,
+ meltanoLogo,
+ prometheusLogo,
+ }),
computed: {
- generalApplicationDescription() {
- return sprintf(
- _.escape(
- s__(
- `ClusterIntegration|Install applications on your Kubernetes cluster.
- Read more about %{helpLink}`,
- ),
- ),
- {
- helpLink: `<a href="${this.helpPath}">
- ${_.escape(s__('ClusterIntegration|installing applications'))}
- </a>`,
- },
- false,
+ helmInstalled() {
+ return (
+ this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
+ this.applications.helm.status === APPLICATION_STATUS.UPDATED
);
},
ingressId() {
@@ -128,224 +137,240 @@ export default {
return this.applications.jupyter.hostname;
},
},
+ created() {
+ this.helmInstallIllustration = helmInstallIllustration;
+ },
};
</script>
<template>
- <section
- id="cluster-applications"
- class="settings no-animate expanded"
- >
- <div class="settings-header">
- <h4>
- {{ s__('ClusterIntegration|Applications') }}
- </h4>
- <p
- class="append-bottom-0"
- v-html="generalApplicationDescription"
- >
- </p>
- </div>
+ <section id="cluster-applications">
+ <h4>
+ {{ s__('ClusterIntegration|Applications') }}
+ </h4>
+ <p class="append-bottom-0">
+ {{ s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.
+ Helm Tiller is required to install any of the following applications.`) }}
+ <a :href="helpPath">
+ {{ __('More information') }}
+ </a>
+ </p>
- <div class="settings-content">
- <div class="append-bottom-20">
- <application-row
- id="helm"
- :title="applications.helm.title"
- :status="applications.helm.status"
- :status-reason="applications.helm.statusReason"
- :request-status="applications.helm.requestStatus"
- :request-reason="applications.helm.requestReason"
- title-link="https://docs.helm.sh/"
- >
- <div slot="description">
- {{ s__(`ClusterIntegration|Helm streamlines installing
- and managing Kubernetes applications.
- Tiller runs inside of your Kubernetes Cluster,
- and manages releases of your charts.`) }}
- </div>
- </application-row>
- <application-row
- :id="ingressId"
- :title="applications.ingress.title"
- :status="applications.ingress.status"
- :status-reason="applications.ingress.statusReason"
- :request-status="applications.ingress.requestStatus"
- :request-reason="applications.ingress.requestReason"
- title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
+ <div class="cluster-application-list prepend-top-10">
+ <application-row
+ id="helm"
+ :logo-url="helmLogo"
+ :title="applications.helm.title"
+ :status="applications.helm.status"
+ :status-reason="applications.helm.statusReason"
+ :request-status="applications.helm.requestStatus"
+ :request-reason="applications.helm.requestReason"
+ class="rounded-top"
+ title-link="https://docs.helm.sh/"
+ >
+ <div slot="description">
+ {{ s__(`ClusterIntegration|Helm streamlines installing
+ and managing Kubernetes applications.
+ Tiller runs inside of your Kubernetes Cluster,
+ and manages releases of your charts.`) }}
+ </div>
+ </application-row>
+ <div
+ v-show="!helmInstalled"
+ class="cluster-application-warning"
+ >
+ <div
+ class="svg-container"
+ v-html="helmInstallIllustration"
>
- <div slot="description">
- <p>
- {{ s__(`ClusterIntegration|Ingress gives you a way to route
- requests to services based on the request host or path,
- centralizing a number of services into a single entrypoint.`) }}
- </p>
+ </div>
+ {{ s__(`ClusterIntegration|You must first install Helm Tiller before
+ installing the applications below`) }}
+ </div>
+ <application-row
+ :id="ingressId"
+ :logo-url="kubernetesLogo"
+ :title="applications.ingress.title"
+ :status="applications.ingress.status"
+ :status-reason="applications.ingress.statusReason"
+ :request-status="applications.ingress.requestStatus"
+ :request-reason="applications.ingress.requestReason"
+ :disabled="!helmInstalled"
+ title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
+ >
+ <div slot="description">
+ <p>
+ {{ s__(`ClusterIntegration|Ingress gives you a way to route
+ requests to services based on the request host or path,
+ centralizing a number of services into a single entrypoint.`) }}
+ </p>
- <template v-if="ingressInstalled">
- <div class="form-group">
- <label for="ingress-ip-address">
- {{ s__('ClusterIntegration|Ingress IP Address') }}
- </label>
- <div
- v-if="ingressExternalIp"
- class="input-group"
- >
- <input
- id="ingress-ip-address"
- :value="ingressExternalIp"
- type="text"
- class="form-control js-ip-address"
- readonly
- />
- <span class="input-group-append">
- <clipboard-button
- :text="ingressExternalIp"
- :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
- class="input-group-text js-clipboard-btn"
- />
- </span>
- </div>
+ <template v-if="ingressInstalled">
+ <div class="form-group">
+ <label for="ingress-ip-address">
+ {{ s__('ClusterIntegration|Ingress IP Address') }}
+ </label>
+ <div
+ v-if="ingressExternalIp"
+ class="input-group"
+ >
<input
- v-else
+ id="ingress-ip-address"
+ :value="ingressExternalIp"
type="text"
class="form-control js-ip-address"
readonly
- value="?"
/>
+ <span class="input-group-append">
+ <clipboard-button
+ :text="ingressExternalIp"
+ :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
+ class="input-group-text js-clipboard-btn"
+ />
+ </span>
</div>
+ <input
+ v-else
+ type="text"
+ class="form-control js-ip-address"
+ readonly
+ value="?"
+ />
+ </div>
- <p
- v-if="!ingressExternalIp"
- class="settings-message js-no-ip-message"
- >
- {{ s__(`ClusterIntegration|The IP address is in
- the process of being assigned. Please check your Kubernetes
- cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
+ <p
+ v-if="!ingressExternalIp"
+ class="settings-message js-no-ip-message"
+ >
+ {{ s__(`ClusterIntegration|The IP address is in
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
- <a
- :href="ingressHelpPath"
- target="_blank"
- rel="noopener noreferrer"
- >
- {{ __('More information') }}
- </a>
- </p>
+ <a
+ :href="ingressHelpPath"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
- <p>
- {{ s__(`ClusterIntegration|Point a wildcard DNS to this
- generated IP address in order to access
- your application after it has been deployed.`) }}
- <a
- :href="ingressDnsHelpPath"
- target="_blank"
- rel="noopener noreferrer"
- >
- {{ __('More information') }}
- </a>
- </p>
+ <p>
+ {{ s__(`ClusterIntegration|Point a wildcard DNS to this
+ generated IP address in order to access
+ your application after it has been deployed.`) }}
+ <a
+ :href="ingressDnsHelpPath"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
- </template>
- <div
- v-else
- v-html="ingressDescription"
- >
- </div>
- </div>
- </application-row>
- <application-row
- id="prometheus"
- :title="applications.prometheus.title"
- :manage-link="managePrometheusPath"
- :status="applications.prometheus.status"
- :status-reason="applications.prometheus.statusReason"
- :request-status="applications.prometheus.requestStatus"
- :request-reason="applications.prometheus.requestReason"
- title-link="https://prometheus.io/docs/introduction/overview/"
- >
+ </template>
<div
- slot="description"
- v-html="prometheusDescription"
+ v-html="ingressDescription"
>
</div>
- </application-row>
- <application-row
- id="runner"
- :title="applications.runner.title"
- :status="applications.runner.status"
- :status-reason="applications.runner.statusReason"
- :request-status="applications.runner.requestStatus"
- :request-reason="applications.runner.requestReason"
- title-link="https://docs.gitlab.com/runner/"
- >
- <div slot="description">
- {{ s__(`ClusterIntegration|GitLab Runner connects to this
- project's repository and executes CI/CD jobs,
- pushing results back and deploying,
- applications to production.`) }}
- </div>
- </application-row>
- <application-row
- id="jupyter"
- :title="applications.jupyter.title"
- :status="applications.jupyter.status"
- :status-reason="applications.jupyter.statusReason"
- :request-status="applications.jupyter.requestStatus"
- :request-reason="applications.jupyter.requestReason"
- :install-application-request-params="{ hostname: applications.jupyter.hostname }"
- title-link="https://jupyterhub.readthedocs.io/en/stable/"
+ </div>
+ </application-row>
+ <application-row
+ id="prometheus"
+ :logo-url="prometheusLogo"
+ :title="applications.prometheus.title"
+ :manage-link="managePrometheusPath"
+ :status="applications.prometheus.status"
+ :status-reason="applications.prometheus.statusReason"
+ :request-status="applications.prometheus.requestStatus"
+ :request-reason="applications.prometheus.requestReason"
+ :disabled="!helmInstalled"
+ title-link="https://prometheus.io/docs/introduction/overview/"
+ >
+ <div
+ slot="description"
+ v-html="prometheusDescription"
>
- <div slot="description">
- <p>
- {{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
- manages, and proxies multiple instances of the single-user
- Jupyter notebook server. JupyterHub can be used to serve
- notebooks to a class of students, a corporate data science group,
- or a scientific research group.`) }}
- </p>
+ </div>
+ </application-row>
+ <application-row
+ id="runner"
+ :logo-url="gitlabLogo"
+ :title="applications.runner.title"
+ :status="applications.runner.status"
+ :status-reason="applications.runner.statusReason"
+ :request-status="applications.runner.requestStatus"
+ :request-reason="applications.runner.requestReason"
+ :disabled="!helmInstalled"
+ title-link="https://docs.gitlab.com/runner/"
+ >
+ <div slot="description">
+ {{ s__(`ClusterIntegration|GitLab Runner connects to this
+ project's repository and executes CI/CD jobs,
+ pushing results back and deploying,
+ applications to production.`) }}
+ </div>
+ </application-row>
+ <application-row
+ id="jupyter"
+ :logo-url="jupyterhubLogo"
+ :title="applications.jupyter.title"
+ :status="applications.jupyter.status"
+ :status-reason="applications.jupyter.statusReason"
+ :request-status="applications.jupyter.requestStatus"
+ :request-reason="applications.jupyter.requestReason"
+ :install-application-request-params="{ hostname: applications.jupyter.hostname }"
+ :disabled="!helmInstalled"
+ class="hide-bottom-border rounded-bottom"
+ title-link="https://jupyterhub.readthedocs.io/en/stable/"
+ >
+ <div slot="description">
+ <p>
+ {{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
+ manages, and proxies multiple instances of the single-user
+ Jupyter notebook server. JupyterHub can be used to serve
+ notebooks to a class of students, a corporate data science group,
+ or a scientific research group.`) }}
+ </p>
- <template v-if="ingressExternalIp">
- <div class="form-group">
- <label for="jupyter-hostname">
- {{ s__('ClusterIntegration|Jupyter Hostname') }}
- </label>
+ <template v-if="ingressExternalIp">
+ <div class="form-group">
+ <label for="jupyter-hostname">
+ {{ s__('ClusterIntegration|Jupyter Hostname') }}
+ </label>
- <div class="input-group">
- <input
- v-model="applications.jupyter.hostname"
- :readonly="jupyterInstalled"
- type="text"
- class="form-control js-hostname"
+ <div class="input-group">
+ <input
+ v-model="applications.jupyter.hostname"
+ :readonly="jupyterInstalled"
+ type="text"
+ class="form-control js-hostname"
+ />
+ <span
+ class="input-group-btn"
+ >
+ <clipboard-button
+ :text="jupyterHostname"
+ :title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
+ class="js-clipboard-btn"
/>
- <span
- class="input-group-btn"
- >
- <clipboard-button
- :text="jupyterHostname"
- :title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
- class="js-clipboard-btn"
- />
- </span>
- </div>
+ </span>
</div>
- <p v-if="ingressInstalled">
- {{ s__(`ClusterIntegration|Replace this with your own hostname if you want.
- If you do so, point hostname to Ingress IP Address from above.`) }}
- <a
- :href="ingressDnsHelpPath"
- target="_blank"
- rel="noopener noreferrer"
- >
- {{ __('More information') }}
- </a>
- </p>
- </template>
- </div>
- </application-row>
- <!--
- NOTE: Don't forget to update `clusters.scss`
- min-height for this block and uncomment `application_spec` tests
- -->
- </div>
+ </div>
+ <p v-if="ingressInstalled">
+ {{ s__(`ClusterIntegration|Replace this with your own hostname if you want.
+ If you do so, point hostname to Ingress IP Address from above.`) }}
+ <a
+ :href="ingressDnsHelpPath"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
+ </template>
+ </div>
+ </application-row>
</div>
</section>
</template>
diff --git a/app/assets/javascripts/clusters/components/gcp_signup_offer.js b/app/assets/javascripts/clusters/components/gcp_signup_offer.js
deleted file mode 100644
index 8bc20a1c09f..00000000000
--- a/app/assets/javascripts/clusters/components/gcp_signup_offer.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import $ from 'jquery';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import Flash from '~/flash';
-
-export default function gcpSignupOffer() {
- const alertEl = document.querySelector('.gcp-signup-offer');
- if (!alertEl) {
- return;
- }
-
- const closeButtonEl = alertEl.getElementsByClassName('close')[0];
- const { dismissEndpoint, featureId } = closeButtonEl.dataset;
-
- closeButtonEl.addEventListener('click', () => {
- axios
- .post(dismissEndpoint, {
- feature_name: featureId,
- })
- .then(() => {
- $(alertEl).alert('close');
- })
- .catch(() => {
- Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
- });
- });
-}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 95c4be64d35..4849b0fa3db 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -76,10 +76,10 @@
<template>
<div class="content-list pipelines">
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
:label="s__('Pipelines|Loading Pipelines')"
- size="3"
+ :size="3"
class="prepend-top-20"
/>
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
index 923c036f5a4..1411f7ffd5e 100644
--- a/app/assets/javascripts/commons/gitlab_ui.js
+++ b/app/assets/javascripts/commons/gitlab_ui.js
@@ -1,4 +1,17 @@
import Vue from 'vue';
-import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar';
+import {
+ Pagination,
+ ProgressBar,
+ Modal,
+ LoadingIcon,
+ ModalDirective,
+ TooltipDirective,
+} from '@gitlab-org/gitlab-ui';
-Vue.component('gl-progress-bar', progressBar);
+Vue.component('gl-pagination', Pagination);
+Vue.component('gl-progress-bar', ProgressBar);
+Vue.component('gl-ui-modal', Modal);
+Vue.component('gl-loading-icon', LoadingIcon);
+
+Vue.directive('gl-modal', ModalDirective);
+Vue.directive('gl-tooltip', TooltipDirective);
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 742cf490ad2..539d0d29e0d 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -14,10 +14,10 @@ import 'core-js/es6/map';
import 'core-js/es6/weak-map';
// Browser polyfills
-import 'classlist-polyfill';
import 'formdata-polyfill';
import './polyfills/custom_event';
import './polyfills/element';
import './polyfills/event';
import './polyfills/nodelist';
import './polyfills/request_idle_callback';
+import './polyfills/svg';
diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js
index b593bde6aa2..dde5e8f54f9 100644
--- a/app/assets/javascripts/commons/polyfills/element.js
+++ b/app/assets/javascripts/commons/polyfills/element.js
@@ -1,12 +1,17 @@
-Element.prototype.closest = Element.prototype.closest ||
+// polyfill Element.classList and DOMTokenList with classList.js
+import 'classlist-polyfill';
+
+Element.prototype.closest =
+ Element.prototype.closest ||
function closest(selector, selectedElement = this) {
if (!selectedElement) return null;
- return selectedElement.matches(selector) ?
- selectedElement :
- Element.prototype.closest(selector, selectedElement.parentElement);
+ return selectedElement.matches(selector)
+ ? selectedElement
+ : Element.prototype.closest(selector, selectedElement.parentElement);
};
-Element.prototype.matches = Element.prototype.matches ||
+Element.prototype.matches =
+ Element.prototype.matches ||
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
@@ -15,13 +20,15 @@ Element.prototype.matches = Element.prototype.matches ||
function matches(selector) {
const elms = (this.document || this.ownerDocument).querySelectorAll(selector);
let i = elms.length - 1;
- while (i >= 0 && elms.item(i) !== this) { i -= 1; }
+ while (i >= 0 && elms.item(i) !== this) {
+ i -= 1;
+ }
return i > -1;
};
// From the polyfill on MDN, https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill
-((arr) => {
- arr.forEach((item) => {
+(arr => {
+ arr.forEach(item => {
if (Object.prototype.hasOwnProperty.call(item, 'remove')) {
return;
}
diff --git a/app/assets/javascripts/commons/polyfills/svg.js b/app/assets/javascripts/commons/polyfills/svg.js
new file mode 100644
index 00000000000..8648a568f6f
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills/svg.js
@@ -0,0 +1,5 @@
+import svg4everybody from 'svg4everybody';
+
+// polyfill support for external SVG file references via <use xlink:href>
+// @see https://css-tricks.com/svg-use-external-source/
+svg4everybody();
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index 7399fc97d45..10548da8ec5 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -1,11 +1,7 @@
<script>
-import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import eventHub from '../eventhub';
export default {
- components: {
- loadingIcon,
- },
props: {
deployKey: {
type: Object,
@@ -45,7 +41,7 @@ export default {
class="btn"
@click="doAction">
<slot></slot>
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
:inline="true"
/>
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index d91e4809126..aa52f120fe7 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,7 +1,6 @@
<script>
import { s__ } from '~/locale';
import Flash from '~/flash';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
@@ -11,7 +10,6 @@ import KeysPanel from './keys_panel.vue';
export default {
components: {
KeysPanel,
- LoadingIcon,
NavigationTabs,
},
props: {
@@ -114,10 +112,10 @@ export default {
<template>
<div class="append-bottom-default deploy-keys">
- <loading-icon
+ <gl-loading-icon
v-if="isLoading && !hasKeys"
:label="s__('DeployKeys|Loading deploy keys')"
- size="2"
+ :size="2"
/>
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index f66ca070445..c05b9b1de79 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -145,8 +145,8 @@ export default {
<icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
</a>
<a
- v-tooltip
v-if="isExpandable"
+ v-tooltip
:title="restProjectsTooltip"
class="label deploy-project-label"
@click="toggleExpanded"
@@ -154,10 +154,10 @@ export default {
<span>{{ restProjectsLabel }}</span>
</a>
<a
- v-tooltip
v-for="deployKeysProject in restProjects"
v-else-if="isExpanded"
:key="deployKeysProject.project.full_path"
+ v-tooltip
:href="deployKeysProject.project.full_path"
:title="projectTooltipTitle(deployKeysProject)"
class="label deploy-project-label"
@@ -198,8 +198,8 @@ export default {
{{ __('Enable') }}
</action-btn>
<a
- v-tooltip
v-if="deployKey.can_edit"
+ v-tooltip
:href="editDeployKeyPath"
:title="__('Edit')"
class="btn btn-default text-secondary"
@@ -208,8 +208,8 @@ export default {
<icon name="pencil"/>
</a>
<action-btn
- v-tooltip
v-if="isRemovable"
+ v-tooltip
:deploy-key="deployKey"
:title="__('Remove')"
btn-css-class="btn-danger"
@@ -219,8 +219,8 @@ export default {
<icon name="remove"/>
</action-btn>
<action-btn
- v-tooltip
v-else-if="isEnabled"
+ v-tooltip
:deploy-key="deployKey"
:title="__('Disable')"
btn-css-class="btn-warning"
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
index 5ed13488788..6fcad187b35 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
@@ -1,4 +1,4 @@
-/* eslint-disable object-shorthand, func-names, comma-dangle, no-else-return, quotes */
+/* eslint-disable object-shorthand, func-names, no-else-return */
/* global CommentsStore */
/* global ResolveService */
@@ -25,44 +25,44 @@ const ResolveDiscussionBtn = Vue.extend({
};
},
computed: {
- showButton: function () {
+ showButton: function() {
if (this.discussion) {
return this.discussion.isResolvable();
} else {
return false;
}
},
- isDiscussionResolved: function () {
+ isDiscussionResolved: function() {
if (this.discussion) {
return this.discussion.isResolved();
} else {
return false;
}
},
- buttonText: function () {
+ buttonText: function() {
if (this.isDiscussionResolved) {
- return "Unresolve discussion";
+ return 'Unresolve discussion';
} else {
- return "Resolve discussion";
+ return 'Resolve discussion';
}
},
- loading: function () {
+ loading: function() {
if (this.discussion) {
return this.discussion.loading;
} else {
return false;
}
- }
+ },
},
- created: function () {
+ created: function() {
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
this.discussion = CommentsStore.state[this.discussionId];
},
methods: {
- resolve: function () {
+ resolve: function() {
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
- }
+ },
},
});
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index 0b3568e432d..e69eaad4423 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -8,9 +8,7 @@ window.gl = window.gl || {};
class ResolveServiceClass {
constructor(root) {
- this.noteResource = Vue.resource(
- `${root}/notes{/noteId}/resolve?html=true`,
- );
+ this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
this.discussionResource = Vue.resource(
`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
);
@@ -51,10 +49,7 @@ class ResolveServiceClass {
discussion.updateHeadline(data);
})
.catch(
- () =>
- new Flash(
- 'An error occurred when trying to resolve a discussion. Please try again.',
- ),
+ () => new Flash('An error occurred when trying to resolve a discussion. Please try again.'),
);
}
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index b5b05df4d34..edca45f22f9 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -4,23 +4,23 @@ import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import createFlash from '~/flash';
import eventHub from '../../notes/event_hub';
-import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import CompareVersions from './compare_versions.vue';
-import ChangedFiles from './changed_files.vue';
import DiffFile from './diff_file.vue';
import NoChanges from './no_changes.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
+import CommitWidget from './commit_widget.vue';
+import TreeList from './tree_list.vue';
export default {
name: 'DiffsApp',
components: {
Icon,
- LoadingIcon,
CompareVersions,
- ChangedFiles,
DiffFile,
NoChanges,
HiddenFilesWarning,
+ CommitWidget,
+ TreeList,
},
props: {
endpoint: {
@@ -58,8 +58,9 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
+ ...mapState('diffs', ['showTreeList']),
...mapGetters('diffs', ['isParallelView']),
- ...mapGetters(['isNotesFetched']),
+ ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
targetBranch() {
return {
branchName: this.targetBranchName,
@@ -88,6 +89,9 @@ export default {
canCurrentUserFork() {
return this.currentUser.canFork === true && this.currentUser.canCreateMergeRequest;
},
+ showCompareVersions() {
+ return this.mergeRequestDiffs && this.mergeRequestDiff;
+ },
},
watch: {
diffViewType() {
@@ -102,6 +106,8 @@ export default {
this.adjustView();
},
+ isLoading: 'adjustView',
+ showTreeList: 'adjustView',
},
mounted() {
this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath });
@@ -112,13 +118,25 @@ export default {
},
created() {
this.adjustView();
+ eventHub.$once('fetchedNotesData', this.setDiscussions);
},
methods: {
- ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles', 'startRenderDiffsQueue']),
+ ...mapActions('diffs', [
+ 'setBaseConfig',
+ 'fetchDiffFiles',
+ 'startRenderDiffsQueue',
+ 'assignDiscussionsToDiff',
+ ]),
fetchData() {
this.fetchDiffFiles()
.then(() => {
- requestIdleCallback(this.startRenderDiffsQueue, { timeout: 1000 });
+ requestIdleCallback(
+ () => {
+ this.setDiscussions();
+ this.startRenderDiffsQueue();
+ },
+ { timeout: 1000 },
+ );
})
.catch(() => {
createFlash(__('Something went wrong on our end. Please try again!'));
@@ -128,11 +146,22 @@ export default {
eventHub.$emit('fetchNotesData');
}
},
+ setDiscussions() {
+ if (this.isNotesFetched) {
+ requestIdleCallback(
+ () => {
+ this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
+ },
+ { timeout: 1000 },
+ );
+ }
+ },
adjustView() {
- if (this.shouldShow && this.isParallelView) {
- window.mrTabs.expandViewContainer();
- } else {
- window.mrTabs.resetViewContainer();
+ if (this.shouldShow) {
+ this.$nextTick(() => {
+ window.mrTabs.resetViewContainer();
+ window.mrTabs.expandViewContainer(this.showTreeList);
+ });
}
},
},
@@ -145,7 +174,7 @@ export default {
v-if="isLoading"
class="loading"
>
- <loading-icon />
+ <gl-loading-icon />
</div>
<div
v-else
@@ -154,7 +183,7 @@ export default {
class="diffs tab-pane"
>
<compare-versions
- v-if="!commit && mergeRequestDiffs.length > 1"
+ v-if="showCompareVersions"
:merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff"
:start-version="startVersion"
@@ -187,22 +216,31 @@ export default {
</div>
</div>
- <changed-files
- :diff-files="diffFiles"
+ <commit-widget
+ v-if="commit"
+ :commit="commit"
/>
- <div
- v-if="diffFiles.length > 0"
- class="files"
- >
- <diff-file
- v-for="file in diffFiles"
- :key="file.newPath"
- :file="file"
- :can-current-user-fork="canCurrentUserFork"
- />
+ <div class="files d-flex prepend-top-default">
+ <div
+ v-show="showTreeList"
+ class="diff-tree-list"
+ >
+ <tree-list />
+ </div>
+ <div
+ v-if="diffFiles.length > 0"
+ class="diff-files-holder"
+ >
+ <diff-file
+ v-for="file in diffFiles"
+ :key="file.newPath"
+ :file="file"
+ :can-current-user-fork="canCurrentUserFork"
+ />
+ </div>
+ <no-changes v-else />
</div>
- <no-changes v-else />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue
deleted file mode 100644
index 97751db1254..00000000000
--- a/app/assets/javascripts/diffs/components/changed_files.vue
+++ /dev/null
@@ -1,171 +0,0 @@
-<script>
-import { mapGetters, mapActions } from 'vuex';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
-import { pluralize } from '~/lib/utils/text_utility';
-import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
-import { contentTop } from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
-import ChangedFilesDropdown from './changed_files_dropdown.vue';
-import changedFilesMixin from '../mixins/changed_files';
-
-export default {
- components: {
- Icon,
- ChangedFilesDropdown,
- ClipboardButton,
- },
- mixins: [changedFilesMixin],
- data() {
- return {
- isStuck: false,
- maxWidth: 'auto',
- offsetTop: 0,
- };
- },
- computed: {
- ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
- sumAddedLines() {
- return this.sumValues('addedLines');
- },
- sumRemovedLines() {
- return this.sumValues('removedLines');
- },
- whitespaceVisible() {
- return !getParameterValues('w')[0];
- },
- toggleWhitespaceText() {
- if (this.whitespaceVisible) {
- return __('Hide whitespace changes');
- }
- return __('Show whitespace changes');
- },
- toggleWhitespacePath() {
- if (this.whitespaceVisible) {
- return mergeUrlParams({ w: 1 }, window.location.href);
- }
-
- return mergeUrlParams({ w: 0 }, window.location.href);
- },
- top() {
- return `${this.offsetTop}px`;
- },
- },
- created() {
- document.addEventListener('scroll', this.handleScroll);
- this.offsetTop = contentTop();
- },
- beforeDestroy() {
- document.removeEventListener('scroll', this.handleScroll);
- },
- methods: {
- ...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
- pluralize,
- handleScroll() {
- if (!this.updating) {
- this.$nextTick(this.updateIsStuck);
- this.updating = true;
- }
- },
- updateIsStuck() {
- if (!this.$refs.wrapper) {
- return;
- }
-
- const scrollPosition = window.scrollY;
-
- this.isStuck = scrollPosition + this.offsetTop >= this.$refs.placeholder.offsetTop;
- this.updating = false;
- },
- sumValues(key) {
- return this.diffFiles.reduce((total, file) => total + file[key], 0);
- },
- },
-};
-</script>
-
-<template>
- <span>
- <div ref="placeholder"></div>
- <div
- ref="wrapper"
- :style="{ top }"
- :class="{'is-stuck': isStuck}"
- class="content-block oneline-block diff-files-changed diff-files-changed-merge-request
- files-changed js-diff-files-changed"
- >
- <div class="files-changed-inner">
- <div
- class="inline-parallel-buttons d-none d-md-block"
- >
- <a
- v-if="areAllFilesCollapsed"
- class="btn btn-default"
- @click="expandAllFiles"
- >
- {{ __('Expand all') }}
- </a>
- <a
- :href="toggleWhitespacePath"
- class="btn btn-default"
- >
- {{ toggleWhitespaceText }}
- </a>
- <div class="btn-group">
- <button
- id="inline-diff-btn"
- :class="{ active: isInlineView }"
- type="button"
- class="btn js-inline-diff-button"
- data-view-type="inline"
- @click="setInlineDiffViewType"
- >
- {{ __('Inline') }}
- </button>
- <button
- id="parallel-diff-btn"
- :class="{ active: isParallelView }"
- type="button"
- class="btn js-parallel-diff-button"
- data-view-type="parallel"
- @click="setParallelDiffViewType"
- >
- {{ __('Side-by-side') }}
- </button>
- </div>
- </div>
-
- <div class="commit-stat-summary dropdown">
- <changed-files-dropdown
- :diff-files="diffFiles"
- />
-
- <span
- class="js-diff-stats-additions-deletions-expanded
- diff-stats-additions-deletions-expanded"
- >
- with
- <strong class="cgreen">
- {{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }}
- </strong>
- and
- <strong class="cred">
- {{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
- </strong>
- </span>
- <div
- class="js-diff-stats-additions-deletions-collapsed
- diff-stats-additions-deletions-collapsed float-right d-sm-none"
- >
- <strong class="cgreen">
- +{{ sumAddedLines }}
- </strong>
- <strong class="cred">
- -{{ sumRemovedLines }}
- </strong>
- </div>
- </div>
- </div>
- </div>
- </span>
-</template>
diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
deleted file mode 100644
index 045688a32bf..00000000000
--- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-<script>
-import Icon from '~/vue_shared/components/icon.vue';
-import changedFilesMixin from '../mixins/changed_files';
-
-export default {
- components: {
- Icon,
- },
- mixins: [changedFilesMixin],
- data() {
- return {
- searchText: '',
- };
- },
- computed: {
- filteredDiffFiles() {
- return this.diffFiles.filter(file =>
- file.filePath.toLowerCase().includes(this.searchText.toLowerCase()),
- );
- },
- },
- methods: {
- clearSearch() {
- this.searchText = '';
- },
- },
-};
-</script>
-
-<template>
- <span>
- Showing
- <button
- class="diff-stats-summary-toggler"
- data-toggle="dropdown"
- type="button"
- aria-expanded="false"
- >
- <span>
- {{ n__('%d changed file', '%d changed files', diffFiles.length) }}
- </span>
- <icon
- class="caret-icon"
- name="chevron-down"
- />
- </button>
- <div class="dropdown-menu diff-file-changes">
- <div class="dropdown-input">
- <input
- v-model="searchText"
- type="search"
- class="dropdown-input-field"
- placeholder="Search files"
- autocomplete="off"
- />
- <i
- v-if="searchText.length === 0"
- aria-hidden="true"
- data-hidden="true"
- class="fa fa-search dropdown-input-search">
- </i>
- <i
- v-else
- role="button"
- class="fa fa-times dropdown-input-search"
- @click="clearSearch"
- ></i>
- </div>
- <div class="dropdown-content">
- <ul>
- <li
- v-for="diffFile in filteredDiffFiles"
- :key="diffFile.name"
- >
- <a
- :href="`#${diffFile.fileHash}`"
- :title="diffFile.newPath"
- class="diff-changed-file"
- >
- <icon
- :name="fileChangedIcon(diffFile)"
- :size="16"
- :class="fileChangedClass(diffFile)"
- class="diff-file-changed-icon append-right-8"
- />
- <span class="diff-changed-file-content append-right-8">
- <strong
- v-if="diffFile.blob && diffFile.blob.name"
- class="diff-changed-file-name"
- >
- {{ diffFile.blob.name }}
- </strong>
- <strong
- v-else
- class="diff-changed-blank-file-name"
- >
- {{ s__('Diffs|No file name available') }}
- </strong>
- <span class="diff-changed-file-path prepend-top-5">
- {{ truncatedDiffPath(diffFile.blob.path) }}
- </span>
- </span>
- <span class="diff-changed-stats">
- <span class="cgreen">
- +{{ diffFile.addedLines }}
- </span>
- <span class="cred">
- -{{ diffFile.removedLines }}
- </span>
- </span>
- </a>
- </li>
-
- <li
- v-show="filteredDiffFiles.length === 0"
- class="dropdown-menu-empty-item"
- >
- <a>
- {{ __('No files found') }}
- </a>
- </li>
- </ul>
- </div>
- </div>
- </span>
-</template>
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
new file mode 100644
index 00000000000..993206b2e73
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -0,0 +1,129 @@
+<script>
+import tooltip from '~/vue_shared/directives/tooltip';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import CIIcon from '~/vue_shared/components/ci_icon.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+
+/**
+ * CommitItem
+ *
+ * -----------------------------------------------------------------
+ * WARNING: Please keep changes up-to-date with the following files:
+ * - `views/projects/commits/_commit.html.haml`
+ * -----------------------------------------------------------------
+ *
+ * This Component was cloned from a HAML view. For the time being they
+ * coexist, but there is an issue to remove the duplication.
+ * https://gitlab.com/gitlab-org/gitlab-ce/issues/51613
+ *
+ */
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ UserAvatarLink,
+ Icon,
+ ClipboardButton,
+ CIIcon,
+ TimeAgoTooltip,
+ CommitPipelineStatus,
+ },
+ props: {
+ commit: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ authorName() {
+ return (this.commit.author && this.commit.author.name) || this.commit.authorName;
+ },
+ authorUrl() {
+ return (this.commit.author && this.commit.author.webUrl) || `mailto:${this.commit.authorEmail}`;
+ },
+ authorAvatar() {
+ return (this.commit.author && this.commit.author.avatarUrl) || this.commit.authorGravatarUrl;
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="commit flex-row js-toggle-container">
+ <user-avatar-link
+ :link-href="authorUrl"
+ :img-src="authorAvatar"
+ :img-alt="authorName"
+ :img-size="36"
+ class="avatar-cell d-none d-sm-block"
+ />
+ <div class="commit-detail flex-list">
+ <div class="commit-content qa-commit-content">
+ <a
+ :href="commit.commitUrl"
+ class="commit-row-message item-title"
+ v-html="commit.titleHtml"
+ ></a>
+
+ <span class="commit-row-message d-block d-sm-none">
+ &middot;
+ {{ commit.shortId }}
+ </span>
+
+ <button
+ v-if="commit.descriptionHtml"
+ class="text-expander js-toggle-button"
+ type="button"
+ :aria-label="__('Toggle commit description')"
+ >
+ <icon
+ :size="12"
+ name="ellipsis_h"
+ />
+ </button>
+
+ <div class="commiter">
+ <a
+ :href="authorUrl"
+ v-text="authorName"
+ ></a>
+ {{ s__('CommitWidget|authored') }}
+ <time-ago-tooltip
+ :time="commit.authoredDate"
+ />
+ </div>
+
+ <pre
+ v-if="commit.descriptionHtml"
+ class="commit-row-description js-toggle-content append-bottom-8"
+ v-html="commit.descriptionHtml"
+ ></pre>
+ </div>
+ <div class="commit-actions flex-row d-none d-sm-flex">
+ <div
+ v-if="commit.signatureHtml"
+ v-html="commit.signatureHtml"
+ ></div>
+ <commit-pipeline-status
+ v-if="commit.pipelineStatusPath"
+ :endpoint="commit.pipelineStatusPath"
+ />
+ <div class="commit-sha-group">
+ <div
+ class="label label-monospace"
+ v-text="commit.shortId"
+ ></div>
+ <clipboard-button
+ :text="commit.id"
+ :title="__('Copy commit SHA to clipboard')"
+ class="btn btn-default"
+ />
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/diffs/components/commit_widget.vue b/app/assets/javascripts/diffs/components/commit_widget.vue
new file mode 100644
index 00000000000..cc8e72eb1c8
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/commit_widget.vue
@@ -0,0 +1,40 @@
+<script>
+import CommitItem from './commit_item.vue';
+
+/**
+ * CommitWidget
+ *
+ * -----------------------------------------------------------------
+ * WARNING: Please keep changes up-to-date with the following files:
+ * - `views/projects/merge_requests/diffs/_commit_widget.html.haml`
+ * -----------------------------------------------------------------
+ *
+ * This Component was cloned from a HAML view. For the time being,
+ * they coexist, but there is an issue to remove the duplication.
+ * https://gitlab.com/gitlab-org/gitlab-ce/issues/51613
+ *
+ */
+export default {
+ components: {
+ CommitItem,
+ },
+ props: {
+ commit: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="info-well prepend-top-default">
+ <div class="well-segment">
+ <ul class="blob-commit-info">
+ <commit-item
+ :commit="commit"
+ />
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 1c9ad8e77f1..9bbf62c0eb6 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -1,9 +1,18 @@
<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Tooltip from '@gitlab-org/gitlab-ui/dist/directives/tooltip';
+import { __ } from '~/locale';
+import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
+import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
export default {
components: {
CompareVersionsDropdown,
+ Icon,
+ },
+ directives: {
+ Tooltip,
},
props: {
mergeRequestDiffs: {
@@ -26,30 +35,119 @@ export default {
},
},
computed: {
+ ...mapState('diffs', ['commit', 'showTreeList']),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
+ isWhitespaceVisible() {
+ return !getParameterValues('w')[0];
+ },
+ toggleWhitespaceText() {
+ if (this.isWhitespaceVisible) {
+ return __('Hide whitespace changes');
+ }
+ return __('Show whitespace changes');
+ },
+ toggleWhitespacePath() {
+ if (this.isWhitespaceVisible) {
+ return mergeUrlParams({ w: 1 }, window.location.href);
+ }
+
+ return mergeUrlParams({ w: 0 }, window.location.href);
+ },
+ showDropdowns() {
+ return !this.commit && this.mergeRequestDiffs.length;
+ },
+ },
+ methods: {
+ ...mapActions('diffs', [
+ 'setInlineDiffViewType',
+ 'setParallelDiffViewType',
+ 'expandAllFiles',
+ 'toggleShowTreeList',
+ ]),
},
};
</script>
<template>
<div class="mr-version-controls">
- <div class="mr-version-menus-container content-block">
- Changes between
- <compare-versions-dropdown
- :other-versions="mergeRequestDiffs"
- :merge-request-version="mergeRequestDiff"
- :show-commit-count="true"
- class="mr-version-dropdown"
- />
- and
- <compare-versions-dropdown
- :other-versions="comparableDiffs"
- :start-version="startVersion"
- :target-branch="targetBranch"
- class="mr-version-compare-dropdown"
- />
+ <div
+ class="mr-version-menus-container content-block"
+ >
+ <button
+ v-tooltip.hover
+ type="button"
+ class="btn btn-default append-right-8 js-toggle-tree-list"
+ :class="{
+ active: showTreeList
+ }"
+ :title="__('Toggle file browser')"
+ @click="toggleShowTreeList"
+ >
+ <icon
+ name="hamburger"
+ />
+ </button>
+ <div
+ v-if="showDropdowns"
+ class="d-flex align-items-center compare-versions-container"
+ >
+ Changes between
+ <compare-versions-dropdown
+ :other-versions="mergeRequestDiffs"
+ :merge-request-version="mergeRequestDiff"
+ :show-commit-count="true"
+ class="mr-version-dropdown"
+ />
+ and
+ <compare-versions-dropdown
+ :other-versions="comparableDiffs"
+ :start-version="startVersion"
+ :target-branch="targetBranch"
+ class="mr-version-compare-dropdown"
+ />
+ </div>
+ <div
+ class="inline-parallel-buttons d-none d-md-flex ml-auto"
+ >
+ <a
+ v-if="areAllFilesCollapsed"
+ class="btn btn-default"
+ @click="expandAllFiles"
+ >
+ {{ __('Expand all') }}
+ </a>
+ <a
+ :href="toggleWhitespacePath"
+ class="btn btn-default"
+ >
+ {{ toggleWhitespaceText }}
+ </a>
+ <div class="btn-group prepend-left-8">
+ <button
+ id="inline-diff-btn"
+ :class="{ active: isInlineView }"
+ type="button"
+ class="btn js-inline-diff-button"
+ data-view-type="inline"
+ @click="setInlineDiffViewType"
+ >
+ {{ __('Inline') }}
+ </button>
+ <button
+ id="parallel-diff-btn"
+ :class="{ active: isParallelView }"
+ type="button"
+ class="btn js-parallel-diff-button"
+ data-view-type="parallel"
+ @click="setParallelDiffViewType"
+ >
+ {{ __('Side-by-side') }}
+ </button>
+ </div>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
index 96cccb49378..c3acc352d5e 100644
--- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -108,7 +108,7 @@ export default {
<template>
<span class="dropdown inline">
<a
- class="dropdown-toggle btn btn-default"
+ class="dropdown-menu-toggle btn btn-default w-100"
data-toggle="dropdown"
aria-expanded="false"
>
@@ -118,6 +118,7 @@ export default {
<Icon
:size="12"
name="angle-down"
+ class="position-absolute"
/>
</a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
@@ -163,3 +164,10 @@ export default {
</div>
</span>
</template>
+
+<style>
+.dropdown {
+ min-width: 0;
+ max-height: 170px;
+}
+</style>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 02d5be1821b..fb5556e3cd7 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -28,7 +28,7 @@ export default {
return diffModes[diffModeKey] || diffModes.replaced;
},
isTextFile() {
- return this.diffFile.text;
+ return this.diffFile.viewer.name === 'text';
},
},
};
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index e64d5511d78..cddbe554fbd 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -1,4 +1,5 @@
<script>
+import { mapActions } from 'vuex';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default {
@@ -11,6 +12,14 @@ export default {
required: true,
},
},
+ methods: {
+ ...mapActions('diffs', ['removeDiscussionsFromDiff']),
+ deleteNoteHandler(discussion) {
+ if (discussion.notes.length <= 1) {
+ this.removeDiscussionsFromDiff(discussion);
+ }
+ },
+ },
};
</script>
@@ -31,6 +40,7 @@ export default {
:render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true"
+ @noteDeleted="deleteNoteHandler"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 59e9ba08b8b..4e04e50c52a 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,9 +1,8 @@
<script>
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
@@ -11,7 +10,6 @@ export default {
components: {
DiffFileHeader,
DiffContent,
- LoadingIcon,
},
props: {
file: {
@@ -30,6 +28,8 @@ export default {
};
},
computed: {
+ ...mapState('diffs', ['currentDiffFileId']),
+ ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
isCollapsed() {
return this.file.collapsed || false;
},
@@ -44,23 +44,23 @@ export default {
);
},
showExpandMessage() {
- return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge;
+ return (
+ this.isCollapsed ||
+ !this.file.highlightedDiffLines &&
+ !this.isLoadingCollapsedDiff &&
+ !this.file.tooLarge &&
+ this.file.text
+ );
},
showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
},
},
methods: {
- ...mapActions('diffs', ['loadCollapsedDiff']),
+ ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']),
handleToggle() {
- const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file;
-
- if (
- collapsed &&
- !highlightedDiffLines &&
- parallelDiffLines !== undefined &&
- !parallelDiffLines.length
- ) {
+ const { highlightedDiffLines, parallelDiffLines } = this.file;
+ if (!highlightedDiffLines && parallelDiffLines !== undefined && !parallelDiffLines.length) {
this.handleLoadCollapsedDiff();
} else {
this.file.collapsed = !this.file.collapsed;
@@ -76,6 +76,14 @@ export default {
this.file.collapsed = false;
this.file.renderIt = true;
})
+ .then(() => {
+ requestIdleCallback(
+ () => {
+ this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
+ },
+ { timeout: 1000 },
+ );
+ })
.catch(() => {
this.isLoadingCollapsedDiff = false;
createFlash(__('Something went wrong on our end. Please try again!'));
@@ -94,6 +102,9 @@ export default {
<template>
<div
:id="file.fileHash"
+ :class="{
+ 'is-active': currentDiffFileId === file.fileHash
+ }"
class="diff-file file-holder"
>
<diff-file-header
@@ -135,12 +146,12 @@ export default {
:class="{ hidden: isCollapsed || file.tooLarge }"
:diff-file="file"
/>
- <loading-icon
- v-else-if="showLoadingIcon"
+ <gl-loading-icon
+ v-if="showLoadingIcon"
class="diff-content loading"
/>
<div
- v-if="showExpandMessage"
+ v-else-if="showExpandMessage"
class="nothing-here-block diff-collapsed"
>
{{ __('This diff is collapsed.') }}
@@ -161,3 +172,20 @@ export default {
</div>
</div>
</template>
+
+<style>
+@keyframes shadow-fade {
+ from {
+ box-shadow: 0 0 4px #919191;
+ }
+
+ to {
+ box-shadow: 0 0 0 #dfdfdf;
+ }
+}
+
+.diff-file.is-active {
+ box-shadow: 0 0 0 #dfdfdf;
+ animation: shadow-fade 1.2s 0.1s 1;
+}
+</style>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index d3ffbe0415a..15b37243030 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -166,23 +166,21 @@ export default {
:title="diffFile.oldPath"
class="file-title-name"
data-container="body"
- >
- {{ diffFile.oldPath }}
- </strong>
+ v-html="diffFile.oldPathHtml"
+ ></strong>
→
<strong
v-tooltip
:title="diffFile.newPath"
class="file-title-name"
data-container="body"
- >
- {{ diffFile.newPath }}
- </strong>
+ v-html="diffFile.newPathHtml"
+ ></strong>
</span>
<strong
- v-tooltip
v-else
+ v-tooltip
:title="filePath"
class="file-title-name"
data-container="body"
@@ -255,8 +253,8 @@ export default {
</a>
<a
- v-tooltip
v-if="diffFile.externalUrl"
+ v-tooltip
:href="diffFile.externalUrl"
:title="`View on ${diffFile.formattedExternalUrl}`"
target="_blank"
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 7e50a0aed84..1b59777f901 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -1,15 +1,11 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import { pluralize, truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
export default {
- directives: {
- tooltip,
- },
components: {
Icon,
UserAvatarImage,
@@ -91,10 +87,10 @@ export default {
@click.native="toggleDiscussions"
/>
<span
- v-tooltip
v-if="moreText"
+ v-gl-tooltip
:title="moreText"
- class="diff-comments-more-count has-tooltip js-diff-comment-avatar js-diff-comment-plus"
+ class="diff-comments-more-count js-diff-comment-avatar js-diff-comment-plus"
data-container="body"
data-placement="top"
role="button"
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index 8ad1ea34245..6eff3013dcd 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -13,6 +13,10 @@ export default {
Icon,
},
props: {
+ line: {
+ type: Object,
+ required: true,
+ },
fileHash: {
type: String,
required: true,
@@ -21,31 +25,16 @@ export default {
type: String,
required: true,
},
- lineType: {
- type: String,
- required: false,
- default: '',
- },
lineNumber: {
type: Number,
required: false,
default: 0,
},
- lineCode: {
- type: String,
- required: false,
- default: '',
- },
linePosition: {
type: String,
required: false,
default: '',
},
- metaData: {
- type: Object,
- required: false,
- default: () => ({}),
- },
showCommentButton: {
type: Boolean,
required: false,
@@ -76,11 +65,6 @@ export default {
required: false,
default: false,
},
- discussions: {
- type: Array,
- required: false,
- default: () => [],
- },
},
computed: {
...mapState({
@@ -89,7 +73,7 @@ export default {
}),
...mapGetters(['isLoggedIn']),
lineHref() {
- return this.lineCode ? `#${this.lineCode}` : '#';
+ return `#${this.line.lineCode || ''}`;
},
shouldShowCommentButton() {
return (
@@ -103,20 +87,19 @@ export default {
);
},
hasDiscussions() {
- return this.discussions.length > 0;
+ return this.line.discussions && this.line.discussions.length > 0;
},
shouldShowAvatarsOnGutter() {
- if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
+ if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) {
return false;
}
-
return this.showCommentButton && this.hasDiscussions;
},
},
methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
handleCommentButton() {
- this.showCommentForm({ lineCode: this.lineCode });
+ this.showCommentForm({ lineCode: this.line.lineCode });
},
handleLoadMoreLines() {
if (this.isRequesting) {
@@ -125,8 +108,8 @@ export default {
this.isRequesting = true;
const endpoint = this.contextLinesPath;
- const oldLineNumber = this.metaData.oldPos || 0;
- const newLineNumber = this.metaData.newPos || 0;
+ const oldLineNumber = this.line.metaData.oldPos || 0;
+ const newLineNumber = this.line.metaData.newPos || 0;
const offset = newLineNumber - oldLineNumber;
const bottom = this.isBottom;
const { fileHash } = this;
@@ -201,7 +184,7 @@ export default {
</a>
<diff-gutter-avatars
v-if="shouldShowAvatarsOnGutter"
- :discussions="discussions"
+ :discussions="line.discussions"
/>
</template>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index cbe4551d06b..bb9bb821de3 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -1,9 +1,7 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import createFlash from '~/flash';
import { s__ } from '~/locale';
import noteForm from '../../notes/components/note_form.vue';
-import { getNoteFormData } from '../store/utils';
import autosave from '../../notes/mixins/autosave';
import { DIFF_NOTE_TYPE } from '../constants';
@@ -21,7 +19,7 @@ export default {
type: Object,
required: true,
},
- position: {
+ linePosition: {
type: String,
required: false,
default: '',
@@ -38,6 +36,16 @@ export default {
}),
...mapGetters('diffs', ['getDiffFileByHash']),
...mapGetters(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']),
+ formData() {
+ return {
+ noteableData: this.noteableData,
+ noteableType: this.noteableType,
+ noteTargetLine: this.noteTargetLine,
+ diffViewType: this.diffViewType,
+ diffFile: this.getDiffFileByHash(this.diffFileHash),
+ linePosition: this.linePosition,
+ };
+ },
},
mounted() {
if (this.isLoggedIn) {
@@ -52,8 +60,7 @@ export default {
}
},
methods: {
- ...mapActions('diffs', ['cancelCommentForm']),
- ...mapActions(['saveNote', 'refetchDiscussionById']),
+ ...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff', 'saveDiffDiscussion']),
handleCancelCommentForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
@@ -72,32 +79,9 @@ export default {
});
},
handleSaveNote(note) {
- const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash);
- const postData = getNoteFormData({
- note,
- noteableData: this.noteableData,
- noteableType: this.noteableType,
- noteTargetLine: this.noteTargetLine,
- diffViewType: this.diffViewType,
- diffFile: selectedDiffFile,
- linePosition: this.position,
- });
-
- this.saveNote(postData)
- .then(result => {
- const endpoint = this.getNotesDataByProp('discussionsPath');
-
- this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
- .then(() => {
- this.handleCancelCommentForm();
- })
- .catch(() => {
- createFlash(s__('MergeRequests|Updating discussions failed'));
- });
- })
- .catch(() => {
- createFlash(s__('MergeRequests|Saving the comment failed'));
- });
+ return this.saveDiffDiscussion({ note, formData: this.formData }).then(() =>
+ this.handleCancelCommentForm(),
+ );
},
},
};
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index 33bc8d9971e..5d9a0b123fe 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -11,8 +11,6 @@ import {
LINE_HOVER_CLASS_NAME,
LINE_UNFOLD_CLASS_NAME,
INLINE_DIFF_VIEW_TYPE,
- LINE_POSITION_LEFT,
- LINE_POSITION_RIGHT,
} from '../constants';
export default {
@@ -67,42 +65,24 @@ export default {
required: false,
default: false,
},
- discussions: {
- type: Array,
- required: false,
- default: () => [],
- },
},
computed: {
...mapGetters(['isLoggedIn']),
- normalizedLine() {
- let normalizedLine;
-
- if (this.diffViewType === INLINE_DIFF_VIEW_TYPE) {
- normalizedLine = this.line;
- } else if (this.linePosition === LINE_POSITION_LEFT) {
- normalizedLine = this.line.left;
- } else if (this.linePosition === LINE_POSITION_RIGHT) {
- normalizedLine = this.line.right;
- }
-
- return normalizedLine;
- },
isMatchLine() {
- return this.normalizedLine.type === MATCH_LINE_TYPE;
+ return this.line.type === MATCH_LINE_TYPE;
},
isContextLine() {
- return this.normalizedLine.type === CONTEXT_LINE_TYPE;
+ return this.line.type === CONTEXT_LINE_TYPE;
},
isMetaLine() {
- const { type } = this.normalizedLine;
+ const { type } = this.line;
return (
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
);
},
classNameMap() {
- const { type } = this.normalizedLine;
+ const { type } = this.line;
return {
[type]: type,
@@ -116,9 +96,9 @@ export default {
};
},
lineNumber() {
- const { lineType, normalizedLine } = this;
+ const { lineType } = this;
- return lineType === OLD_LINE_TYPE ? normalizedLine.oldLine : normalizedLine.newLine;
+ return lineType === OLD_LINE_TYPE ? this.line.oldLine : this.line.newLine;
},
},
};
@@ -129,20 +109,17 @@ export default {
:class="classNameMap"
>
<diff-line-gutter-content
+ :line="line"
:file-hash="fileHash"
:context-lines-path="contextLinesPath"
- :line-type="normalizedLine.type"
- :line-code="normalizedLine.lineCode"
:line-position="linePosition"
:line-number="lineNumber"
- :meta-data="normalizedLine.metaData"
:show-comment-button="showCommentButton"
:is-hover="isHover"
:is-bottom="isBottom"
:is-match-line="isMatchLine"
:is-context-line="isContentLine"
:is-meta-line="isMetaLine"
- :discussions="discussions"
/>
</td>
</template>
diff --git a/app/assets/javascripts/diffs/components/file_row_stats.vue b/app/assets/javascripts/diffs/components/file_row_stats.vue
new file mode 100644
index 00000000000..105f7ebdbed
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/file_row_stats.vue
@@ -0,0 +1,30 @@
+<script>
+export default {
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <span
+ v-once
+ class="file-row-stats"
+ >
+ <span class="cgreen">
+ +{{ file.addedLines }}
+ </span>
+ <span class="cred">
+ -{{ file.removedLines }}
+ </span>
+ </span>
+</template>
+
+<style>
+.file-row-stats {
+ font-size: 12px;
+}
+</style>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
index caf84dc9573..46a51859da5 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -21,18 +21,13 @@ export default {
type: Number,
required: true,
},
- discussions: {
- type: Array,
- required: false,
- default: () => [],
- },
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
className() {
- return this.discussions.length ? '' : 'js-temp-notes-holder';
+ return this.line.discussions.length ? '' : 'js-temp-notes-holder';
},
},
};
@@ -44,14 +39,13 @@ export default {
class="notes_holder"
>
<td
- class="notes_line"
- colspan="2"
- ></td>
- <td class="notes_content">
+ class="notes_content"
+ colspan="3"
+ >
<div class="content">
<diff-discussions
- v-if="discussions.length"
- :discussions="discussions"
+ v-if="line.discussions.length"
+ :discussions="line.discussions"
/>
<diff-line-note-form
v-if="diffLineCommentForms[line.lineCode]"
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 32d65ff994f..62fa34e835a 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapGetters, mapActions } from 'vuex';
import DiffTableCell from './diff_table_cell.vue';
import {
NEW_LINE_TYPE,
@@ -33,11 +33,6 @@ export default {
required: false,
default: false,
},
- discussions: {
- type: Array,
- required: false,
- default: () => [],
- },
},
data() {
return {
@@ -68,7 +63,11 @@ export default {
this.linePositionLeft = LINE_POSITION_LEFT;
this.linePositionRight = LINE_POSITION_RIGHT;
},
+ mounted() {
+ this.scrollToLineIfNeededInline(this.line);
+ },
methods: {
+ ...mapActions('diffs', ['scrollToLineIfNeededInline']),
handleMouseMove(e) {
// To show the comment icon on the gutter we need to know if we hover the line.
// Current table structure doesn't allow us to do this with CSS in both of the diff view types
@@ -94,7 +93,6 @@ export default {
:is-bottom="isBottom"
:is-hover="isHover"
:show-comment-button="true"
- :discussions="discussions"
class="diff-line-num old_line"
/>
<diff-table-cell
@@ -104,7 +102,6 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isHover"
- :discussions="discussions"
class="diff-line-num new_line"
/>
<td
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index e7d789734c3..fbf9e77ac07 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -2,7 +2,6 @@
import { mapGetters, mapState } from 'vuex';
import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue';
-import { trimFirstCharOfLineContent } from '../store/utils';
export default {
components: {
@@ -20,29 +19,17 @@ export default {
},
},
computed: {
- ...mapGetters('diffs', [
- 'commitId',
- 'shouldRenderInlineCommentRow',
- 'singleDiscussionByLineCode',
- ]),
+ ...mapGetters('diffs', ['commitId', 'shouldRenderInlineCommentRow']),
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
- normalizedDiffLines() {
- return this.diffLines.map(line => (line.richText ? trimFirstCharOfLineContent(line) : line));
- },
diffLinesLength() {
- return this.normalizedDiffLines.length;
+ return this.diffLines.length;
},
userColorScheme() {
return window.gon.user_color_scheme;
},
},
- methods: {
- discussionsList(line) {
- return line.lineCode !== undefined ? this.singleDiscussionByLineCode(line.lineCode) : [];
- },
- },
};
</script>
@@ -53,23 +40,21 @@ export default {
class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view">
<tbody>
<template
- v-for="(line, index) in normalizedDiffLines"
+ v-for="(line, index) in diffLines"
>
<inline-diff-table-row
+ :key="line.lineCode"
:file-hash="diffFile.fileHash"
:context-lines-path="diffFile.contextLinesPath"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
- :key="line.lineCode"
- :discussions="discussionsList(line)"
/>
<inline-diff-comment-row
v-if="shouldRenderInlineCommentRow(line)"
+ :key="index"
:diff-file-hash="diffFile.fileHash"
:line="line"
:line-index="index"
- :key="index"
- :discussions="discussionsList(line)"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue
index d817157fbcd..6905630ad8c 100644
--- a/app/assets/javascripts/diffs/components/no_changes.vue
+++ b/app/assets/javascripts/diffs/components/no_changes.vue
@@ -38,7 +38,7 @@ export default {
<div class="text-center">
<a
:href="newBlobPath"
- class="btn btn-save"
+ class="btn btn-success"
>
{{ __('Create commit') }}
</a>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index 48b8feeb0b4..3339c56cbb6 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -21,51 +21,49 @@ export default {
type: Number,
required: true,
},
- leftDiscussions: {
- type: Array,
- required: false,
- default: () => [],
- },
- rightDiscussions: {
- type: Array,
- required: false,
- default: () => [],
- },
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
leftLineCode() {
- return this.line.left.lineCode;
+ return this.line.left && this.line.left.lineCode;
},
rightLineCode() {
- return this.line.right.lineCode;
+ return this.line.right && this.line.right.lineCode;
},
hasExpandedDiscussionOnLeft() {
- const discussions = this.leftDiscussions;
-
- return discussions ? discussions.every(discussion => discussion.expanded) : false;
+ return this.line.left && this.line.left.discussions
+ ? this.line.left.discussions.every(discussion => discussion.expanded)
+ : false;
},
hasExpandedDiscussionOnRight() {
- const discussions = this.rightDiscussions;
-
- return discussions ? discussions.every(discussion => discussion.expanded) : false;
+ return this.line.right && this.line.right.discussions
+ ? this.line.right.discussions.every(discussion => discussion.expanded)
+ : false;
},
hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
shouldRenderDiscussionsOnLeft() {
- return this.leftDiscussions && this.hasExpandedDiscussionOnLeft;
+ return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft;
},
shouldRenderDiscussionsOnRight() {
- return this.rightDiscussions && this.hasExpandedDiscussionOnRight && this.line.right.type;
+ return (
+ this.line.right &&
+ this.line.right.discussions &&
+ this.hasExpandedDiscussionOnRight &&
+ this.line.right.type
+ );
},
showRightSideCommentForm() {
- return this.line.right.type && this.diffLineCommentForms[this.rightLineCode];
+ return (
+ this.line.right && this.line.right.type && this.diffLineCommentForms[this.rightLineCode]
+ );
},
className() {
- return this.leftDiscussions.length > 0 || this.rightDiscussions.length > 0
+ return (this.left && this.line.left.discussions.length > 0) ||
+ (this.right && this.line.right.discussions.length > 0)
? ''
: 'js-temp-notes-holder';
},
@@ -85,8 +83,8 @@ export default {
class="content"
>
<diff-discussions
- v-if="leftDiscussions.length"
- :discussions="leftDiscussions"
+ v-if="line.left.discussions.length"
+ :discussions="line.left.discussions"
/>
</div>
<diff-line-note-form
@@ -94,7 +92,7 @@ export default {
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
- position="left"
+ line-position="left"
/>
</td>
<td class="notes_line new"></td>
@@ -104,8 +102,8 @@ export default {
class="content"
>
<diff-discussions
- v-if="rightDiscussions.length"
- :discussions="rightDiscussions"
+ v-if="line.right.discussions.length"
+ :discussions="line.right.discussions"
/>
</div>
<diff-line-note-form
@@ -113,7 +111,7 @@ export default {
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
- position="right"
+ line-position="right"
/>
</td>
</tr>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index d4e54c2bd00..fcc3b3e9117 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -1,6 +1,6 @@
<script>
+import { mapActions } from 'vuex';
import $ from 'jquery';
-import { mapGetters } from 'vuex';
import DiffTableCell from './diff_table_cell.vue';
import {
NEW_LINE_TYPE,
@@ -10,8 +10,7 @@ import {
OLD_NO_NEW_LINE_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
NEW_NO_NEW_LINE_TYPE,
- LINE_POSITION_LEFT,
- LINE_POSITION_RIGHT,
+ EMPTY_CELL_TYPE,
} from '../constants';
export default {
@@ -36,16 +35,6 @@ export default {
required: false,
default: false,
},
- leftDiscussions: {
- type: Array,
- required: false,
- default: () => [],
- },
- rightDiscussions: {
- type: Array,
- required: false,
- default: () => [],
- },
},
data() {
return {
@@ -54,32 +43,33 @@ export default {
};
},
computed: {
- ...mapGetters('diffs', ['isParallelView']),
isContextLine() {
- return this.line.left.type === CONTEXT_LINE_TYPE;
+ return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
},
classNameMap() {
return {
[CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
- [PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView,
+ [PARALLEL_DIFF_VIEW_TYPE]: true,
};
},
parallelViewLeftLineType() {
- if (this.line.right.type === NEW_NO_NEW_LINE_TYPE) {
+ if (this.line.right && this.line.right.type === NEW_NO_NEW_LINE_TYPE) {
return OLD_NO_NEW_LINE_TYPE;
}
- return this.line.left.type;
+ return this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
},
},
created() {
this.newLineType = NEW_LINE_TYPE;
this.oldLineType = OLD_LINE_TYPE;
- this.linePositionLeft = LINE_POSITION_LEFT;
- this.linePositionRight = LINE_POSITION_RIGHT;
this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
},
+ mounted() {
+ this.scrollToLineIfNeededParallel(this.line);
+ },
methods: {
+ ...mapActions('diffs', ['scrollToLineIfNeededParallel']),
handleMouseMove(e) {
const isHover = e.type === 'mouseover';
const hoveringCell = e.target.closest('td');
@@ -116,47 +106,57 @@ export default {
@mouseover="handleMouseMove"
@mouseout="handleMouseMove"
>
- <diff-table-cell
- :file-hash="fileHash"
- :context-lines-path="contextLinesPath"
- :line="line"
- :line-type="oldLineType"
- :line-position="linePositionLeft"
- :is-bottom="isBottom"
- :is-hover="isLeftHover"
- :show-comment-button="true"
- :diff-view-type="parallelDiffViewType"
- :discussions="leftDiscussions"
- class="diff-line-num old_line"
- />
- <td
- :id="line.left.lineCode"
- :class="parallelViewLeftLineType"
- class="line_content parallel left-side"
- @mousedown.native="handleParallelLineMouseDown"
- v-html="line.left.richText"
- >
- </td>
- <diff-table-cell
- :file-hash="fileHash"
- :context-lines-path="contextLinesPath"
- :line="line"
- :line-type="newLineType"
- :line-position="linePositionRight"
- :is-bottom="isBottom"
- :is-hover="isRightHover"
- :show-comment-button="true"
- :diff-view-type="parallelDiffViewType"
- :discussions="rightDiscussions"
- class="diff-line-num new_line"
- />
- <td
- :id="line.right.lineCode"
- :class="line.right.type"
- class="line_content parallel right-side"
- @mousedown.native="handleParallelLineMouseDown"
- v-html="line.right.richText"
- >
- </td>
+ <template v-if="line.left">
+ <diff-table-cell
+ :file-hash="fileHash"
+ :context-lines-path="contextLinesPath"
+ :line="line.left"
+ :line-type="oldLineType"
+ :is-bottom="isBottom"
+ :is-hover="isLeftHover"
+ :show-comment-button="true"
+ :diff-view-type="parallelDiffViewType"
+ line-position="left"
+ class="diff-line-num old_line"
+ />
+ <td
+ :id="line.left.lineCode"
+ :class="parallelViewLeftLineType"
+ class="line_content parallel left-side"
+ @mousedown.native="handleParallelLineMouseDown"
+ v-html="line.left.richText"
+ >
+ </td>
+ </template>
+ <template v-else>
+ <td class="diff-line-num old_line empty-cell"></td>
+ <td class="line_content parallel left-side empty-cell"></td>
+ </template>
+ <template v-if="line.right">
+ <diff-table-cell
+ :file-hash="fileHash"
+ :context-lines-path="contextLinesPath"
+ :line="line.right"
+ :line-type="newLineType"
+ :is-bottom="isBottom"
+ :is-hover="isRightHover"
+ :show-comment-button="true"
+ :diff-view-type="parallelDiffViewType"
+ line-position="right"
+ class="diff-line-num new_line"
+ />
+ <td
+ :id="line.right.lineCode"
+ :class="line.right.type"
+ class="line_content parallel right-side"
+ @mousedown.native="handleParallelLineMouseDown"
+ v-html="line.right.richText"
+ >
+ </td>
+ </template>
+ <template v-else>
+ <td class="diff-line-num old_line empty-cell"></td>
+ <td class="line_content parallel right-side empty-cell"></td>
+ </template>
</tr>
</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 24ceb52a04a..3452f0d2b00 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -2,8 +2,6 @@
import { mapState, mapGetters } from 'vuex';
import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
-import { EMPTY_CELL_TYPE } from '../constants';
-import { trimFirstCharOfLineContent } from '../store/utils';
export default {
components: {
@@ -21,46 +19,17 @@ export default {
},
},
computed: {
- ...mapGetters('diffs', [
- 'commitId',
- 'singleDiscussionByLineCode',
- 'shouldRenderParallelCommentRow',
- ]),
+ ...mapGetters('diffs', ['commitId', 'shouldRenderParallelCommentRow']),
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
- parallelDiffLines() {
- return this.diffLines.map(line => {
- const parallelLine = Object.assign({}, line);
-
- if (line.left) {
- parallelLine.left = trimFirstCharOfLineContent(line.left);
- } else {
- parallelLine.left = { type: EMPTY_CELL_TYPE };
- }
-
- if (line.right) {
- parallelLine.right = trimFirstCharOfLineContent(line.right);
- } else {
- parallelLine.right = { type: EMPTY_CELL_TYPE };
- }
-
- return parallelLine;
- });
- },
diffLinesLength() {
- return this.parallelDiffLines.length;
+ return this.diffLines.length;
},
userColorScheme() {
return window.gon.user_color_scheme;
},
},
- methods: {
- discussionsByLine(line, leftOrRight) {
- return line[leftOrRight] && line[leftOrRight].lineCode !== undefined ?
- this.singleDiscussionByLineCode(line[leftOrRight].lineCode) : [];
- },
- },
};
</script>
@@ -73,16 +42,14 @@ export default {
<table>
<tbody>
<template
- v-for="(line, index) in parallelDiffLines"
+ v-for="(line, index) in diffLines"
>
<parallel-diff-table-row
+ :key="index"
:file-hash="diffFile.fileHash"
:context-lines-path="diffFile.contextLinesPath"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
- :key="index"
- :left-discussions="discussionsByLine(line, 'left')"
- :right-discussions="discussionsByLine(line, 'right')"
/>
<parallel-diff-comment-row
v-if="shouldRenderParallelCommentRow(line)"
@@ -90,8 +57,6 @@ export default {
:line="line"
:diff-file-hash="diffFile.fileHash"
:line-index="index"
- :left-discussions="discussionsByLine(line, 'left')"
- :right-discussions="discussionsByLine(line, 'right')"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
new file mode 100644
index 00000000000..cfe4273742f
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -0,0 +1,101 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileRow from '~/vue_shared/components/file_row.vue';
+import FileRowStats from './file_row_stats.vue';
+
+export default {
+ components: {
+ Icon,
+ FileRow,
+ },
+ data() {
+ return {
+ search: '',
+ };
+ },
+ computed: {
+ ...mapState('diffs', ['tree', 'addedLines', 'removedLines']),
+ ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
+ filteredTreeList() {
+ const search = this.search.toLowerCase().trim();
+
+ if (search === '') return this.tree;
+
+ return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0);
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
+ clearSearch() {
+ this.search = '';
+ },
+ },
+ FileRowStats,
+};
+</script>
+
+<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"
+ >
+ <icon
+ name="close"
+ />
+ </button>
+ </div>
+ <div
+ class="tree-list-scroll"
+ >
+ <template v-if="filteredTreeList.length">
+ <file-row
+ v-for="file in filteredTreeList"
+ :key="file.key"
+ :file="file"
+ :level="0"
+ :hide-extra-on-tree="true"
+ :extra-component="$options.FileRowStats"
+ :show-changed-icon="true"
+ @toggleTreeOpen="toggleTreeOpen"
+ @clickFile="scrollToFile"
+ />
+ </template>
+ <p
+ v-else
+ class="prepend-top-20 append-bottom-20 text-center"
+ >
+ {{ s__('MergeRequest|No files found') }}
+ </p>
+ </div>
+ <div
+ v-once
+ class="pt-3 pb-3 text-center"
+ >
+ {{ n__('%d changed file', '%d changed files', diffFilesLength) }}
+ <div>
+ <span class="cgreen">
+ {{ n__('%d addition', '%d additions', addedLines) }}
+ </span>
+ <span class="cred">
+ {{ n__('%d deleted', '%d deletions', removedLines) }}
+ </span>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index f68afa44837..6a50d2c1426 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -7,6 +7,7 @@ export const CONTEXT_LINE_TYPE = 'context';
export const EMPTY_CELL_TYPE = 'empty-cell';
export const COMMENT_FORM_TYPE = 'commentForm';
export const DIFF_NOTE_TYPE = 'DiffNote';
+export const LEGACY_DIFF_NOTE_TYPE = 'LegacyDiffNote';
export const NOTE_TYPE = 'Note';
export const NEW_LINE_TYPE = 'new';
export const OLD_LINE_TYPE = 'old';
@@ -28,3 +29,5 @@ export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000;
+
+export const MR_TREE_SHOW_KEY = 'mr_tree_show';
diff --git a/app/assets/javascripts/diffs/mixins/changed_files.js b/app/assets/javascripts/diffs/mixins/changed_files.js
deleted file mode 100644
index da1339f0ffa..00000000000
--- a/app/assets/javascripts/diffs/mixins/changed_files.js
+++ /dev/null
@@ -1,38 +0,0 @@
-export default {
- props: {
- diffFiles: {
- type: Array,
- required: true,
- },
- },
- methods: {
- fileChangedIcon(diffFile) {
- if (diffFile.deletedFile) {
- return 'file-deletion';
- } else if (diffFile.newFile) {
- return 'file-addition';
- }
- return 'file-modified';
- },
- fileChangedClass(diffFile) {
- if (diffFile.deletedFile) {
- return 'cred';
- } else if (diffFile.newFile) {
- return 'cgreen';
- }
-
- return '';
- },
- truncatedDiffPath(path) {
- const maxLength = 60;
-
- if (path.length > maxLength) {
- const start = path.length - maxLength;
- const end = start + maxLength;
- return `...${path.slice(start, end)}`;
- }
-
- return path;
- },
- },
-};
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 4ab6ceb249a..1e0b27b538d 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -1,13 +1,18 @@
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
+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 {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
DIFF_VIEW_COOKIE_NAME,
+ MR_TREE_SHOW_KEY,
} from '../constants';
export const setBaseConfig = ({ commit }, options) => {
@@ -29,25 +34,53 @@ export const fetchDiffFiles = ({ state, commit }) => {
.then(handleLocationHash);
};
-export const startRenderDiffsQueue = ({ state, commit }) => {
- const checkItem = () => {
- const nextFile = state.diffFiles.find(
- file => !file.renderIt && (!file.collapsed || !file.text),
- );
- if (nextFile) {
- requestAnimationFrame(() => {
- commit(types.RENDER_FILE, nextFile);
+// 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) => {
+ 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,
});
- requestIdleCallback(
- () => {
- checkItem();
- },
- { timeout: 1000 },
- );
}
- };
+ });
+};
- checkItem();
+export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
+ const { fileHash, line_code } = removeDiscussion;
+ commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code });
+};
+
+export const startRenderDiffsQueue = ({ state, commit }) => {
+ const checkItem = () =>
+ new Promise(resolve => {
+ const nextFile = state.diffFiles.find(
+ file => !file.renderIt && (!file.collapsed || !file.text),
+ );
+
+ if (nextFile) {
+ requestAnimationFrame(() => {
+ commit(types.RENDER_FILE, nextFile);
+ });
+ requestIdleCallback(
+ () => {
+ checkItem()
+ .then(resolve)
+ .catch(() => {});
+ },
+ { timeout: 1000 },
+ );
+ } else {
+ resolve();
+ }
+ });
+
+ return checkItem();
};
export const setInlineDiffViewType = ({ commit }) => {
@@ -91,6 +124,25 @@ export const loadMoreLines = ({ commit }, options) => {
});
};
+export const scrollToLineIfNeededInline = (_, line) => {
+ const hash = getLocationHash();
+
+ if (hash && line.lineCode === hash) {
+ handleLocationHash();
+ }
+};
+
+export const scrollToLineIfNeededParallel = (_, line) => {
+ const hash = getLocationHash();
+
+ if (
+ hash &&
+ ((line.left && line.left.lineCode === hash) || (line.right && line.right.lineCode === hash))
+ ) {
+ handleLocationHash();
+ }
+};
+
export const loadCollapsedDiff = ({ commit }, file) =>
axios.get(file.loadCollapsedDiffUrl).then(res => {
commit(types.ADD_COLLAPSED_DIFFS, {
@@ -130,5 +182,37 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
});
};
+export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
+ const postData = getNoteFormData({
+ note,
+ ...formData,
+ });
+
+ return dispatch('saveNote', postData, { root: true })
+ .then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
+ .then(discussion =>
+ dispatch('assignDiscussionsToDiff', reduceDiscussionsToLineCodes([discussion])),
+ )
+ .catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
+};
+
+export const toggleTreeOpen = ({ commit }, path) => {
+ commit(types.TOGGLE_FOLDER_OPEN, path);
+};
+
+export const scrollToFile = ({ state, commit }, path) => {
+ const { fileHash } = state.treeEntries[path];
+ document.location.hash = fileHash;
+
+ commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
+
+ setTimeout(() => commit(types.UPDATE_CURRENT_DIFF_FILE_ID, ''), 1000);
+};
+
+export const toggleShowTreeList = ({ commit, state }) => {
+ commit(types.TOGGLE_SHOW_TREE_LIST);
+ localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 4a47646d7fa..d4c205882ff 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -17,7 +17,10 @@ export const commitId = state => (state.commit && state.commit.id ? state.commit
export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
const discussions = getters.getDiffFileDiscussions(diff);
- return (discussions.length && discussions.every(discussion => discussion.expanded)) || false;
+ return (
+ (discussions && discussions.length && discussions.every(discussion => discussion.expanded)) ||
+ false
+ );
};
/**
@@ -28,7 +31,10 @@ export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
export const diffHasAllCollpasedDiscussions = (state, getters) => diff => {
const discussions = getters.getDiffFileDiscussions(diff);
- return (discussions.length && discussions.every(discussion => !discussion.expanded)) || false;
+ return (
+ (discussions && discussions.length && discussions.every(discussion => !discussion.expanded)) ||
+ false
+ );
};
/**
@@ -40,7 +46,9 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => {
const discussions = getters.getDiffFileDiscussions(diff);
return (
- (discussions.length && discussions.find(discussion => discussion.expanded) !== undefined) ||
+ (discussions &&
+ discussions.length &&
+ discussions.find(discussion => discussion.expanded) !== undefined) ||
false
);
};
@@ -64,50 +72,47 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
) || [];
-export const singleDiscussionByLineCode = (state, getters, rootState, rootGetters) => lineCode => {
- if (!lineCode || lineCode === undefined) return [];
- const discussions = rootGetters.discussionsByLineCode;
- return discussions[lineCode] || [];
-};
-
-export const shouldRenderParallelCommentRow = (state, getters) => line => {
- const leftLineCode = line.left.lineCode;
- const rightLineCode = line.right.lineCode;
- const leftDiscussions = getters.singleDiscussionByLineCode(leftLineCode);
- const rightDiscussions = getters.singleDiscussionByLineCode(rightLineCode);
- const hasDiscussion = leftDiscussions.length || rightDiscussions.length;
+export const shouldRenderParallelCommentRow = state => line => {
+ const hasDiscussion =
+ (line.left && line.left.discussions && line.left.discussions.length) ||
+ (line.right && line.right.discussions && line.right.discussions.length);
- const hasExpandedDiscussionOnLeft = leftDiscussions.length
- ? leftDiscussions.every(discussion => discussion.expanded)
- : false;
- const hasExpandedDiscussionOnRight = rightDiscussions.length
- ? rightDiscussions.every(discussion => discussion.expanded)
- : false;
+ const hasExpandedDiscussionOnLeft =
+ line.left && line.left.discussions && line.left.discussions.length
+ ? line.left.discussions.every(discussion => discussion.expanded)
+ : false;
+ const hasExpandedDiscussionOnRight =
+ line.right && line.right.discussions && line.right.discussions.length
+ ? line.right.discussions.every(discussion => discussion.expanded)
+ : false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true;
}
- const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode];
- const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode];
+ const hasCommentFormOnLeft = line.left && state.diffLineCommentForms[line.left.lineCode];
+ const hasCommentFormOnRight = line.right && state.diffLineCommentForms[line.right.lineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight;
};
-export const shouldRenderInlineCommentRow = (state, getters) => line => {
+export const shouldRenderInlineCommentRow = state => line => {
if (state.diffLineCommentForms[line.lineCode]) return true;
- const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode);
- if (lineDiscussions.length === 0) {
+ if (!line.discussions || line.discussions.length === 0) {
return false;
}
- return lineDiscussions.every(discussion => discussion.expanded);
+ return line.discussions.every(discussion => discussion.expanded);
};
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.fileHash === fileHash);
+export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.type === 'blob');
+
+export const diffFilesLength = state => state.diffFiles.length;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 39d90a64aab..ae8930c8968 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -1,18 +1,25 @@
import Cookies from 'js-cookie';
import { getParameterValues } from '~/lib/utils/url_utility';
-import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
+import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants';
const viewTypeFromQueryString = getParameterValues('view')[0];
const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
const defaultViewType = INLINE_DIFF_VIEW_TYPE;
+const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY);
export default () => ({
isLoading: true,
endpoint: '',
basePath: '',
commit: null,
+ startVersion: null,
diffFiles: [],
mergeRequestDiffs: [],
+ mergeRequestDiff: null,
diffLineCommentForms: {},
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
+ tree: [],
+ treeEntries: {},
+ showTreeList: storedTreeShow === null ? true : storedTreeShow === 'true',
+ currentDiffFileId: '',
});
diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js
index 20d1ebbe049..6860e24db6b 100644
--- a/app/assets/javascripts/diffs/store/modules/index.js
+++ b/app/assets/javascripts/diffs/store/modules/index.js
@@ -3,10 +3,10 @@ import * as getters from '../getters';
import mutations from '../mutations';
import createState from './diff_state';
-export default {
+export default () => ({
namespaced: true,
state: createState(),
getters,
actions,
mutations,
-};
+});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index c999d637d50..6474ee628e2 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -9,3 +9,8 @@ export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
export const RENDER_FILE = 'RENDER_FILE';
+export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
+export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
+export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
+export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
+export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 0522e32c410..0b4485ecdb5 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -1,8 +1,15 @@
import Vue from 'vue';
-import _ from 'underscore';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils';
-import { LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED } from '../constants';
+import { sortTree } from '~/ide/stores/utils';
+import {
+ findDiffFile,
+ addLineReferences,
+ removeMatchLine,
+ addContextLines,
+ prepareDiffData,
+ isDiscussionApplicableToLine,
+ generateTreeList,
+} from './utils';
import * as types from './mutation_types';
export default {
@@ -17,41 +24,13 @@ export default {
[types.SET_DIFF_DATA](state, data) {
const diffData = convertObjectPropsToCamelCase(data, { deep: true });
- let showingLines = 0;
- const filesLength = diffData.diffFiles.length;
- let i;
- for (i = 0; i < filesLength; i += 1) {
- const file = diffData.diffFiles[i];
- if (file.parallelDiffLines) {
- const linesLength = file.parallelDiffLines.length;
- let u = 0;
- for (u = 0; u < linesLength; u += 1) {
- const line = file.parallelDiffLines[u];
- if (line.left) delete line.left.text;
- if (line.right) delete line.right.text;
- }
- }
-
- if (file.highlightedDiffLines) {
- const linesLength = file.highlightedDiffLines.length;
- let u;
- for (u = 0; u < linesLength; u += 1) {
- const line = file.highlightedDiffLines[u];
- delete line.text;
- }
- }
-
- if (file.highlightedDiffLines) {
- showingLines += file.parallelDiffLines.length;
- }
- Object.assign(file, {
- renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
- collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
- });
- }
+ prepareDiffData(diffData);
+ const { tree, treeEntries } = generateTreeList(diffData.diffFiles);
Object.assign(state, {
...diffData,
+ tree: sortTree(tree),
+ treeEntries,
});
},
@@ -98,19 +77,104 @@ export default {
[types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
+ prepareDiffData(normalizedData);
const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
-
- if (newFileData) {
- const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash);
- state.diffFiles.splice(index, 1, newFileData);
- }
+ const selectedFile = state.diffFiles.find(f => f.fileHash === file.fileHash);
+ Object.assign(selectedFile, { ...newFileData });
},
[types.EXPAND_ALL_FILES](state) {
- // eslint-disable-next-line no-param-reassign
state.diffFiles = state.diffFiles.map(file => ({
...file,
collapsed: false,
}));
},
+
+ [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 &&
+ 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,
+ });
+ }
+ }
+
+ if (selectedFile.highlightedDiffLines) {
+ const targetInlineLine = selectedFile.highlightedDiffLines.find(
+ line => line.lineCode === firstDiscussion.line_code,
+ );
+
+ if (targetInlineLine) {
+ Object.assign(targetInlineLine, {
+ discussions,
+ });
+ }
+ }
+ }
+ },
+
+ [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
+ const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
+ if (selectedFile) {
+ const targetLine = selectedFile.parallelDiffLines.find(
+ line =>
+ (line.left && line.left.lineCode === lineCode) ||
+ (line.right && line.right.lineCode === lineCode),
+ );
+ if (targetLine) {
+ const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
+
+ Object.assign(targetLine[side], {
+ discussions: [],
+ });
+ }
+
+ if (selectedFile.highlightedDiffLines) {
+ const targetInlineLine = selectedFile.highlightedDiffLines.find(
+ line => line.lineCode === lineCode,
+ );
+
+ if (targetInlineLine) {
+ Object.assign(targetInlineLine, {
+ discussions: [],
+ });
+ }
+ }
+ }
+ },
+ [types.TOGGLE_FOLDER_OPEN](state, path) {
+ state.treeEntries[path].opened = !state.treeEntries[path].opened;
+ },
+ [types.TOGGLE_SHOW_TREE_LIST](state) {
+ state.showTreeList = !state.showTreeList;
+ },
+ [types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
+ state.currentDiffFileId = fileId;
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 82082ac508a..a482a2b82c0 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -4,10 +4,13 @@ import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
TEXT_DIFF_POSITION_TYPE,
+ LEGACY_DIFF_NOTE_TYPE,
DIFF_NOTE_TYPE,
NEW_LINE_TYPE,
OLD_LINE_TYPE,
MATCH_LINE_TYPE,
+ LINES_TO_BE_RENDERED_DIRECTLY,
+ MAX_LINES_TO_BE_RENDERED,
} from '../constants';
export function findDiffFile(files, hash) {
@@ -22,7 +25,7 @@ export const getReversePosition = linePosition => {
return LINE_POSITION_RIGHT;
};
-export function getNoteFormData(params) {
+export function getFormData(params) {
const {
note,
noteableType,
@@ -52,20 +55,30 @@ export function getNoteFormData(params) {
note_project_id: '',
target_type: noteableData.targetType,
target_id: noteableData.id,
+ return_discussion: true,
note: {
note,
position,
noteable_type: noteableType,
noteable_id: noteableData.id,
commit_id: '',
- type: DIFF_NOTE_TYPE,
+ type:
+ diffFile.diffRefs.startSha && diffFile.diffRefs.headSha
+ ? DIFF_NOTE_TYPE
+ : LEGACY_DIFF_NOTE_TYPE,
line_code: noteTargetLine.lineCode,
},
};
+ return postData;
+}
+
+export function getNoteFormData(params) {
+ const data = getFormData(params);
+
return {
- endpoint: noteableData.create_note_path,
- data: postData,
+ endpoint: params.noteableData.create_note_path,
+ data,
};
}
@@ -161,6 +174,11 @@ export function addContextLines(options) {
* @returns {Object}
*/
export function trimFirstCharOfLineContent(line = {}) {
+ // eslint-disable-next-line no-param-reassign
+ delete line.text;
+ // eslint-disable-next-line no-param-reassign
+ line.discussions = [];
+
const parsedLine = Object.assign({}, line);
if (line.richText) {
@@ -174,7 +192,44 @@ export function trimFirstCharOfLineContent(line = {}) {
return parsedLine;
}
-export function getDiffRefsByLineCode(diffFiles) {
+// This prepares and optimizes the incoming diff data from the server
+// by setting up incremental rendering and removing unneeded data
+export function prepareDiffData(diffData) {
+ const filesLength = diffData.diffFiles.length;
+ let showingLines = 0;
+ for (let i = 0; i < filesLength; i += 1) {
+ const file = diffData.diffFiles[i];
+
+ if (file.parallelDiffLines) {
+ const linesLength = file.parallelDiffLines.length;
+ for (let u = 0; u < linesLength; u += 1) {
+ const line = file.parallelDiffLines[u];
+ if (line.left) {
+ line.left = trimFirstCharOfLineContent(line.left);
+ }
+ if (line.right) {
+ line.right = trimFirstCharOfLineContent(line.right);
+ }
+ }
+ }
+
+ if (file.highlightedDiffLines) {
+ const linesLength = file.highlightedDiffLines.length;
+ for (let u = 0; u < linesLength; u += 1) {
+ const line = file.highlightedDiffLines[u];
+ Object.assign(line, { ...trimFirstCharOfLineContent(line) });
+ }
+ showingLines += file.parallelDiffLines.length;
+ }
+
+ Object.assign(file, {
+ renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
+ collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
+ });
+ }
+}
+
+export function getDiffPositionByLineCode(diffFiles) {
return diffFiles.reduce((acc, diffFile) => {
const { baseSha, headSha, startSha } = diffFile.diffRefs;
const { newPath, oldPath } = diffFile;
@@ -186,7 +241,17 @@ export function getDiffRefsByLineCode(diffFiles) {
const { lineCode, oldLine, newLine } = line;
if (lineCode) {
- acc[lineCode] = { baseSha, headSha, startSha, newPath, oldPath, oldLine, newLine };
+ acc[lineCode] = {
+ baseSha,
+ headSha,
+ startSha,
+ newPath,
+ oldPath,
+ oldLine,
+ newLine,
+ lineCode,
+ positionType: 'text',
+ };
}
});
}
@@ -194,3 +259,64 @@ export function getDiffRefsByLineCode(diffFiles) {
return acc;
}, {});
}
+
+// This method will check whether the discussion is still applicable
+// to the diff line in question regarding different versions of the MR
+export function isDiscussionApplicableToLine({ discussion, diffPosition, latestDiff }) {
+ const { lineCode, ...diffPositionCopy } = diffPosition;
+
+ if (discussion.original_position && discussion.position) {
+ const originalRefs = convertObjectPropsToCamelCase(discussion.original_position);
+ const refs = convertObjectPropsToCamelCase(discussion.position);
+
+ return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy);
+ }
+
+ return latestDiff && discussion.active && lineCode === discussion.line_code;
+}
+
+export const generateTreeList = files =>
+ files.reduce(
+ (acc, file) => {
+ const { fileHash, addedLines, removedLines, newFile, deletedFile, newPath } = file;
+ const split = newPath.split('/');
+
+ split.forEach((name, i) => {
+ const parent = acc.treeEntries[split.slice(0, i).join('/')];
+ const path = `${parent ? `${parent.path}/` : ''}${name}`;
+
+ if (!acc.treeEntries[path]) {
+ const type = path === newPath ? 'blob' : 'tree';
+ acc.treeEntries[path] = {
+ key: path,
+ path,
+ name,
+ type,
+ tree: [],
+ };
+
+ const entry = acc.treeEntries[path];
+
+ if (type === 'blob') {
+ Object.assign(entry, {
+ changed: true,
+ tempFile: newFile,
+ deleted: deletedFile,
+ fileHash,
+ addedLines,
+ removedLines,
+ });
+ } else {
+ Object.assign(entry, {
+ opened: true,
+ });
+ }
+
+ (parent ? parent.tree : acc.tree).push(entry);
+ }
+ });
+
+ return acc;
+ },
+ { treeEntries: {}, tree: [] },
+ );
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
deleted file mode 100644
index a5af37e80b6..00000000000
--- a/app/assets/javascripts/dispatcher.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/* eslint-disable consistent-return, no-new */
-
-import $ from 'jquery';
-import GfmAutoComplete from './gfm_auto_complete';
-import { convertPermissionToBoolean } from './lib/utils/common_utils';
-import GlFieldErrors from './gl_field_errors';
-import Shortcuts from './shortcuts';
-import SearchAutocomplete from './search_autocomplete';
-import performanceBar from './performance_bar';
-
-function initSearch() {
- // Only when search form is present
- if ($('.search').length) {
- return new SearchAutocomplete();
- }
-}
-
-function initFieldErrors() {
- $('.gl-show-field-errors').each((i, form) => {
- new GlFieldErrors(form);
- });
-}
-
-function initPageShortcuts(page) {
- const pagesWithCustomShortcuts = [
- 'projects:activity',
- 'projects:artifacts:browse',
- 'projects:artifacts:file',
- 'projects:blame:show',
- 'projects:blob:show',
- 'projects:commit:show',
- 'projects:commits:show',
- 'projects:find_file:show',
- 'projects:issues:edit',
- 'projects:issues:index',
- 'projects:issues:new',
- 'projects:issues:show',
- 'projects:merge_requests:creations:diffs',
- 'projects:merge_requests:creations:new',
- 'projects:merge_requests:edit',
- 'projects:merge_requests:index',
- 'projects:merge_requests:show',
- 'projects:network:show',
- 'projects:show',
- 'projects:tree:show',
- 'groups:show',
- ];
-
- if (pagesWithCustomShortcuts.indexOf(page) === -1) {
- new Shortcuts();
- }
-}
-
-function initGFMInput() {
- $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
- const gfm = new GfmAutoComplete(
- gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
- );
- const enableGFM = convertPermissionToBoolean(
- el.dataset.supportsAutocomplete,
- );
- gfm.setup($(el), {
- emojis: true,
- members: enableGFM,
- issues: enableGFM,
- milestones: enableGFM,
- mergeRequests: enableGFM,
- labels: enableGFM,
- });
- });
-}
-
-function initPerformanceBar() {
- if (document.querySelector('#js-peek')) {
- performanceBar({ container: '#js-peek' });
- }
-}
-
-export default () => {
- initSearch();
- initFieldErrors();
-
- const page = $('body').attr('data-page');
- if (page) {
- initPageShortcuts(page);
- initGFMInput();
- initPerformanceBar();
- }
-};
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 5528ad9f38d..d2778bcdf1c 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,12 +1,25 @@
import $ from 'jquery';
import Dropzone from 'dropzone';
import _ from 'underscore';
-import './preview_markdown';
+import './behaviors/preview_markdown';
import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils';
Dropzone.autoDiscover = false;
+/**
+ * Return the error message string from the given response.
+ *
+ * @param {String|Object} res
+ */
+function getErrorMessage(res) {
+ if (!res || _.isString(res)) {
+ return res;
+ }
+
+ return res.message;
+}
+
export default function dropzoneInput(form) {
const divHover = '<div class="div-dropzone-hover"></div>';
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
@@ -18,7 +31,7 @@ export default function dropzoneInput(form) {
const $uploadingErrorContainer = form.find('.uploading-error-container');
const $uploadingErrorMessage = form.find('.uploading-error-message');
const $uploadingProgressContainer = form.find('.uploading-progress-container');
- const uploadsPath = window.uploads_path || null;
+ const uploadsPath = form.data('uploads-path') || window.uploads_path || null;
const maxFileSize = gon.max_file_size || 10;
const formTextarea = form.find('.js-gfm-input');
let handlePaste;
@@ -42,7 +55,7 @@ export default function dropzoneInput(form) {
if (!uploadsPath) {
$formDropzone.addClass('js-invalid-dropzone');
- return;
+ return null;
}
const dropzone = $formDropzone.dropzone({
@@ -84,9 +97,7 @@ export default function dropzoneInput(form) {
// xhr object (xhr.responseText is error message).
// On error we hide the 'Attach' and 'Cancel' buttons
// and show an error.
-
- // If there's xhr error message, let's show it instead of dropzone's one.
- const message = xhr ? xhr.responseText : errorMessage;
+ const message = getErrorMessage(errorMessage || xhr.responseText);
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
@@ -274,4 +285,6 @@ export default function dropzoneInput(form) {
$(this).closest('.gfm-form').find('.div-dropzone').click();
formTextarea.focus();
});
+
+ return Dropzone.forElement($formDropzone.get(0));
}
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 9aa224fa407..9de851c9409 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -1,12 +1,10 @@
<script>
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
export default {
components: {
environmentTable,
- loadingIcon,
tablePagination,
},
props: {
@@ -42,11 +40,11 @@
<template>
<div class="environments-container">
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
+ :size="3"
class="prepend-top-default"
label="Loading environments"
- size="3"
/>
<slot name="emptyState"></slot>
diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue
index 00e63c3467a..cf78f89981e 100644
--- a/app/assets/javascripts/environments/components/empty_state.vue
+++ b/app/assets/javascripts/environments/components/empty_state.vue
@@ -35,7 +35,7 @@ code gets deployed, such as staging or production.`) }}
<a
v-if="canCreateEnvironment"
:href="newPath"
- class="btn btn-create js-new-environment-button"
+ class="btn btn-success js-new-environment-button"
>
{{ s__("Environments|New environment") }}
</a>
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 63d83e307ee..e1f9248bc4c 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,7 +1,6 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
@@ -9,7 +8,6 @@ export default {
tooltip,
},
components: {
- loadingIcon,
Icon,
},
props: {
@@ -67,7 +65,7 @@ export default {
aria-hidden="true"
>
</i>
- <loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" />
</span>
</button>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 11e3b781e5a..a1d8e531940 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -466,7 +466,9 @@ export default {
class="gl-responsive-table-row"
role="row">
<div
- class="table-section section-wrap section-15"
+ v-tooltip
+ :title="model.name"
+ class="table-section section-wrap section-15 text-truncate"
role="gridcell"
>
<div
@@ -480,9 +482,7 @@ export default {
v-if="!model.isFolder"
class="environment-name table-mobile-content">
<a
- v-tooltip
:href="environmentPath"
- :title="model.name"
>
{{ model.name }}
</a>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index ccc8419ca6d..a0797b594cb 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,12 +2,14 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
+import { Button } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
+ 'gl-button': Button,
},
directives: {
tooltip,
@@ -26,15 +28,16 @@ export default {
};
</script>
<template>
- <a
+ <gl-button
v-tooltip
:href="monitoringUrl"
:title="title"
:aria-label="title"
- class="btn monitoring-url d-none d-sm-none d-md-block"
+ class="monitoring-url d-none d-sm-none d-md-block"
data-container="body"
rel="noopener noreferrer nofollow"
+ variant="default"
>
<icon name="chart" />
- </a>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 4deeef4beb9..efbf88d0f11 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -9,12 +9,10 @@ import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../event_hub';
-import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
Icon,
- LoadingIcon,
},
directives: {
@@ -70,6 +68,6 @@ export default {
v-else
name="redo"/>
- <loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" />
</button>
</template>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 8efdfb8abe0..e2ecf426e64 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -107,7 +107,7 @@
>
<a
:href="newEnvironmentPath"
- class="btn btn-create"
+ class="btn btn-success"
>
{{ s__("Environments|New environment") }}
</a>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 016e9f7c7b3..16abafebbc0 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -2,13 +2,11 @@
/**
* Render environments table.
*/
-import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import environmentItem from './environment_item.vue';
export default {
components: {
environmentItem,
- loadingIcon,
},
props: {
@@ -85,10 +83,10 @@ export default {
:model="model">
<div
is="environment-item"
+ :key="`environment-item-${i}`"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
- :key="`environment-item-${i}`"
/>
<template
@@ -97,17 +95,17 @@ export default {
<div
v-if="model.isLoadingFolderContent"
:key="`loading-item-${i}`">
- <loading-icon size="2" />
+ <gl-loading-icon :size="2" />
</div>
<template v-else>
<div
is="environment-item"
v-for="(children, index) in model.children"
+ :key="`env-item-${i}-${index}`"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
- :key="`env-item-${i}-${index}`"
/>
<div :key="`sub-div-${i}`">
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index d88624f7f8d..d71964612c5 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -13,7 +13,6 @@ import eventHub from '../event_hub';
import EnvironmentsStore from '../stores/environments_store';
import EnvironmentsService from '../services/environments_service';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
import tabs from '../../vue_shared/components/navigation_tabs.vue';
@@ -24,7 +23,6 @@ export default {
components: {
environmentTable,
container,
- loadingIcon,
tabs,
tablePagination,
},
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
index 2f27c9351bc..03dfa942d69 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -16,7 +16,7 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const hideOnScroll = togglePopover.bind($selector, false);
$selector
- // Setup popover
+ // Set up popover
.data('content', $popoverContent.prop('outerHTML'))
.popover({
html: true,
diff --git a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js
new file mode 100644
index 00000000000..d7aa4ce597f
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js
@@ -0,0 +1,21 @@
+import FilteredSearchTokenKeys from './filtered_search_token_keys';
+
+const tokenKeys = [{
+ key: 'status',
+ type: 'string',
+ param: 'status',
+ symbol: '',
+ icon: 'messages',
+ tag: 'status',
+}, {
+ key: 'type',
+ type: 'string',
+ param: 'type',
+ symbol: '',
+ icon: 'cube',
+ tag: 'type',
+}];
+
+const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys);
+
+export default AdminRunnersFilteredSearchTokenKeys;
diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
index a8eb8d94be3..21b5ccdb613 100644
--- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
+++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
@@ -72,8 +72,8 @@ export default {
@click="onItemActivated(item.text)">
<span>
<span
- v-for="(token, index) in item.tokens"
- :key="`dropdown-token-${index}`"
+ v-for="(token, tokenIndex) in item.tokens"
+ :key="`dropdown-token-${tokenIndex}`"
class="filtered-search-history-dropdown-token"
>
<span class="name">{{ token.prefix }}</span>
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 184b34b7b5e..c568f4e4ebf 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -51,7 +51,11 @@ export default class DropdownHint extends FilteredSearchDropdown {
FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
}
- FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
+ const key = token.replace(':', '');
+ const { uppercaseTokenName } = this.tokenKeys.searchByKey(key);
+ FilteredSearchDropdownManager.addWordToInput(key, '', false, {
+ uppercaseTokenName,
+ });
}
this.dismissDropdown();
this.dispatchInputEvent();
@@ -62,7 +66,7 @@ export default class DropdownHint extends FilteredSearchDropdown {
renderContent() {
const dropdownData = this.tokenKeys.get()
.map(tokenKey => ({
- icon: `fa-${tokenKey.icon}`,
+ icon: `${gon.sprite_icons}#${tokenKey.icon}`,
hint: tokenKey.key,
tag: `:${tokenKey.tag}`,
type: tokenKey.type,
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 27fff488603..6da6ca10008 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -143,7 +143,9 @@ export default class DropdownUtils {
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
- FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
+ FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true, {
+ capitalizeTokenValue: selected.hasAttribute('data-capitalize'),
+ });
}
// Return boolean based on whether it was set
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index 296571606d6..cd3d532c958 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -7,6 +7,7 @@ import DropdownHint from './dropdown_hint';
import DropdownEmoji from './dropdown_emoji';
import DropdownNonUser from './dropdown_non_user';
import DropdownUser from './dropdown_user';
+import NullDropdown from './null_dropdown';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager {
@@ -90,6 +91,21 @@ export default class FilteredSearchDropdownManager {
gl: DropdownEmoji,
element: this.container.querySelector('#js-dropdown-my-reaction'),
},
+ wip: {
+ reference: null,
+ gl: DropdownNonUser,
+ element: this.container.querySelector('#js-dropdown-wip'),
+ },
+ status: {
+ reference: null,
+ gl: NullDropdown,
+ element: this.container.querySelector('#js-dropdown-admin-runner-status'),
+ },
+ type: {
+ reference: null,
+ gl: NullDropdown,
+ element: this.container.querySelector('#js-dropdown-admin-runner-type'),
+ },
};
supportedTokens.forEach((type) => {
@@ -125,10 +141,16 @@ export default class FilteredSearchDropdownManager {
return endpoint;
}
- static addWordToInput(tokenName, tokenValue = '', clicked = false) {
+ static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) {
+ const {
+ uppercaseTokenName = false,
+ capitalizeTokenValue = false,
+ } = options;
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
-
- FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
+ FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, {
+ uppercaseTokenName,
+ capitalizeTokenValue,
+ });
input.value = '';
if (clicked) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 81286c54c4c..54533ebb70d 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -3,10 +3,10 @@ import {
getParameterByName,
getUrlParamsArray,
} from '~/lib/utils/common_utils';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import FilteredSearchContainer from './container';
-import FilteredSearchTokenKeys from './filtered_search_token_keys';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service';
@@ -23,7 +23,7 @@ export default class FilteredSearchManager {
isGroup = false,
isGroupAncestor = true,
isGroupDecendent = false,
- filteredSearchTokenKeys = FilteredSearchTokenKeys,
+ filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
}) {
this.isGroup = isGroup;
@@ -405,7 +405,10 @@ export default class FilteredSearchManager {
if (isLastVisualTokenValid) {
tokens.forEach((t) => {
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
- FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
+ FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, {
+ uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key),
+ capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(t.key),
+ });
});
const fragments = searchToken.split(':');
@@ -421,7 +424,10 @@ export default class FilteredSearchManager {
FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
}
- FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
+ FilteredSearchVisualTokens.addFilterVisualToken(tokenKey, null, {
+ uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(tokenKey),
+ capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey),
+ });
input.value = input.value.replace(`${tokenKey}:`, '');
}
} else {
@@ -429,7 +435,10 @@ export default class FilteredSearchManager {
const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
- FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
+ const tokenKey = FilteredSearchVisualTokens.getLastTokenPartial();
+ FilteredSearchVisualTokens.addFilterVisualToken(searchToken, null, {
+ capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey),
+ });
// Trim the last space as seen in the if statement above
input.value = input.value.replace(searchToken, '').trim();
@@ -480,7 +489,7 @@ export default class FilteredSearchManager {
FilteredSearchVisualTokens.addFilterVisualToken(
condition.tokenKey,
condition.value,
- canEdit,
+ { canEdit },
);
} else {
// Sanitize value since URL converts spaces into +
@@ -506,10 +515,15 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
+ const { uppercaseTokenName, capitalizeTokenValue } = match;
FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
- canEdit,
+ {
+ canEdit,
+ uppercaseTokenName,
+ capitalizeTokenValue,
+ },
);
} else if (!match && keyParam === 'assignee_id') {
const id = parseInt(value, 10);
@@ -517,7 +531,7 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'assignee';
const canEdit = this.canEdit && this.canEdit(tokenName);
- FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
+ FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit });
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
@@ -525,7 +539,7 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'author';
const canEdit = this.canEdit && this.canEdit(tokenName);
- FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
+ FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit });
}
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true;
@@ -561,15 +575,17 @@ export default class FilteredSearchManager {
this.saveCurrentSearchQuery();
- const { tokens, searchToken }
- = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys());
+ const tokenKeys = this.filteredSearchTokenKeys.getKeys();
+ const { tokens, searchToken } = this.tokenizer.processTokens(searchQuery, tokenKeys);
const currentState = state || getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
tokens.forEach((token) => {
const condition = this.filteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
- const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
+ const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
+ const { param } = tokenConfig;
+
// Replace hyphen with underscore to use as request parameter
// e.g. 'my-reaction' => 'my_reaction'
const underscoredKey = token.key.replace('-', '_');
@@ -581,6 +597,10 @@ export default class FilteredSearchManager {
} else {
let tokenValue = token.value;
+ if (tokenConfig.lowercaseValueOnSubmit) {
+ tokenValue = tokenValue.toLowerCase();
+ }
+
if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index 087ef5cd6f2..a09ad3e4758 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -1,103 +1,48 @@
-const tokenKeys = [{
- key: 'author',
- type: 'string',
- param: 'username',
- symbol: '@',
- icon: 'pencil',
- tag: '@author',
-}, {
- key: 'assignee',
- type: 'string',
- param: 'username',
- symbol: '@',
- icon: 'user',
- tag: '@assignee',
-}, {
- key: 'milestone',
- type: 'string',
- param: 'title',
- symbol: '%',
- icon: 'clock-o',
- tag: '%milestone',
-}, {
- key: 'label',
- type: 'array',
- param: 'name[]',
- symbol: '~',
- icon: 'tag',
- tag: '~label',
-}];
-
-if (gon.current_user_id) {
- // Appending tokenkeys only logged-in
- tokenKeys.push({
- key: 'my-reaction',
- type: 'string',
- param: 'emoji',
- symbol: '',
- icon: 'thumbs-up',
- tag: 'emoji',
- });
-}
+export default class FilteredSearchTokenKeys {
+ constructor(tokenKeys = [], alternativeTokenKeys = [], conditions = []) {
+ this.tokenKeys = tokenKeys;
+ this.alternativeTokenKeys = alternativeTokenKeys;
+ this.conditions = conditions;
-const alternativeTokenKeys = [{
- key: 'label',
- type: 'string',
- param: 'name',
- symbol: '~',
-}];
-
-const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
-
-const conditions = [{
- url: 'assignee_id=0',
- tokenKey: 'assignee',
- value: 'none',
-}, {
- url: 'milestone_title=No+Milestone',
- tokenKey: 'milestone',
- value: 'none',
-}, {
- url: 'milestone_title=%23upcoming',
- tokenKey: 'milestone',
- value: 'upcoming',
-}, {
- url: 'milestone_title=%23started',
- tokenKey: 'milestone',
- value: 'started',
-}, {
- url: 'label_name[]=No+Label',
- tokenKey: 'label',
- value: 'none',
-}];
+ this.tokenKeysWithAlternative = this.tokenKeys.concat(this.alternativeTokenKeys);
+ }
-export default class FilteredSearchTokenKeys {
- static get() {
- return tokenKeys;
+ get() {
+ return this.tokenKeys;
+ }
+
+ getKeys() {
+ return this.tokenKeys.map(i => i.key);
+ }
+
+ getAlternatives() {
+ return this.alternativeTokenKeys;
}
- static getKeys() {
- return tokenKeys.map(i => i.key);
+ getConditions() {
+ return this.conditions;
}
- static getAlternatives() {
- return alternativeTokenKeys;
+ shouldUppercaseTokenName(tokenKey) {
+ const token = this.searchByKey(tokenKey.toLowerCase());
+ return token && token.uppercaseTokenName;
}
- static getConditions() {
- return conditions;
+ shouldCapitalizeTokenValue(tokenKey) {
+ const token = this.searchByKey(tokenKey.toLowerCase());
+ return token && token.capitalizeTokenValue;
}
- static searchByKey(key) {
- return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
+ searchByKey(key) {
+ return this.tokenKeys.find(tokenKey => tokenKey.key === key) || null;
}
- static searchBySymbol(symbol) {
- return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
+ searchBySymbol(symbol) {
+ return this.tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
}
- static searchByKeyParam(keyParam) {
- return tokenKeysWithAlternative.find((tokenKey) => {
+ searchByKeyParam(keyParam) {
+ return this.tokenKeysWithAlternative.find((tokenKey) => {
let tokenKeyParam = tokenKey.key;
// Replace hyphen with underscore to compare keyParam with tokenKeyParam
@@ -112,12 +57,29 @@ export default class FilteredSearchTokenKeys {
}) || null;
}
- static searchByConditionUrl(url) {
- return conditions.find(condition => condition.url === url) || null;
+ searchByConditionUrl(url) {
+ return this.conditions.find(condition => condition.url === url) || null;
}
- static searchByConditionKeyValue(key, value) {
- return conditions
+ searchByConditionKeyValue(key, value) {
+ return this.conditions
.find(condition => condition.tokenKey === key && condition.value === value) || null;
}
+
+ addExtraTokensForMergeRequests() {
+ const wipToken = {
+ key: 'wip',
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'admin',
+ tag: 'Yes or No',
+ lowercaseValueOnSubmit: true,
+ uppercaseTokenName: true,
+ capitalizeTokenValue: true,
+ };
+
+ this.tokenKeys.push(wipToken);
+ this.tokenKeysWithAlternative.push(wipToken);
+ }
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index 56fe1ab4e90..0854c1822fb 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -55,12 +55,18 @@ export default class FilteredSearchVisualTokens {
}
}
- static createVisualTokenElementHTML(canEdit = true) {
+ static createVisualTokenElementHTML(options = {}) {
+ const {
+ canEdit = true,
+ uppercaseTokenName = false,
+ capitalizeTokenValue = false,
+ } = options;
+
return `
<div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
- <div class="name"></div>
+ <div class="${uppercaseTokenName ? 'text-uppercase' : ''} name"></div>
<div class="value-container">
- <div class="value"></div>
+ <div class="${capitalizeTokenValue ? 'text-capitalize' : ''} value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
@@ -182,16 +188,26 @@ export default class FilteredSearchVisualTokens {
}
}
- static addVisualTokenElement(name, value, isSearchTerm, canEdit) {
+ static addVisualTokenElement(name, value, options = {}) {
+ const {
+ isSearchTerm = false,
+ canEdit,
+ uppercaseTokenName,
+ capitalizeTokenValue,
+ } = options;
const li = document.createElement('li');
li.classList.add('js-visual-token');
li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
if (value) {
- li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit);
+ li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({
+ canEdit,
+ uppercaseTokenName,
+ capitalizeTokenValue,
+ });
FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value);
} else {
- li.innerHTML = '<div class="name"></div>';
+ li.innerHTML = `<div class="${uppercaseTokenName ? 'text-uppercase' : ''} name"></div>`;
}
li.querySelector('.name').innerText = name;
@@ -212,20 +228,32 @@ export default class FilteredSearchVisualTokens {
}
}
- static addFilterVisualToken(tokenName, tokenValue, canEdit) {
+ static addFilterVisualToken(tokenName, tokenValue, {
+ canEdit,
+ uppercaseTokenName = false,
+ capitalizeTokenValue = false,
+ } = {}) {
const { lastVisualToken, isLastVisualTokenValid }
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { addVisualTokenElement } = FilteredSearchVisualTokens;
if (isLastVisualTokenValid) {
- addVisualTokenElement(tokenName, tokenValue, false, canEdit);
+ addVisualTokenElement(tokenName, tokenValue, {
+ canEdit,
+ uppercaseTokenName,
+ capitalizeTokenValue,
+ });
} else {
const previousTokenName = lastVisualToken.querySelector('.name').innerText;
const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
tokensContainer.removeChild(lastVisualToken);
const value = tokenValue || tokenName;
- addVisualTokenElement(previousTokenName, value, false, canEdit);
+ addVisualTokenElement(previousTokenName, value, {
+ canEdit,
+ uppercaseTokenName,
+ capitalizeTokenValue,
+ });
}
}
@@ -235,7 +263,9 @@ export default class FilteredSearchVisualTokens {
if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`;
} else {
- FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true);
+ FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, {
+ isSearchTerm: true,
+ });
}
}
@@ -306,7 +336,9 @@ export default class FilteredSearchVisualTokens {
let value;
if (token.classList.contains('filtered-search-token')) {
- FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText);
+ FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText, null, {
+ uppercaseTokenName: nameElement.classList.contains('text-uppercase'),
+ });
const valueContainerElement = token.querySelector('.value-container');
value = valueContainerElement.dataset.originalValue;
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
new file mode 100644
index 00000000000..cc7291c9f59
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -0,0 +1,77 @@
+import FilteredSearchTokenKeys from './filtered_search_token_keys';
+
+export const tokenKeys = [{
+ key: 'author',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ icon: 'pencil',
+ tag: '@author',
+}, {
+ key: 'assignee',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ icon: 'user',
+ tag: '@assignee',
+}, {
+ key: 'milestone',
+ type: 'string',
+ param: 'title',
+ symbol: '%',
+ icon: 'clock',
+ tag: '%milestone',
+}, {
+ key: 'label',
+ type: 'array',
+ param: 'name[]',
+ symbol: '~',
+ icon: 'labels',
+ tag: '~label',
+}];
+
+if (gon.current_user_id) {
+ // Appending tokenkeys only logged-in
+ tokenKeys.push({
+ key: 'my-reaction',
+ type: 'string',
+ param: 'emoji',
+ symbol: '',
+ icon: 'thumb-up',
+ tag: 'emoji',
+ });
+}
+
+export const alternativeTokenKeys = [{
+ key: 'label',
+ type: 'string',
+ param: 'name',
+ symbol: '~',
+}];
+
+export const conditions = [{
+ url: 'assignee_id=0',
+ tokenKey: 'assignee',
+ value: 'none',
+}, {
+ url: 'milestone_title=No+Milestone',
+ tokenKey: 'milestone',
+ value: 'none',
+}, {
+ url: 'milestone_title=%23upcoming',
+ tokenKey: 'milestone',
+ value: 'upcoming',
+}, {
+ url: 'milestone_title=%23started',
+ tokenKey: 'milestone',
+ value: 'started',
+}, {
+ url: 'label_name[]=No+Label',
+ tokenKey: 'label',
+ value: 'none',
+}];
+
+const IssuableFilteredSearchTokenKeys =
+ new FilteredSearchTokenKeys(tokenKeys, alternativeTokenKeys, conditions);
+
+export default IssuableFilteredSearchTokenKeys;
diff --git a/app/assets/javascripts/filtered_search/null_dropdown.js b/app/assets/javascripts/filtered_search/null_dropdown.js
new file mode 100644
index 00000000000..4cfce2a5beb
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/null_dropdown.js
@@ -0,0 +1,9 @@
+import FilteredSearchDropdown from './filtered_search_dropdown';
+
+export default class NullDropdown extends FilteredSearchDropdown {
+ renderContent(forceShowList = false) {
+ this.droplab.changeHookList(this.hookId, this.dropdown, [], this.config);
+
+ super.renderContent(forceShowList);
+ }
+}
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 8b4f3b05ee7..f820f0dc3f0 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -65,8 +65,8 @@ export const hideMenu = (el) => {
const parentEl = el.parentNode;
- el.style.display = ''; // eslint-disable-line no-param-reassign
- el.style.transform = ''; // eslint-disable-line no-param-reassign
+ el.style.display = '';
+ el.style.transform = '';
el.classList.remove(IS_ABOVE_CLASS);
parentEl.classList.remove(IS_OVER_CLASS);
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 2f030de8967..70a8838b772 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -1,6 +1,5 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import AccessorUtilities from '~/lib/utils/accessor';
import eventHub from '../event_hub';
import store from '../store/';
@@ -13,7 +12,6 @@ import frequentItemsMixin from './frequent_items_mixin';
export default {
store,
components: {
- LoadingIcon,
FrequentItemsSearchInput,
FrequentItemsList,
},
@@ -98,11 +96,11 @@ export default {
<frequent-items-search-input
:namespace="namespace"
/>
- <loading-icon
+ <gl-loading-icon
v-if="isLoadingItems"
:label="translations.loadingMessage"
+ :size="2"
class="loading-animation prepend-top-20"
- size="2"
/>
<div
v-if="!isLoadingItems && !hasSearchQuery"
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 1f1665ff7fe..2399ee15332 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,5 +1,5 @@
<script>
-/* eslint-disable vue/require-default-prop, vue/require-prop-types */
+/* eslint-disable vue/require-default-prop */
import Identicon from '../../vue_shared/components/identicon.vue';
export default {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 73b2cd0b2c7..95636a9ccdd 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -15,6 +15,7 @@ export const defaultAutocompleteConfig = {
epics: true,
milestones: true,
labels: true,
+ snippets: true,
};
class GfmAutoComplete {
@@ -50,6 +51,7 @@ class GfmAutoComplete {
if (this.enableMap.milestones) this.setupMilestones($input);
if (this.enableMap.mergeRequests) this.setupMergeRequests($input);
if (this.enableMap.labels) this.setupLabels($input);
+ if (this.enableMap.snippets) this.setupSnippets($input);
// We don't instantiate the quick actions autocomplete for note and issue/MR edit forms
$input.filter('[data-supports-quick-actions="true"]').atwho({
@@ -360,6 +362,39 @@ class GfmAutoComplete {
});
}
+ setupSnippets($input) {
+ $input.atwho({
+ at: '$',
+ alias: 'snippets',
+ searchKey: 'search',
+ displayTpl(value) {
+ let tmpl = GfmAutoComplete.Loading.template;
+ if (value.title != null) {
+ tmpl = GfmAutoComplete.Issues.template;
+ }
+ return tmpl;
+ },
+ data: GfmAutoComplete.defaultLoadingData,
+ // eslint-disable-next-line no-template-curly-in-string
+ insertTpl: '${atwho-at}${id}',
+ callbacks: {
+ ...this.getDefaultCallbacks(),
+ beforeSave(snippets) {
+ return $.map(snippets, (m) => {
+ if (m.title == null) {
+ return m;
+ }
+ return {
+ id: m.id,
+ title: sanitize(m.title),
+ search: `${m.id} ${m.title}`,
+ };
+ });
+ },
+ },
+ });
+ }
+
getDefaultCallbacks() {
const fetchData = this.fetchData.bind(this);
@@ -470,7 +505,7 @@ class GfmAutoComplete {
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
- const atSymbolsWithBar = Object.keys(controllers).join('|');
+ const atSymbolsWithBar = Object.keys(controllers).join('|').replace(/[$]/, '\\$&');
const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
@@ -497,6 +532,7 @@ GfmAutoComplete.atTypeMap = {
'~': 'labels',
'%': 'milestones',
'/': 'commands',
+ $: 'snippets',
};
// Emoji
@@ -519,7 +555,7 @@ GfmAutoComplete.Labels = {
// eslint-disable-next-line no-template-curly-in-string
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
};
-// Issues and MergeRequests
+// Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = {
// eslint-disable-next-line no-template-curly-in-string
template: '<li><small>${id}</small> ${title}</li>',
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index c74de7ac34d..e672284a2d0 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -18,7 +18,7 @@ export default class GLForm {
});
// Before we start, we should clean up any previous data for this form
this.destroy();
- // Setup the form
+ // Set up the form
this.setupForm();
this.form.data('glForm', this);
}
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index b0765747a36..a032f291546 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -2,23 +2,32 @@
/* global Flash */
import $ from 'jquery';
-import { s__ } from '~/locale';
-import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+import { s__, sprintf } from '~/locale';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
-import { COMMON_STR } from '../constants';
+import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
import groupsComponent from './groups.vue';
export default {
components: {
- loadingIcon,
DeprecatedModal,
groupsComponent,
},
props: {
+ action: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ containerId: {
+ type: String,
+ required: false,
+ default: '',
+ },
store: {
type: Object,
required: true,
@@ -56,31 +65,28 @@ export default {
? COMMON_STR.GROUP_SEARCH_EMPTY
: COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
- eventHub.$on('fetchPage', this.fetchPage);
- eventHub.$on('toggleChildren', this.toggleChildren);
- eventHub.$on('showLeaveGroupModal', this.showLeaveGroupModal);
- eventHub.$on('updatePagination', this.updatePagination);
- eventHub.$on('updateGroups', this.updateGroups);
+ eventHub.$on(`${this.action}fetchPage`, this.fetchPage);
+ eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren);
+ eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
+ eventHub.$on(`${this.action}updatePagination`, this.updatePagination);
+ eventHub.$on(`${this.action}updateGroups`, this.updateGroups);
},
mounted() {
this.fetchAllGroups();
+
+ if (this.containerId) {
+ this.containerEl = document.getElementById(this.containerId);
+ }
},
beforeDestroy() {
- eventHub.$off('fetchPage', this.fetchPage);
- eventHub.$off('toggleChildren', this.toggleChildren);
- eventHub.$off('showLeaveGroupModal', this.showLeaveGroupModal);
- eventHub.$off('updatePagination', this.updatePagination);
- eventHub.$off('updateGroups', this.updateGroups);
+ eventHub.$off(`${this.action}fetchPage`, this.fetchPage);
+ eventHub.$off(`${this.action}toggleChildren`, this.toggleChildren);
+ eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
+ eventHub.$off(`${this.action}updatePagination`, this.updatePagination);
+ eventHub.$off(`${this.action}updateGroups`, this.updateGroups);
},
methods: {
- fetchGroups({
- parentId,
- page,
- filterGroupsBy,
- sortBy,
- archived,
- updatePagination,
- }) {
+ fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
return this.service
.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
.then(res => {
@@ -165,13 +171,13 @@ export default {
}
},
showLeaveGroupModal(group, parentGroup) {
+ const { fullName } = group;
this.targetGroup = group;
this.targetParentGroup = parentGroup;
this.showModal = true;
- this.groupLeaveConfirmationMessage = s__(
- `GroupsTree|Are you sure you want to leave the "${
- group.fullName
- }" group?`,
+ this.groupLeaveConfirmationMessage = sprintf(
+ s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'),
+ { fullName },
);
},
hideLeaveGroupModal() {
@@ -197,16 +203,35 @@ export default {
this.targetGroup.isBeingRemoved = false;
});
},
+ showEmptyState() {
+ const { containerEl } = this;
+ const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
+ const emptyStateEl = containerEl.querySelector('.empty-state');
+
+ if (contentListEl) {
+ contentListEl.remove();
+ }
+
+ if (emptyStateEl) {
+ emptyStateEl.classList.remove(HIDDEN_CLASS);
+ }
+ },
updatePagination(headers) {
this.store.setPaginationInfo(headers);
},
updateGroups(groups, fromSearch) {
- this.isSearchEmpty = groups ? groups.length === 0 : false;
+ const hasGroups = groups && groups.length > 0;
+ this.isSearchEmpty = !hasGroups;
+
if (fromSearch) {
this.store.setSearchedGroups(groups);
} else {
this.store.setGroups(groups);
}
+
+ if (this.action && !hasGroups && !fromSearch) {
+ this.showEmptyState();
+ }
},
},
};
@@ -214,11 +239,11 @@ export default {
<template>
<div>
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
:label="s__('GroupsTree|Loading groups')"
+ :size="2"
class="loading-animation prepend-top-20"
- size="2"
/>
<groups-component
v-if="!isLoading"
@@ -226,6 +251,7 @@ export default {
:search-empty="isSearchEmpty"
:search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
+ :action="action"
/>
<deprecated-modal
v-show="showModal"
diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue
index 647c9d0046d..bcc7a638346 100644
--- a/app/assets/javascripts/groups/components/group_folder.vue
+++ b/app/assets/javascripts/groups/components/group_folder.vue
@@ -11,8 +11,12 @@ export default {
},
groups: {
type: Array,
+ required: true,
+ },
+ action: {
+ type: String,
required: false,
- default: () => ([]),
+ default: '',
},
},
computed: {
@@ -37,6 +41,7 @@ export default {
:key="index"
:group="group"
:parent-group="parentGroup"
+ :action="action"
/>
<li
v-if="hasMoreChildren"
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 2b9e2a929fc..44d6fa26914 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -30,6 +30,11 @@ export default {
type: Object,
required: true,
},
+ action: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
groupDomId() {
@@ -56,10 +61,12 @@ export default {
methods: {
onClickRowGroup(e) {
const NO_EXPAND_CLS = 'no-expand';
- if (!(e.target.classList.contains(NO_EXPAND_CLS) ||
- e.target.parentElement.classList.contains(NO_EXPAND_CLS))) {
+ const targetClasses = e.target.classList;
+ const parentElClasses = e.target.parentElement.classList;
+
+ if (!(targetClasses.contains(NO_EXPAND_CLS) || parentElClasses.contains(NO_EXPAND_CLS))) {
if (this.hasChildren) {
- eventHub.$emit('toggleChildren', this.group);
+ eventHub.$emit(`${this.action}toggleChildren`, this.group);
} else {
visitUrl(this.group.relativePath);
}
@@ -93,7 +100,7 @@ export default {
</div>
<div
:class="{ 'content-loading': group.isChildrenLoading }"
- class="avatar-container s24 d-none d-sm-block"
+ class="avatar-container s24 d-none d-sm-flex"
>
<a
:href="group.relativePath"
@@ -158,6 +165,7 @@ export default {
v-if="group.isOpen && hasChildren"
:parent-group="group"
:groups="group.children"
+ :action="action"
/>
</li>
</template>
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index adde8c8cdb3..81b2e5ea37b 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -1,57 +1,66 @@
<script>
- import tablePagination from '~/vue_shared/components/table_pagination.vue';
- import eventHub from '../event_hub';
- import { getParameterByName } from '../../lib/utils/common_utils';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import eventHub from '../event_hub';
+import { getParameterByName } from '../../lib/utils/common_utils';
- export default {
- components: {
- tablePagination,
+export default {
+ components: {
+ PaginationLinks,
+ },
+ props: {
+ groups: {
+ type: Array,
+ required: true,
},
- props: {
- groups: {
- type: Array,
- required: true,
- },
- pageInfo: {
- type: Object,
- required: true,
- },
- searchEmpty: {
- type: Boolean,
- required: true,
- },
- searchEmptyMessage: {
- type: String,
- required: true,
- },
+ pageInfo: {
+ type: Object,
+ required: true,
},
- methods: {
- change(page) {
- const filterGroupsParam = getParameterByName('filter_groups');
- const sortParam = getParameterByName('sort');
- const archivedParam = getParameterByName('archived');
- eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
- },
+ searchEmpty: {
+ type: Boolean,
+ required: true,
},
- };
+ searchEmptyMessage: {
+ type: String,
+ required: true,
+ },
+ action: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ methods: {
+ change(page) {
+ const filterGroupsParam = getParameterByName('filter_groups');
+ const sortParam = getParameterByName('sort');
+ const archivedParam = getParameterByName('archived');
+ eventHub.$emit(`${this.action}fetchPage`, page, filterGroupsParam, sortParam, archivedParam);
+ },
+ },
+};
</script>
<template>
- <div class="groups-list-tree-container">
+ <div class="groups-list-tree-container qa-groups-list-tree-container">
<div
v-if="searchEmpty"
class="has-no-search-results"
>
{{ searchEmptyMessage }}
</div>
- <group-folder
- v-if="!searchEmpty"
- :groups="groups"
- />
- <table-pagination
- v-if="!searchEmpty"
- :change="change"
- :page-info="pageInfo"
- />
+ <template
+ v-else
+ >
+ <group-folder
+ :groups="groups"
+ :action="action"
+ />
+ <pagination-links
+ :change="change"
+ :page-info="pageInfo"
+ class="d-flex justify-content-center prepend-top-default"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index 24eec4901ec..c1783d5ce25 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -21,6 +21,11 @@ export default {
type: Object,
required: true,
},
+ action: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
leaveBtnTitle() {
@@ -32,7 +37,7 @@ export default {
},
methods: {
onLeaveGroup() {
- eventHub.$emit('showLeaveGroupModal', this.group, this.parentGroup);
+ eventHub.$emit(`${this.action}showLeaveGroupModal`, this.group, this.parentGroup);
},
},
};
@@ -41,8 +46,8 @@ export default {
<template>
<div class="controls">
<a
- v-tooltip
v-if="group.canEdit"
+ v-tooltip
:href="group.editPath"
:title="editBtnTitle"
:aria-label="editBtnTitle"
@@ -52,8 +57,8 @@ export default {
<icon name="settings"/>
</a>
<a
- v-tooltip
v-if="group.canLeave"
+ v-tooltip
:href="group.leavePath"
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js
index b8baed682f5..9c246cf3ba6 100644
--- a/app/assets/javascripts/groups/constants.js
+++ b/app/assets/javascripts/groups/constants.js
@@ -2,13 +2,23 @@ import { __, s__ } from '../locale';
export const MAX_CHILDREN_COUNT = 20;
+export const ACTIVE_TAB_SUBGROUPS_AND_PROJECTS = 'subgroups_and_projects';
+export const ACTIVE_TAB_SHARED = 'shared';
+export const ACTIVE_TAB_ARCHIVED = 'archived';
+
+export const GROUPS_LIST_HOLDER_CLASS = '.js-groups-list-holder';
+export const GROUPS_FILTER_FORM_CLASS = '.js-group-filter-form';
+export const CONTENT_LIST_CLASS = '.content-list';
+
export const COMMON_STR = {
FAILURE: __('An error occurred. Please try again.'),
- LEAVE_FORBIDDEN: s__('GroupsTree|Failed to leave the group. Please make sure you are not the only owner.'),
+ LEAVE_FORBIDDEN: s__(
+ 'GroupsTree|Failed to leave the group. Please make sure you are not the only owner.',
+ ),
LEAVE_BTN_TITLE: s__('GroupsTree|Leave this group'),
EDIT_BTN_TITLE: s__('GroupsTree|Edit group'),
- GROUP_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups matched your search'),
- GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups or projects matched your search'),
+ GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'),
+ GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'),
};
export const ITEM_TYPE = {
@@ -17,8 +27,12 @@ export const ITEM_TYPE = {
};
export const GROUP_VISIBILITY_TYPE = {
- public: __('Public - The group and any public projects can be viewed without any authentication.'),
- internal: __('Internal - The group and any internal projects can be viewed by any logged in user.'),
+ public: __(
+ 'Public - The group and any public projects can be viewed without any authentication.',
+ ),
+ internal: __(
+ 'Internal - The group and any internal projects can be viewed by any logged in user.',
+ ),
private: __('Private - The group and its projects can only be viewed by members.'),
};
diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js
index e6db1746487..693519729ac 100644
--- a/app/assets/javascripts/groups/groups_filterable_list.js
+++ b/app/assets/javascripts/groups/groups_filterable_list.js
@@ -4,13 +4,23 @@ import eventHub from './event_hub';
import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils';
export default class GroupFilterableList extends FilterableList {
- constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) {
+ constructor({
+ form,
+ filter,
+ holder,
+ filterEndpoint,
+ pagePath,
+ dropdownSel,
+ filterInputField,
+ action,
+ }) {
super(form, filter, holder, filterInputField);
this.form = form;
this.filterEndpoint = filterEndpoint;
this.pagePath = pagePath;
this.filterInputField = filterInputField;
this.$dropdown = $(dropdownSel);
+ this.action = action;
}
getFilterEndpoint() {
@@ -20,15 +30,16 @@ export default class GroupFilterableList extends FilterableList {
getPagePath(queryData) {
const params = queryData ? $.param(queryData) : '';
const queryString = params ? `?${params}` : '';
- return `${this.pagePath}${queryString}`;
+ const path = this.pagePath || window.location.pathname;
+ return `${path}${queryString}`;
}
bindEvents() {
super.bindEvents();
- this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
+ this.onFilterOptionClickWrapper = this.onOptionClick.bind(this);
- this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
+ this.$dropdown.on('click', 'a', this.onFilterOptionClickWrapper);
}
onFilterInput() {
@@ -53,7 +64,12 @@ export default class GroupFilterableList extends FilterableList {
}
setDefaultFilterOption() {
- const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').first().text());
+ const defaultOption = $.trim(
+ this.$dropdown
+ .find('.dropdown-menu li.js-filter-sort-order a')
+ .first()
+ .text(),
+ );
this.$dropdown.find('.dropdown-label').text(defaultOption);
}
@@ -65,11 +81,19 @@ export default class GroupFilterableList extends FilterableList {
// Get type of option selected from dropdown
const currentTargetClassList = e.currentTarget.parentElement.classList;
const isOptionFilterBySort = currentTargetClassList.contains('js-filter-sort-order');
- const isOptionFilterByArchivedProjects = currentTargetClassList.contains('js-filter-archived-projects');
+ const isOptionFilterByArchivedProjects = currentTargetClassList.contains(
+ 'js-filter-archived-projects',
+ );
// Get option query param, also preserve currently applied query param
- const sortParam = getParameterByName('sort', isOptionFilterBySort ? e.currentTarget.href : window.location.href);
- const archivedParam = getParameterByName('archived', isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href);
+ const sortParam = getParameterByName(
+ 'sort',
+ isOptionFilterBySort ? e.currentTarget.href : window.location.href,
+ );
+ const archivedParam = getParameterByName(
+ 'archived',
+ isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href,
+ );
if (sortParam) {
queryData.sort = sortParam;
@@ -86,7 +110,9 @@ export default class GroupFilterableList extends FilterableList {
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').removeClass('is-active');
} else if (isOptionFilterByArchivedProjects) {
- this.$dropdown.find('.dropdown-menu li.js-filter-archived-projects a').removeClass('is-active');
+ this.$dropdown
+ .find('.dropdown-menu li.js-filter-archived-projects a')
+ .removeClass('is-active');
}
$(e.target).addClass('is-active');
@@ -98,11 +124,19 @@ export default class GroupFilterableList extends FilterableList {
onFilterSuccess(res, queryData) {
const currentPath = this.getPagePath(queryData);
- window.history.replaceState({
- page: currentPath,
- }, document.title, currentPath);
-
- eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
- eventHub.$emit('updatePagination', normalizeHeaders(res.headers));
+ window.history.replaceState(
+ {
+ page: currentPath,
+ },
+ document.title,
+ currentPath,
+ );
+
+ eventHub.$emit(
+ `${this.action}updateGroups`,
+ res.data,
+ Object.prototype.hasOwnProperty.call(queryData, this.filterInputField),
+ );
+ eventHub.$emit(`${this.action}updatePagination`, normalizeHeaders(res.headers));
}
}
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index 83a9008a94b..0f68f05b523 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -7,18 +7,26 @@ import GroupsService from './service/groups_service';
import groupsApp from './components/app.vue';
import groupFolderComponent from './components/group_folder.vue';
import groupItemComponent from './components/group_item.vue';
+import { GROUPS_LIST_HOLDER_CLASS, CONTENT_LIST_CLASS } from './constants';
Vue.use(Translate);
-export default () => {
- const el = document.getElementById('js-groups-tree');
+export default (containerId = 'js-groups-tree', endpoint, action = '') => {
+ const containerEl = document.getElementById(containerId);
+ let dataEl;
// Don't do anything if element doesn't exist (No groups)
// This is for when the user enters directly to the page via URL
- if (!el) {
+ if (!containerEl) {
return;
}
+ const el = action ? containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS) : containerEl;
+
+ if (action) {
+ dataEl = containerEl.querySelector(CONTENT_LIST_CLASS);
+ }
+
Vue.component('group-folder', groupFolderComponent);
Vue.component('group-item', groupItemComponent);
@@ -29,20 +37,26 @@ export default () => {
groupsApp,
},
data() {
- const { dataset } = this.$options.el;
+ const { dataset } = dataEl || this.$options.el;
const hideProjects = dataset.hideProjects === 'true';
+ const service = new GroupsService(endpoint || dataset.endpoint);
const store = new GroupsStore(hideProjects);
- const service = new GroupsService(dataset.endpoint);
return {
+ action,
store,
service,
hideProjects,
loading: true,
+ containerId,
};
},
beforeMount() {
- const { dataset } = this.$options.el;
+ if (this.action) {
+ return;
+ }
+
+ const { dataset } = dataEl || this.$options.el;
let groupFilterList = null;
const form = document.querySelector(dataset.formSel);
const filter = document.querySelector(dataset.filterSel);
@@ -52,10 +66,11 @@ export default () => {
form,
filter,
holder,
- filterEndpoint: dataset.endpoint,
+ filterEndpoint: endpoint || dataset.endpoint,
pagePath: dataset.path,
dropdownSel: dataset.dropdownSel,
filterInputField: 'filter',
+ action: this.action,
};
groupFilterList = new GroupFilterableList(opts);
@@ -64,9 +79,11 @@ export default () => {
render(createElement) {
return createElement('groups-app', {
props: {
+ action: this.action,
store: this.store,
service: this.service,
hideProjects: this.hideProjects,
+ containerId: this.containerId,
},
});
},
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 4ae3a714bee..175d0b8498b 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,5 +1,9 @@
import $ from 'jquery';
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility';
+import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
+import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
/**
* Updates todo counter when todos are toggled.
@@ -17,3 +21,54 @@ export default function initTodoToggle() {
$todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
}
+
+document.addEventListener('DOMContentLoaded', () => {
+ const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
+ const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
+
+ if (setStatusModalTriggerEl || setStatusModalWrapperEl) {
+ Vue.use(Translate);
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: setStatusModalTriggerEl,
+ data() {
+ const { hasStatus } = this.$options.el.dataset;
+
+ return {
+ hasStatus: hasStatus === 'true',
+ };
+ },
+ render(createElement) {
+ return createElement(SetStatusModalTrigger, {
+ props: {
+ hasStatus: this.hasStatus,
+ },
+ });
+ },
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: setStatusModalWrapperEl,
+ data() {
+ const { currentEmoji, currentMessage } = this.$options.el.dataset;
+
+ return {
+ currentEmoji,
+ currentMessage,
+ };
+ },
+ render(createElement) {
+ const { currentEmoji, currentMessage } = this;
+
+ return createElement(SetStatusModalWrapper, {
+ props: {
+ currentEmoji,
+ currentMessage,
+ },
+ });
+ },
+ });
+ }
+});
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
index 6db7b9d6b0e..52ccc537c9d 100644
--- a/app/assets/javascripts/ide/components/branches/search_list.vue
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -1,13 +1,11 @@
<script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import Item from './item.vue';
export default {
components: {
- LoadingIcon,
Item,
Icon,
},
@@ -62,8 +60,8 @@ export default {
<div class="position-relative">
<input
ref="searchInput"
- :placeholder="__('Search branches')"
v-model="search"
+ :placeholder="__('Search branches')"
type="search"
class="form-control dropdown-input-field"
@input="searchBranches"
@@ -76,10 +74,10 @@ export default {
</div>
</div>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
+ :size="2"
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
- size="2"
/>
<ul
v-else
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
new file mode 100644
index 00000000000..b0e60edcbe5
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -0,0 +1,79 @@
+<script>
+import $ from 'jquery';
+import { mapActions } from 'vuex';
+import { __ } from '~/locale';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
+
+export default {
+ components: {
+ FileIcon,
+ ChangedFileIcon,
+ },
+ props: {
+ activeFile: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ activeButtonText() {
+ return this.activeFile.staged ? __('Unstage') : __('Stage');
+ },
+ isStaged() {
+ return !this.activeFile.changed && this.activeFile.staged;
+ },
+ },
+ methods: {
+ ...mapActions(['stageChange', 'unstageChange']),
+ actionButtonClicked() {
+ if (this.activeFile.staged) {
+ this.unstageChange(this.activeFile.path);
+ } else {
+ this.stageChange(this.activeFile.path);
+ }
+ },
+ showDiscardModal() {
+ $(document.getElementById(`discard-file-${this.activeFile.path}`)).modal('show');
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex ide-commit-editor-header align-items-center">
+ <file-icon
+ :file-name="activeFile.name"
+ :size="16"
+ class="mr-2"
+ />
+ <strong class="mr-2">
+ {{ activeFile.path }}
+ </strong>
+ <changed-file-icon
+ :file="activeFile"
+ class="ml-0"
+ />
+ <div class="ml-auto">
+ <button
+ v-if="!isStaged"
+ type="button"
+ class="btn btn-remove btn-inverted append-right-8"
+ @click="showDiscardModal"
+ >
+ {{ __('Discard') }}
+ </button>
+ <button
+ :class="{
+ 'btn-success': !isStaged,
+ 'btn-warning': isStaged
+ }"
+ type="button"
+ class="btn btn-inverted"
+ @click="actionButtonClicked"
+ >
+ {{ activeButtonText }}
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index d0fb0e3d99e..3e3539e364b 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,7 +1,9 @@
<script>
+import $ from 'jquery';
import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
@@ -9,6 +11,7 @@ export default {
components: {
Icon,
ListItem,
+ GlModal,
},
directives: {
tooltip,
@@ -56,6 +59,11 @@ export default {
type: String,
required: true,
},
+ emptyStateText: {
+ type: String,
+ required: false,
+ default: __('No changes'),
+ },
},
computed: {
titleText() {
@@ -68,11 +76,19 @@ export default {
},
},
methods: {
- ...mapActions(['stageAllChanges', 'unstageAllChanges']),
+ ...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']),
actionBtnClicked() {
this[this.action]();
+
+ $(this.$refs.actionBtn).tooltip('hide');
+ },
+ openDiscardModal() {
+ $('#discard-all-changes').modal('show');
},
},
+ discardModalText: __(
+ "You will loose all the unstaged changes you've made in this project. This action cannot be undone.",
+ ),
};
</script>
@@ -81,27 +97,32 @@ export default {
class="ide-commit-list-container"
>
<header
- class="multi-file-commit-panel-header"
+ class="multi-file-commit-panel-header d-flex mb-0"
>
<div
- class="multi-file-commit-panel-header-title"
+ class="d-flex align-items-center flex-fill"
>
<icon
v-once
:name="iconName"
:size="18"
+ class="append-right-8"
/>
- {{ titleText }}
+ <strong>
+ {{ titleText }}
+ </strong>
<div class="d-flex ml-auto">
<button
+ ref="actionBtn"
v-tooltip
- v-show="filesLength"
+ :title="actionBtnText"
+ :aria-label="actionBtnText"
+ :disabled="!filesLength"
:class="{
- 'd-flex': filesLength
+ 'disabled-content': !filesLength
}"
- :title="actionBtnText"
type="button"
- class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center"
+ class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
data-placement="bottom"
data-container="body"
data-boundary="viewport"
@@ -109,18 +130,32 @@ export default {
>
<icon
:name="actionBtnIcon"
- :size="12"
+ :size="16"
class="ml-auto mr-auto"
/>
</button>
- <span
+ <button
+ v-if="!stagedList"
+ v-tooltip
+ :title="__('Discard all changes')"
+ :aria-label="__('Discard all changes')"
+ :disabled="!filesLength"
:class="{
- 'rounded-right': !filesLength
+ 'disabled-content': !filesLength
}"
- class="ide-commit-file-count order-0 rounded-left text-center"
+ type="button"
+ class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
+ data-placement="bottom"
+ data-container="body"
+ data-boundary="viewport"
+ @click="openDiscardModal"
>
- {{ filesLength }}
- </span>
+ <icon
+ :size="16"
+ name="remove-all"
+ class="ml-auto mr-auto"
+ />
+ </button>
</div>
</div>
</header>
@@ -143,9 +178,19 @@ export default {
</ul>
<p
v-else
- class="multi-file-commit-list form-text text-muted"
+ class="multi-file-commit-list form-text text-muted text-center"
>
- {{ __('No changes') }}
+ {{ emptyStateText }}
</p>
+ <gl-modal
+ v-if="!stagedList"
+ id="discard-all-changes"
+ :footer-primary-button-text="__('Discard all changes')"
+ :header-title-text="__('Discard all unstaged changes?')"
+ footer-primary-button-variant="danger"
+ @submit="discardAllChanges"
+ >
+ {{ $options.discardModalText }}
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 391004dcd3c..ee0e72cd05f 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -2,6 +2,7 @@
import { mapActions } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
@@ -12,6 +13,7 @@ export default {
Icon,
StageButton,
UnstageButton,
+ FileIcon,
},
directives: {
tooltip,
@@ -48,7 +50,7 @@ export default {
return `${getCommitIconMap(this.file).icon}${suffix}`;
},
iconClass() {
- return `${getCommitIconMap(this.file).class} append-right-8`;
+ return `${getCommitIconMap(this.file).class} ml-auto mr-auto`;
},
fullKey() {
return `${this.keyPrefix}-${this.file.key}`;
@@ -105,17 +107,20 @@ export default {
@click="openFileInEditor"
>
<span class="multi-file-commit-list-file-path d-flex align-items-center">
- <icon
- :name="iconName"
- :size="16"
- :css-classes="iconClass"
+ <file-icon
+ :file-name="file.name"
+ class="append-right-8"
/>{{ file.name }}
</span>
+ <div class="ml-auto d-flex align-items-center">
+ <div class="d-flex align-items-center ide-commit-list-changed-icon">
+ <icon
+ :name="iconName"
+ :size="16"
+ :css-classes="iconClass"
+ />
+ </div>
+ </div>
</div>
- <component
- :is="actionComponent"
- :path="file.path"
- class="d-flex position-absolute"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
index e6044401c9f..8a1836a5c92 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -1,11 +1,15 @@
<script>
+import $ from 'jquery';
import { mapActions } from 'vuex';
+import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
export default {
components: {
Icon,
+ GlModal,
},
directives: {
tooltip,
@@ -16,8 +20,22 @@ export default {
required: true,
},
},
+ computed: {
+ modalId() {
+ return `discard-file-${this.path}`;
+ },
+ modalTitle() {
+ return sprintf(
+ __('Discard changes to %{path}?'),
+ { path: this.path },
+ );
+ },
+ },
methods: {
...mapActions(['stageChange', 'discardFileChanges']),
+ showDiscardModal() {
+ $(document.getElementById(this.modalId)).modal('show');
+ },
},
};
</script>
@@ -25,51 +43,50 @@ export default {
<template>
<div
v-once
- class="multi-file-discard-btn dropdown"
+ class="multi-file-discard-btn d-flex"
>
<button
v-tooltip
:aria-label="__('Stage changes')"
:title="__('Stage changes')"
type="button"
- class="btn btn-blank append-right-5 d-flex align-items-center"
+ class="btn btn-blank align-items-center"
data-container="body"
data-boundary="viewport"
data-placement="bottom"
- @click.stop="stageChange(path)"
+ @click.stop.prevent="stageChange(path)"
>
<icon
- :size="12"
+ :size="16"
name="mobile-issue-close"
+ class="ml-auto mr-auto"
/>
</button>
<button
v-tooltip
- :title="__('More actions')"
+ :aria-label="__('Discard changes')"
+ :title="__('Discard changes')"
type="button"
- class="btn btn-blank d-flex align-items-center"
+ class="btn btn-blank align-items-center"
data-container="body"
data-boundary="viewport"
data-placement="bottom"
- data-toggle="dropdown"
- data-display="static"
+ @click.stop.prevent="showDiscardModal"
>
<icon
- :size="12"
- name="ellipsis_h"
+ :size="16"
+ name="remove"
+ class="ml-auto mr-auto"
/>
</button>
- <div class="dropdown-menu dropdown-menu-right">
- <ul>
- <li>
- <button
- type="button"
- @click.stop="discardFileChanges(path)"
- >
- {{ __('Discard changes') }}
- </button>
- </li>
- </ul>
- </div>
+ <gl-modal
+ :id="modalId"
+ :header-title-text="modalTitle"
+ :footer-primary-button-text="__('Discard changes')"
+ footer-primary-button-variant="danger"
+ @submit="discardFileChanges(path)"
+ >
+ {{ __("You will loose all changes you've made to this file. This action cannot be undone.") }}
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
index 9cec73ec00e..86c40602074 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -25,22 +25,23 @@ export default {
<template>
<div
v-once
- class="multi-file-discard-btn"
+ class="multi-file-discard-btn d-flex"
>
<button
v-tooltip
:aria-label="__('Unstage changes')"
:title="__('Unstage changes')"
type="button"
- class="btn btn-blank d-flex align-items-center"
+ class="btn btn-blank align-items-center"
data-container="body"
data-boundary="viewport"
data-placement="bottom"
- @click="unstageChange(path)"
+ @click.stop.prevent="unstageChange(path)"
>
<icon
- :size="12"
- name="history"
+ :size="16"
+ name="redo"
+ class="ml-auto mr-auto"
/>
</button>
</div>
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index acbc98b7a7b..a20dc0a7006 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -1,11 +1,7 @@
<script>
import { mapActions } from 'vuex';
-import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
- components: {
- LoadingIcon,
- },
props: {
message: {
type: Object,
@@ -59,7 +55,7 @@ export default {
@click.stop.prevent="clickAction"
>
{{ message.actionText }}
- <loading-icon
+ <gl-loading-icon
v-show="isLoading"
inline
/>
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
index 0ba33053717..760ed8654ee 100644
--- a/app/assets/javascripts/ide/components/file_finder/index.vue
+++ b/app/assets/javascripts/ide/components/file_finder/index.vue
@@ -174,8 +174,8 @@ export default {
<div class="dropdown-input">
<input
ref="searchInput"
- :placeholder="__('Search files')"
v-model="searchText"
+ :placeholder="__('Search files')"
type="search"
class="dropdown-input-field"
autocomplete="off"
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
index f5252ce7706..72ce37be63a 100644
--- a/app/assets/javascripts/ide/components/file_finder/item.vue
+++ b/app/assets/javascripts/ide/components/file_finder/item.vue
@@ -1,7 +1,7 @@
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import FileIcon from '../../../vue_shared/components/file_icon.vue';
-import ChangedFileIcon from '../changed_file_icon.vue';
+import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
const MAX_PATH_LENGTH = 60;
@@ -78,10 +78,10 @@ export default {
class="diff-changed-file-name"
>
<span
- v-for="(char, index) in file.name.split('')"
- :key="index + char"
+ v-for="(char, charIndex) in file.name.split('')"
+ :key="charIndex + char"
:class="{
- highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
+ highlighted: nameSearchTextOccurences.indexOf(charIndex) >= 0,
}"
v-text="char"
>
@@ -91,10 +91,10 @@ export default {
class="diff-changed-file-path prepend-top-5"
>
<span
- v-for="(char, index) in pathWithEllipsis.split('')"
- :key="index + char"
+ v-for="(char, charIndex) in pathWithEllipsis.split('')"
+ :key="charIndex + char"
:class="{
- highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
+ highlighted: pathSearchTextOccurences.indexOf(charIndex) >= 0,
}"
v-text="char"
>
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
new file mode 100644
index 00000000000..2ad14b88410
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -0,0 +1,104 @@
+<script>
+import { mapGetters } from 'vuex';
+import { n__, __, sprintf } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
+import NewDropdown from './new_dropdown/index.vue';
+import MrFileIcon from './mr_file_icon.vue';
+
+export default {
+ name: 'FileRowExtra',
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ NewDropdown,
+ ChangedFileIcon,
+ MrFileIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ mouseOver: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters([
+ 'getChangesInFolder',
+ 'getUnstagedFilesCountForPath',
+ 'getStagedFilesCountForPath',
+ ]),
+ folderUnstagedCount() {
+ return this.getUnstagedFilesCountForPath(this.file.path);
+ },
+ folderStagedCount() {
+ return this.getStagedFilesCountForPath(this.file.path);
+ },
+ changesCount() {
+ return this.getChangesInFolder(this.file.path);
+ },
+ folderChangesTooltip() {
+ if (this.changesCount === 0) return undefined;
+
+ if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
+ return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
+ } else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
+ return n__('%d staged change', '%d staged changes', this.folderStagedCount);
+ }
+
+ return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
+ unstaged: this.folderUnstagedCount,
+ staged: this.folderStagedCount,
+ });
+ },
+ showTreeChangesCount() {
+ return this.file.type === 'tree' && this.changesCount > 0 && !this.file.opened;
+ },
+ showChangedFileIcon() {
+ return this.file.changed || this.file.tempFile || this.file.staged;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="float-right ide-file-icon-holder">
+ <mr-file-icon
+ v-if="file.mrChange"
+ />
+ <span
+ v-if="showTreeChangesCount"
+ class="ide-tree-changes"
+ >
+ {{ changesCount }}
+ <icon
+ v-tooltip
+ :title="folderChangesTooltip"
+ :size="12"
+ data-container="body"
+ data-placement="right"
+ name="file-modified"
+ css-classes="prepend-left-5 ide-file-modified"
+ />
+ </span>
+ <changed-file-icon
+ v-else-if="showChangedFileIcon"
+ :file="file"
+ :show-tooltip="true"
+ :show-staged-icon="true"
+ :force-modified-icon="true"
+ />
+ <new-dropdown
+ :type="file.type"
+ :path="file.path"
+ :mouse-over="mouseOver"
+ class="prepend-left-8"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue
new file mode 100644
index 00000000000..23be5f45f16
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_templates/bar.vue
@@ -0,0 +1,80 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Dropdown from './dropdown.vue';
+
+export default {
+ components: {
+ Dropdown,
+ },
+ computed: {
+ ...mapGetters(['activeFile']),
+ ...mapGetters('fileTemplates', ['templateTypes']),
+ ...mapState('fileTemplates', ['selectedTemplateType', 'updateSuccess']),
+ showTemplatesDropdown() {
+ return Object.keys(this.selectedTemplateType).length > 0;
+ },
+ },
+ watch: {
+ activeFile: 'setInitialType',
+ },
+ mounted() {
+ this.setInitialType();
+ },
+ methods: {
+ ...mapActions('fileTemplates', [
+ 'setSelectedTemplateType',
+ 'fetchTemplate',
+ 'undoFileTemplate',
+ ]),
+ setInitialType() {
+ const initialTemplateType = this.templateTypes.find(t => t.name === this.activeFile.name);
+
+ if (initialTemplateType) {
+ this.setSelectedTemplateType(initialTemplateType);
+ }
+ },
+ selectTemplateType(templateType) {
+ this.setSelectedTemplateType(templateType);
+ },
+ selectTemplate(template) {
+ this.fetchTemplate(template);
+ },
+ undo() {
+ this.undoFileTemplate();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex align-items-center ide-file-templates">
+ <strong class="append-right-default">
+ {{ __('File templates') }}
+ </strong>
+ <dropdown
+ :data="templateTypes"
+ :label="selectedTemplateType.name || __('Choose a type...')"
+ class="mr-2"
+ @click="selectTemplateType"
+ />
+ <dropdown
+ v-if="showTemplatesDropdown"
+ :label="__('Choose a template...')"
+ :is-async-data="true"
+ :searchable="true"
+ :title="__('File templates')"
+ class="mr-2"
+ @click="selectTemplate"
+ />
+ <transition name="fade">
+ <button
+ v-show="updateSuccess"
+ type="button"
+ class="btn btn-default"
+ @click="undo"
+ >
+ {{ __('Undo') }}
+ </button>
+ </transition>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
new file mode 100644
index 00000000000..ef1f6de3a86
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -0,0 +1,123 @@
+<script>
+import $ from 'jquery';
+import { mapActions, mapState } from 'vuex';
+import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+
+export default {
+ components: {
+ DropdownButton,
+ },
+ props: {
+ data: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ isAsyncData: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ searchable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ };
+ },
+ computed: {
+ ...mapState('fileTemplates', ['templates', 'isLoading']),
+ outputData() {
+ return (this.isAsyncData ? this.templates : this.data).filter(t => {
+ if (!this.searchable) return true;
+
+ return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
+ });
+ },
+ showLoading() {
+ return this.isAsyncData ? this.isLoading : false;
+ },
+ },
+ mounted() {
+ $(this.$el).on('show.bs.dropdown', this.fetchTemplatesIfAsync);
+ },
+ beforeDestroy() {
+ $(this.$el).off('show.bs.dropdown', this.fetchTemplatesIfAsync);
+ },
+ methods: {
+ ...mapActions('fileTemplates', ['fetchTemplateTypes']),
+ fetchTemplatesIfAsync() {
+ if (this.isAsyncData) {
+ this.fetchTemplateTypes();
+ }
+ },
+ clickItem(item) {
+ this.$emit('click', item);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown">
+ <dropdown-button
+ :toggle-text="label"
+ data-display="static"
+ />
+ <div class="dropdown-menu pb-0">
+ <div
+ v-if="title"
+ class="dropdown-title ml-0 mr-0"
+ >
+ {{ title }}
+ </div>
+ <div
+ v-if="!showLoading && searchable"
+ class="dropdown-input"
+ >
+ <input
+ v-model="search"
+ :placeholder="__('Filter...')"
+ type="search"
+ class="dropdown-input-field"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ ></i>
+ </div>
+ <div class="dropdown-content">
+ <gl-loading-icon
+ v-if="showLoading"
+ :size="2"
+ />
+ <ul v-else>
+ <li
+ v-for="(item, index) in outputData"
+ :key="index"
+ >
+ <button
+ type="button"
+ @click="clickItem(item)"
+ >
+ {{ item.name }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 6a5ab35a16a..ad6151e3bf6 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,4 +1,5 @@
<script>
+import Vue from 'vue';
import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
@@ -10,6 +11,7 @@ import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
import RightPane from './panes/right.vue';
import ErrorMessage from './error_message.vue';
+import CommitEditorHeader from './commit_sidebar/editor_header.vue';
const originalStopCallback = Mousetrap.stopCallback;
@@ -21,8 +23,15 @@ export default {
IdeStatusBar,
RepoEditor,
FindFile,
- RightPane,
ErrorMessage,
+ CommitEditorHeader,
+ },
+ props: {
+ rightPaneComponent: {
+ type: Vue.Component,
+ required: false,
+ default: () => RightPane,
+ },
},
computed: {
...mapState([
@@ -34,7 +43,7 @@ export default {
'currentProjectId',
'errorMessage',
]),
- ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']),
+ ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
@@ -96,7 +105,12 @@ export default {
<template
v-if="activeFile"
>
+ <commit-editor-header
+ v-if="isCommitModeActive"
+ :active-file="activeFile"
+ />
<repo-tabs
+ v-else
:active-file="activeFile"
:files="openFiles"
:viewer="viewer"
@@ -136,7 +150,8 @@ export default {
</div>
</template>
</div>
- <right-pane
+ <component
+ :is="rightPaneComponent"
v-if="currentProjectId"
/>
</div>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 4771c58a11d..f99ff6d6da8 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapGetters } from 'vuex';
-import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
@@ -13,7 +13,7 @@ import { activityBarViews } from '../constants';
export default {
components: {
- SkeletonLoadingContainer,
+ SkeletonLoading,
ResizablePanel,
ActivityBar,
CommitSection,
@@ -56,7 +56,7 @@ export default {
:key="n"
class="multi-file-loading-container"
>
- <skeleton-loading-container />
+ <skeleton-loading />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 715dc1bfb42..a04d09ef374 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -50,7 +50,9 @@ export default {
this.stopPipelinePolling();
},
methods: {
- ...mapActions(['setRightPane']),
+ ...mapActions('rightPane', {
+ openRightPane: 'open',
+ }),
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
startTimer() {
this.intervalId = setInterval(() => {
@@ -88,7 +90,7 @@ export default {
<button
type="button"
class="p-0 border-0 h-50"
- @click="setRightPane($options.rightSidebarViews.pipelines)"
+ @click="openRightPane($options.rightSidebarViews.pipelines)"
>
<ci-icon
v-tooltip
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 00ae5ea2c15..cfe25084b42 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -1,16 +1,17 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
-import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import RepoFile from './repo_file.vue';
+import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import FileRow from '~/vue_shared/components/file_row.vue';
import NavDropdown from './nav_dropdown.vue';
+import FileRowExtra from './file_row_extra.vue';
export default {
components: {
Icon,
- RepoFile,
- SkeletonLoadingContainer,
+ SkeletonLoading,
NavDropdown,
+ FileRow,
},
props: {
viewerType: {
@@ -34,8 +35,9 @@ export default {
this.updateViewer(this.viewerType);
},
methods: {
- ...mapActions(['updateViewer']),
+ ...mapActions(['updateViewer', 'toggleTreeOpen']),
},
+ FileRowExtra,
};
</script>
@@ -49,7 +51,7 @@ export default {
:key="n"
class="multi-file-loading-container"
>
- <skeleton-loading-container />
+ <skeleton-loading />
</div>
</template>
<template v-else>
@@ -63,11 +65,13 @@ export default {
<div
class="ide-tree-body h-100"
>
- <repo-file
+ <file-row
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
+ :extra-component="$options.FileRowExtra"
+ @toggleTreeOpen="toggleTreeOpen"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
index 3b16b860ecd..acd37605d16 100644
--- a/app/assets/javascripts/ide/components/jobs/list.vue
+++ b/app/assets/javascripts/ide/components/jobs/list.vue
@@ -1,11 +1,9 @@
<script>
import { mapActions } from 'vuex';
-import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Stage from './stage.vue';
export default {
components: {
- LoadingIcon,
Stage,
},
props: {
@@ -26,10 +24,10 @@ export default {
<template>
<div>
- <loading-icon
+ <gl-loading-icon
v-if="loading && !stages.length"
+ :size="2"
class="prepend-top-default"
- size="2"
/>
<template v-else>
<stage
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 15e881b7bc8..ec168d36b9e 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -2,7 +2,6 @@
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
-import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Item from './item.vue';
export default {
@@ -12,7 +11,6 @@ export default {
components: {
Icon,
CiIcon,
- LoadingIcon,
Item,
},
props: {
@@ -71,8 +69,8 @@ export default {
:size="24"
/>
<strong
- v-tooltip="showTooltip"
ref="stageTitle"
+ v-tooltip="showTooltip"
:title="showTooltip ? stage.name : null"
data-container="body"
class="prepend-left-8 ide-stage-title"
@@ -96,7 +94,7 @@ export default {
v-show="!stage.isCollapsed"
class="card-body"
>
- <loading-icon
+ <gl-loading-icon
v-if="showLoadingIcon"
/>
<template v-else>
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index fc612956688..c8343e77860 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -3,7 +3,6 @@ import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
@@ -14,7 +13,6 @@ const SEARCH_TYPES = [
export default {
components: {
- LoadingIcon,
TokenedInput,
Item,
Icon,
@@ -98,10 +96,10 @@ export default {
</div>
</div>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
+ :size="2"
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
- size="2"
/>
<template v-else>
<ul
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index e500ef0e1b5..bcd53ac1ba2 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,6 +1,7 @@
<script>
+import $ from 'jquery';
import { __ } from '~/locale';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants';
@@ -15,6 +16,7 @@ export default {
},
computed: {
...mapState(['entryModal']),
+ ...mapGetters('fileTemplates', ['templateTypes']),
entryName: {
get() {
if (this.entryModal.type === modalTypes.rename) {
@@ -31,7 +33,9 @@ export default {
if (this.entryModal.type === modalTypes.tree) {
return __('Create new directory');
} else if (this.entryModal.type === modalTypes.rename) {
- return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
+ return this.entryModal.entry.type === modalTypes.tree
+ ? __('Rename folder')
+ : __('Rename file');
}
return __('Create new file');
@@ -40,11 +44,16 @@ export default {
if (this.entryModal.type === modalTypes.tree) {
return __('Create directory');
} else if (this.entryModal.type === modalTypes.rename) {
- return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
+ return this.entryModal.entry.type === modalTypes.tree
+ ? __('Rename folder')
+ : __('Rename file');
}
return __('Create file');
},
+ isCreatingNew() {
+ return this.entryModal.type !== modalTypes.rename;
+ },
},
methods: {
...mapActions(['createTempEntry', 'renameEntry']),
@@ -61,6 +70,14 @@ export default {
});
}
},
+ createFromTemplate(template) {
+ this.createTempEntry({
+ name: template.name,
+ type: this.entryModal.type,
+ });
+
+ $('#ide-new-entry').modal('toggle');
+ },
focusInput() {
this.$refs.fieldName.focus();
},
@@ -77,6 +94,7 @@ export default {
:header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success"
+ modal-size="lg"
@submit="submitForm"
@open="focusInput"
@closed="closedModal"
@@ -84,16 +102,35 @@ export default {
<div
class="form-group row"
>
- <label class="label-bold col-form-label col-sm-3">
+ <label class="label-bold col-form-label col-sm-2">
{{ __('Name') }}
</label>
- <div class="col-sm-9">
+ <div class="col-sm-10">
<input
ref="fieldName"
v-model="entryName"
type="text"
class="form-control"
+ placeholder="/dir/file_name"
/>
+ <ul
+ v-if="isCreatingNew"
+ class="prepend-top-default list-inline"
+ >
+ <li
+ v-for="(template, index) in templateTypes"
+ :key="index"
+ class="list-inline-item"
+ >
+ <button
+ type="button"
+ class="btn btn-missing p-1 pr-2 pl-2"
+ @click="createFromTemplate(template)"
+ >
+ {{ template.name }}
+ </button>
+ </li>
+ </ul>
</div>
</div>
</gl-modal>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 79df225c432..bd07f372177 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -1,5 +1,7 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
+import _ from 'underscore';
+import { __ } from '~/locale';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants';
@@ -21,28 +23,77 @@ export default {
MergeRequestInfo,
Clientside,
},
+ props: {
+ extensionTabs: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
computed: {
- ...mapState(['rightPane', 'currentMergeRequestId', 'clientsidePreviewEnabled']),
+ ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']),
+ ...mapState('rightPane', ['isOpen', 'currentView']),
...mapGetters(['packageJson']),
- pipelinesActive() {
- return (
- this.rightPane === rightSidebarViews.pipelines ||
- this.rightPane === rightSidebarViews.jobsDetail
- );
- },
+ ...mapGetters('rightPane', ['isActiveView', 'isAliveView']),
showLivePreview() {
return this.packageJson && this.clientsidePreviewEnabled;
},
+ defaultTabs() {
+ return [
+ {
+ show: this.currentMergeRequestId,
+ title: __('Merge Request'),
+ views: [
+ rightSidebarViews.mergeRequestInfo,
+ ],
+ icon: 'text-description',
+ },
+ {
+ show: true,
+ title: __('Pipelines'),
+ views: [
+ rightSidebarViews.pipelines,
+ rightSidebarViews.jobsDetail,
+ ],
+ icon: 'rocket',
+ },
+ {
+ show: this.showLivePreview,
+ title: __('Live preview'),
+ views: [
+ rightSidebarViews.clientSidePreview,
+ ],
+ icon: 'live-preview',
+ },
+ ];
+ },
+ tabs() {
+ return this.defaultTabs
+ .concat(this.extensionTabs)
+ .filter(tab => tab.show);
+ },
+ tabViews() {
+ return _.flatten(this.tabs.map(tab => tab.views));
+ },
+ aliveTabViews() {
+ return this.tabViews.filter(view => this.isAliveView(view.name));
+ },
},
methods: {
- ...mapActions(['setRightPane']),
- clickTab(e, view) {
+ ...mapActions('rightPane', ['toggleOpen', 'open']),
+ clickTab(e, tab) {
e.target.blur();
- this.setRightPane(view);
+ if (this.isActiveTab(tab)) {
+ this.toggleOpen();
+ } else {
+ this.open(tab.views[0]);
+ }
+ },
+ isActiveTab(tab) {
+ return tab.views.some(view => this.isActiveView(view.name));
},
},
- rightSidebarViews,
};
</script>
@@ -51,77 +102,45 @@ export default {
class="multi-file-commit-panel ide-right-sidebar"
>
<resizable-panel
- v-if="rightPane"
+ v-show="isOpen"
:collapsible="false"
:initial-width="350"
:min-size="350"
- :class="`ide-right-sidebar-${rightPane}`"
+ :class="`ide-right-sidebar-${currentView}`"
side="right"
class="multi-file-commit-panel-inner"
>
- <component :is="rightPane" />
+ <div
+ v-for="tabView in aliveTabViews"
+ v-show="isActiveView(tabView.name)"
+ :key="tabView.name"
+ class="h-100"
+ >
+ <component :is="tabView.name" />
+ </div>
</resizable-panel>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li
- v-if="currentMergeRequestId"
+ v-for="tab of tabs"
+ :key="tab.title"
>
<button
v-tooltip
- :title="__('Merge Request')"
- :aria-label="__('Merge Request')"
- :class="{
- active: rightPane === $options.rightSidebarViews.mergeRequestInfo
- }"
- data-container="body"
- data-placement="left"
- class="ide-sidebar-link is-right"
- type="button"
- @click="clickTab($event, $options.rightSidebarViews.mergeRequestInfo)"
- >
- <icon
- :size="16"
- name="text-description"
- />
- </button>
- </li>
- <li>
- <button
- v-tooltip
- :title="__('Pipelines')"
- :aria-label="__('Pipelines')"
- :class="{
- active: pipelinesActive
- }"
- data-container="body"
- data-placement="left"
- class="ide-sidebar-link is-right"
- type="button"
- @click="clickTab($event, $options.rightSidebarViews.pipelines)"
- >
- <icon
- :size="16"
- name="rocket"
- />
- </button>
- </li>
- <li v-if="showLivePreview">
- <button
- v-tooltip
- :title="__('Live preview')"
- :aria-label="__('Live preview')"
+ :title="tab.title"
+ :aria-label="tab.title"
:class="{
- active: rightPane === $options.rightSidebarViews.clientSidePreview
+ active: isActiveTab(tab) && isOpen
}"
data-container="body"
data-placement="left"
class="ide-sidebar-link is-right"
type="button"
- @click="clickTab($event, $options.rightSidebarViews.clientSidePreview)"
+ @click="clickTab($event, tab)"
>
<icon
:size="16"
- name="live-preview"
+ :name="tab.icon"
/>
</button>
</li>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 5757dfdc925..0a2681b7a1e 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -2,7 +2,6 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { sprintf, __ } from '../../../locale';
-import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import Tabs from '../../../vue_shared/components/tabs/tabs';
@@ -12,7 +11,6 @@ import JobsList from '../jobs/list.vue';
export default {
components: {
- LoadingIcon,
Icon,
CiIcon,
Tabs,
@@ -50,10 +48,10 @@ export default {
<template>
<div class="ide-pipeline">
- <loading-icon
+ <gl-loading-icon
v-if="showLoadingIcon"
+ :size="2"
class="prepend-top-default"
- size="2"
/>
<template v-else-if="latestPipeline !== null">
<header
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index 39a1bd1f61b..37a8ad36507 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -3,14 +3,12 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Navigator from './navigator.vue';
import { packageJsonPath } from '../../constants';
import { createPathWithExt } from '../../utils';
export default {
components: {
- LoadingIcon,
Navigator,
},
data() {
@@ -177,9 +175,9 @@ export default {
{{ s__('IDE|Get started with Live Preview') }}
</a>
</div>
- <loading-icon
+ <gl-loading-icon
v-else
- size="2"
+ :size="2"
class="align-self-center mt-auto mb-auto"
/>
</div>
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
index 4bf346946b6..42f23801692 100644
--- a/app/assets/javascripts/ide/components/preview/navigator.vue
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -1,12 +1,10 @@
<script>
import { listen } from 'codesandbox-api';
import Icon from '~/vue_shared/components/icon.vue';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
export default {
components: {
Icon,
- LoadingIcon,
},
props: {
manager: {
@@ -138,7 +136,7 @@ export default {
class="ide-navigator-location form-control bg-white"
readonly
/>
- <loading-icon
+ <gl-loading-icon
v-if="loading"
class="position-absolute ide-preview-loading-icon"
/>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 6f1a941fbc4..d3b24c5b793 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -95,8 +95,9 @@ export default {
:file-list="changedFiles"
:action-btn-text="__('Stage all changes')"
:active-file-key="activeFileKey"
+ :empty-state-text="__('There are no unstaged changes')"
action="stageAllChanges"
- action-btn-icon="mobile-issue-close"
+ action-btn-icon="stage-all"
item-action-component="stage-button"
class="is-first"
icon-name="unstaged"
@@ -108,8 +109,9 @@ export default {
:action-btn-text="__('Unstage all changes')"
:staged-list="true"
:active-file-key="activeFileKey"
+ :empty-state-text="__('There are no staged changes')"
action="unstageAllChanges"
- action-btn-icon="history"
+ action-btn-icon="unstage-all"
item-action-component="unstage-button"
icon-name="staged"
/>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f55aa843444..b2599128213 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
import Editor from '../lib/editor';
import ExternalLink from './external_link.vue';
+import FileTemplatesBar from './file_templates/bar.vue';
export default {
components: {
ContentViewer,
DiffViewer,
ExternalLink,
+ FileTemplatesBar,
},
props: {
file: {
@@ -20,12 +22,14 @@ export default {
},
},
computed: {
+ ...mapState('rightPane', {
+ rightPaneIsOpen: 'isOpen',
+ }),
...mapState([
'rightPanelCollapsed',
'viewer',
'panelResizing',
'currentActivityView',
- 'rightPane',
]),
...mapGetters([
'currentMergeRequest',
@@ -34,6 +38,7 @@ export default {
'isCommitModeActive',
'isReviewModeActive',
]),
+ ...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
@@ -96,7 +101,7 @@ export default {
this.editor.updateDimensions();
}
},
- rightPane() {
+ rightPaneIsOpen() {
this.editor.updateDimensions();
},
},
@@ -216,7 +221,7 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
- <div class="ide-mode-tabs clearfix" >
+ <div class="ide-mode-tabs clearfix">
<ul
v-if="!shouldHideEditor && isEditModeActive"
class="nav-links float-left"
@@ -249,6 +254,9 @@ export default {
:file="file"
/>
</div>
+ <file-templates-bar
+ v-if="showFileTemplatesBar(file.name)"
+ />
<div
v-show="!shouldHideEditor && file.viewMode ==='editor'"
ref="editor"
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
deleted file mode 100644
index 110eda83bb4..00000000000
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ /dev/null
@@ -1,227 +0,0 @@
-<script>
-import { mapActions, mapGetters } from 'vuex';
-import { n__, __, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
-import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import Icon from '~/vue_shared/components/icon.vue';
-import FileIcon from '~/vue_shared/components/file_icon.vue';
-import router from '../ide_router';
-import NewDropdown from './new_dropdown/index.vue';
-import FileStatusIcon from './repo_file_status_icon.vue';
-import ChangedFileIcon from './changed_file_icon.vue';
-import MrFileIcon from './mr_file_icon.vue';
-
-export default {
- name: 'RepoFile',
- directives: {
- tooltip,
- },
- components: {
- SkeletonLoadingContainer,
- NewDropdown,
- FileStatusIcon,
- FileIcon,
- ChangedFileIcon,
- MrFileIcon,
- Icon,
- },
- props: {
- file: {
- type: Object,
- required: true,
- },
- level: {
- type: Number,
- required: true,
- },
- },
- data() {
- return {
- mouseOver: false,
- };
- },
- computed: {
- ...mapGetters([
- 'getChangesInFolder',
- 'getUnstagedFilesCountForPath',
- 'getStagedFilesCountForPath',
- ]),
- folderUnstagedCount() {
- return this.getUnstagedFilesCountForPath(this.file.path);
- },
- folderStagedCount() {
- return this.getStagedFilesCountForPath(this.file.path);
- },
- changesCount() {
- return this.getChangesInFolder(this.file.path);
- },
- folderChangesTooltip() {
- if (this.changesCount === 0) return undefined;
-
- if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
- return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
- } else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
- return n__('%d staged change', '%d staged changes', this.folderStagedCount);
- }
-
- return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
- unstaged: this.folderUnstagedCount,
- staged: this.folderStagedCount,
- });
- },
- isTree() {
- return this.file.type === 'tree';
- },
- isBlob() {
- return this.file.type === 'blob';
- },
- levelIndentation() {
- return {
- marginLeft: `${this.level * 16}px`,
- };
- },
- fileClass() {
- return {
- 'file-open': this.isBlob && this.file.opened,
- 'file-active': this.isBlob && this.file.active,
- folder: this.isTree,
- 'is-open': this.file.opened,
- };
- },
- showTreeChangesCount() {
- return this.isTree && this.changesCount > 0 && !this.file.opened;
- },
- showChangedFileIcon() {
- return this.file.changed || this.file.tempFile || this.file.staged;
- },
- },
- watch: {
- 'file.active': function fileActiveWatch(active) {
- if (this.file.type === 'blob' && active) {
- this.scrollIntoView();
- }
- },
- },
- mounted() {
- if (this.hasPathAtCurrentRoute()) {
- this.scrollIntoView(true);
- }
- },
- methods: {
- ...mapActions(['toggleTreeOpen']),
- clickFile() {
- // Manual Action if a tree is selected/opened
- if (this.isTree && this.hasUrlAtCurrentRoute()) {
- this.toggleTreeOpen(this.file.path);
- }
-
- router.push(`/project${this.file.url}`);
- },
- scrollIntoView(isInit = false) {
- const block = isInit && this.isTree ? 'center' : 'nearest';
-
- this.$el.scrollIntoView({
- behavior: 'smooth',
- block,
- });
- },
- hasPathAtCurrentRoute() {
- if (!this.$router || !this.$router.currentRoute) {
- return false;
- }
-
- // - strip route up to "/-/" and ending "/"
- const routePath = this.$router.currentRoute.path
- .replace(/^.*?[/]-[/]/g, '')
- .replace(/[/]$/g, '');
-
- // - strip ending "/"
- const filePath = this.file.path.replace(/[/]$/g, '');
-
- return filePath === routePath;
- },
- hasUrlAtCurrentRoute() {
- return this.$router.currentRoute.path === `/project${this.file.url}`;
- },
- toggleHover(over) {
- this.mouseOver = over;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div
- :class="fileClass"
- class="file"
- role="button"
- @click="clickFile"
- @mouseover="toggleHover(true)"
- @mouseout="toggleHover(false)"
- >
- <div
- class="file-name"
- >
- <span
- :style="levelIndentation"
- class="ide-file-name str-truncated"
- >
- <file-icon
- :file-name="file.name"
- :loading="file.loading"
- :folder="isTree"
- :opened="file.opened"
- :size="16"
- />
- {{ file.name }}
- <file-status-icon
- :file="file"
- />
- </span>
- <span class="float-right ide-file-icon-holder">
- <mr-file-icon
- v-if="file.mrChange"
- />
- <span
- v-if="showTreeChangesCount"
- class="ide-tree-changes"
- >
- {{ changesCount }}
- <icon
- v-tooltip
- :title="folderChangesTooltip"
- :size="12"
- data-container="body"
- data-placement="right"
- name="file-modified"
- css-classes="prepend-left-5 ide-file-modified"
- />
- </span>
- <changed-file-icon
- v-else-if="showChangedFileIcon"
- :file="file"
- :show-tooltip="true"
- :show-staged-icon="true"
- :force-modified-icon="true"
- class="float-right"
- />
- </span>
- <new-dropdown
- :type="file.type"
- :path="file.path"
- :mouse-over="mouseOver"
- class="float-right prepend-left-8"
- />
- </div>
- </div>
- <template v-if="file.opened">
- <repo-file
- v-for="childFile in file.tree"
- :key="childFile.key"
- :file="childFile"
- :level="level + 1"
- />
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 76a3333be50..97589e116c5 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -26,8 +26,8 @@ export default {
<template>
<span
- v-tooltip
v-if="file.file_lock"
+ v-tooltip
:title="lockTooltip"
data-container="body"
>
diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
deleted file mode 100644
index 7a5ede82253..00000000000
--- a/app/assets/javascripts/ide/components/repo_loading_file.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-
- export default {
- components: {
- skeletonLoadingContainer,
- },
- computed: {
- ...mapState([
- 'leftPanelCollapsed',
- ]),
- },
- };
-</script>
-
-<template>
- <tr
- class="loading-file"
- aria-label="Loading files"
- >
- <td class="multi-file-table-col-name">
- <skeleton-loading-container
- :small="true"
- />
- </td>
- <template v-if="!leftPanelCollapsed">
- <td class="d-none d-sm-none d-md-block">
- <skeleton-loading-container
- :small="true"
- />
- </td>
-
- <td class="d-none d-sm-block">
- <skeleton-loading-container
- :small="true"
- class="animation-container-right"
- />
- </td>
- </template>
- </tr>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index db47b75ec5c..d621653d6fd 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -3,8 +3,8 @@ import { mapActions } from 'vuex';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
+import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import FileStatusIcon from './repo_file_status_icon.vue';
-import ChangedFileIcon from './changed_file_icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 8caa5b86a9b..3b201f006aa 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -29,10 +29,10 @@ export const diffModes = {
};
export const rightSidebarViews = {
- pipelines: 'pipelines-list',
- jobsDetail: 'jobs-detail',
- mergeRequestInfo: 'merge-request-info',
- clientSidePreview: 'clientside',
+ pipelines: { name: 'pipelines-list', keepAlive: true },
+ jobsDetail: { name: 'jobs-detail', keepAlive: false },
+ mergeRequestInfo: { name: 'merge-request-info', keepAlive: true },
+ clientSidePreview: { name: 'clientside', keepAlive: false },
};
export const stageKeys = {
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 79e38ae911e..c0550116633 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -8,16 +8,28 @@ import { convertPermissionToBoolean } from '../lib/utils/common_utils';
Vue.use(Translate);
-export function initIde(el) {
+/**
+ * Initialize the IDE on the given element.
+ *
+ * @param {Element} el - The element that will contain the IDE.
+ * @param {Object} options - Extra options for the IDE (Used by EE).
+ * @param {(e:Element) => Object} options.extraInitialData -
+ * Function that returns extra properties to seed initial data.
+ * @param {Component} options.rootComponent -
+ * Component that overrides the root component.
+ */
+export function initIde(el, options = {}) {
if (!el) return null;
+ const {
+ extraInitialData = () => ({}),
+ rootComponent = ide,
+ } = options;
+
return new Vue({
el,
store,
router,
- components: {
- ide,
- },
created() {
this.setEmptyStateSvgs({
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
@@ -32,13 +44,14 @@ export function initIde(el) {
});
this.setInitialData({
clientsidePreviewEnabled: convertPermissionToBoolean(el.dataset.clientsidePreviewEnabled),
+ ...extraInitialData(el),
});
},
methods: {
...mapActions(['setEmptyStateSvgs', 'setLinks', 'setInitialData']),
},
render(createElement) {
- return createElement('ide');
+ return createElement(rootComponent);
},
});
}
@@ -52,3 +65,18 @@ export function resetServiceWorkersPublicPath() {
const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
__webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
}
+
+/**
+ * Start the IDE.
+ *
+ * @param {Objects} options - Extra options for the IDE (Used by EE).
+ */
+export function startIde(options) {
+ document.addEventListener('DOMContentLoaded', () => {
+ const ideElement = document.getElementById('ide');
+ if (ideElement) {
+ resetServiceWorkersPublicPath();
+ initIde(ideElement, options);
+ }
+ });
+}
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index aa02dfbddc4..e10a132ab4b 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -4,6 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import * as types from './mutation_types';
import FilesDecoratorWorker from './workers/files_decorator_worker';
+import { stageKeys } from '../constants';
export const redirectToUrl = (_, url) => visitUrl(url);
@@ -122,14 +123,28 @@ export const scrollToTab = () => {
});
};
-export const stageAllChanges = ({ state, commit }) => {
+export const stageAllChanges = ({ state, commit, dispatch }) => {
+ const openFile = state.openFiles[0];
+
commit(types.SET_LAST_COMMIT_MSG, '');
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
+
+ dispatch('openPendingTab', {
+ file: state.stagedFiles.find(f => f.path === openFile.path),
+ keyPrefix: stageKeys.staged,
+ });
};
-export const unstageAllChanges = ({ state, commit }) => {
+export const unstageAllChanges = ({ state, commit, dispatch }) => {
+ const openFile = state.openFiles[0];
+
state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
+
+ dispatch('openPendingTab', {
+ file: state.changedFiles.find(f => f.path === openFile.path),
+ keyPrefix: stageKeys.unstaged,
+ });
};
export const updateViewer = ({ commit }, viewer) => {
@@ -169,10 +184,6 @@ export const burstUnusedSeal = ({ state, commit }) => {
}
};
-export const setRightPane = ({ commit }, view) => {
- commit(types.SET_RIGHT_PANE, view);
-};
-
export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
export const setErrorMessage = ({ commit }, errorMessage) =>
@@ -206,6 +217,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => {
const entry = state.entries[entryPath || path];
+
commit(types.RENAME_ENTRY, { path, name, entryPath });
if (entry.type === 'tree') {
@@ -214,7 +226,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath
);
}
- if (!entryPath) {
+ if (!entryPath && !entry.tempFile) {
dispatch('deleteEntry', path);
}
};
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 28b9d0df201..30dcf7ef4df 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -5,7 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import { setPageTitle } from '../utils';
-import { viewerTypes } from '../../constants';
+import { viewerTypes, stageKeys } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
const { path } = file;
@@ -208,8 +208,9 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
};
-export const stageChange = ({ commit, state }, path) => {
+export const stageChange = ({ commit, state, dispatch }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path);
+ const openFile = state.openFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path);
commit(types.SET_LAST_COMMIT_MSG, '');
@@ -217,21 +218,39 @@ export const stageChange = ({ commit, state }, path) => {
if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
}
+
+ if (openFile && openFile.active) {
+ const file = state.stagedFiles.find(f => f.path === path);
+
+ dispatch('openPendingTab', {
+ file,
+ keyPrefix: stageKeys.staged,
+ });
+ }
};
-export const unstageChange = ({ commit }, path) => {
+export const unstageChange = ({ commit, dispatch, state }, path) => {
+ const openFile = state.openFiles.find(f => f.path === path);
+
commit(types.UNSTAGE_CHANGE, path);
+
+ if (openFile && openFile.active) {
+ const file = state.changedFiles.find(f => f.path === path);
+
+ dispatch('openPendingTab', {
+ file,
+ keyPrefix: stageKeys.unstaged,
+ });
+ }
};
-export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
+export const openPendingTab = ({ commit, getters, state }, { file, keyPrefix }) => {
if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false;
state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
- dispatch('scrollToTab');
-
router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
return true;
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index a601dc8f5a0..f1f544b52b2 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -8,6 +8,8 @@ import commitModule from './modules/commit';
import pipelines from './modules/pipelines';
import mergeRequests from './modules/merge_requests';
import branches from './modules/branches';
+import fileTemplates from './modules/file_templates';
+import paneModule from './modules/pane';
Vue.use(Vuex);
@@ -22,6 +24,8 @@ export const createStore = () =>
pipelines,
mergeRequests,
branches,
+ fileTemplates: fileTemplates(),
+ rightPane: paneModule(),
},
});
diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutations.js b/app/assets/javascripts/ide/stores/modules/branches/mutations.js
index 081ec2d4c28..0a455f4500f 100644
--- a/app/assets/javascripts/ide/stores/modules/branches/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/branches/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
index 43237a29466..b7090e09daf 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
@@ -1,6 +1,8 @@
import Api from '~/api';
import { __ } from '~/locale';
+import { normalizeHeaders } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
+import eventHub from '../../../eventhub';
export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
export const receiveTemplateTypesError = ({ commit, dispatch }) => {
@@ -21,19 +23,41 @@ export const receiveTemplateTypesError = ({ commit, dispatch }) => {
export const receiveTemplateTypesSuccess = ({ commit }, templates) =>
commit(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, templates);
-export const fetchTemplateTypes = ({ dispatch, state }) => {
+export const fetchTemplateTypes = ({ dispatch, state, rootState }, page = 1) => {
if (!Object.keys(state.selectedTemplateType).length) return Promise.reject();
dispatch('requestTemplateTypes');
- return Api.templates(state.selectedTemplateType.key)
- .then(({ data }) => dispatch('receiveTemplateTypesSuccess', data))
+ return Api.projectTemplates(rootState.currentProjectId, state.selectedTemplateType.key, { page })
+ .then(({ data, headers }) => {
+ const nextPage = parseInt(normalizeHeaders(headers)['X-NEXT-PAGE'], 10);
+
+ dispatch('receiveTemplateTypesSuccess', data);
+
+ if (nextPage) {
+ dispatch('fetchTemplateTypes', nextPage);
+ }
+ })
.catch(() => dispatch('receiveTemplateTypesError'));
};
-export const setSelectedTemplateType = ({ commit }, type) =>
+export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, type) => {
commit(types.SET_SELECTED_TEMPLATE_TYPE, type);
+ if (rootGetters.activeFile.prevPath === type.name) {
+ dispatch('discardFileChanges', rootGetters.activeFile.path, { root: true });
+ } else if (rootGetters.activeFile.name !== type.name) {
+ dispatch(
+ 'renameEntry',
+ {
+ path: rootGetters.activeFile.path,
+ name: type.name,
+ },
+ { root: true },
+ );
+ }
+};
+
export const receiveTemplateError = ({ dispatch }, template) => {
dispatch(
'setErrorMessage',
@@ -50,12 +74,16 @@ export const receiveTemplateError = ({ dispatch }, template) => {
);
};
-export const fetchTemplate = ({ dispatch, state }, template) => {
+export const fetchTemplate = ({ dispatch, state, rootState }, template) => {
if (template.content) {
return dispatch('setFileTemplate', template);
}
- return Api.templates(`${state.selectedTemplateType.key}/${template.key || template.name}`)
+ return Api.projectTemplate(
+ rootState.currentProjectId,
+ state.selectedTemplateType.key,
+ template.key || template.name,
+ )
.then(({ data }) => {
dispatch('setFileTemplate', data);
})
@@ -69,6 +97,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) =>
{ root: true },
);
commit(types.SET_UPDATE_SUCCESS, true);
+ eventHub.$emit(`editor.update.model.new.content.${rootGetters.activeFile.key}`, template.content);
};
export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
@@ -76,6 +105,12 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true });
commit(types.SET_UPDATE_SUCCESS, false);
+
+ eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.raw);
+
+ if (file.prevPath) {
+ dispatch('discardFileChanges', file.path, { root: true });
+ }
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index 38318fd49bf..628babe6a01 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -1,3 +1,5 @@
+import { activityBarViews } from '../../../constants';
+
export const templateTypes = () => [
{
name: '.gitlab-ci.yml',
@@ -17,7 +19,8 @@ export const templateTypes = () => [
},
];
-export const showFileTemplatesBar = (_, getters) => name =>
- getters.templateTypes.find(t => t.name === name);
+export const showFileTemplatesBar = (_, getters, rootState) => name =>
+ getters.templateTypes.find(t => t.name === name) &&
+ rootState.currentActivityView === activityBarViews.edit;
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/index.js b/app/assets/javascripts/ide/stores/modules/file_templates/index.js
index dfa5ef54413..383ff5db392 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/index.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/index.js
@@ -3,10 +3,10 @@ import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
-export default {
+export default () => ({
namespaced: true,
actions,
state: createState(),
getters,
mutations,
-};
+});
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js
index e413e61eaaa..d519c033769 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
@@ -10,7 +9,7 @@ export default {
},
[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, templates) {
state.isLoading = false;
- state.templates = templates;
+ state.templates = state.templates.concat(templates);
},
[types.SET_SELECTED_TEMPLATE_TYPE](state, type) {
state.selectedTemplateType = type;
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
index 98102a68e08..0eba9c39817 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
diff --git a/app/assets/javascripts/ide/stores/modules/pane/actions.js b/app/assets/javascripts/ide/stores/modules/pane/actions.js
new file mode 100644
index 00000000000..7f5d167a14f
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pane/actions.js
@@ -0,0 +1,30 @@
+import * as types from './mutation_types';
+
+export const toggleOpen = ({ dispatch, state }, view) => {
+ if (state.isOpen) {
+ dispatch('close');
+ } else {
+ dispatch('open', view);
+ }
+};
+
+export const open = ({ commit }, view) => {
+ commit(types.SET_OPEN, true);
+
+ if (view) {
+ const { name, keepAlive } = view;
+
+ commit(types.SET_CURRENT_VIEW, name);
+
+ if (keepAlive) {
+ commit(types.KEEP_ALIVE_VIEW, name);
+ }
+ }
+};
+
+export const close = ({ commit }) => {
+ commit(types.SET_OPEN, false);
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pane/getters.js b/app/assets/javascripts/ide/stores/modules/pane/getters.js
new file mode 100644
index 00000000000..c346cf13689
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pane/getters.js
@@ -0,0 +1,4 @@
+export const isActiveView = state => view => state.currentView === view;
+
+export const isAliveView = (state, getters) => view =>
+ state.keepAliveViews[view] || (state.isOpen && getters.isActiveView(view));
diff --git a/app/assets/javascripts/ide/stores/modules/pane/index.js b/app/assets/javascripts/ide/stores/modules/pane/index.js
new file mode 100644
index 00000000000..5f61cb732c8
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pane/index.js
@@ -0,0 +1,12 @@
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+export default () => ({
+ namespaced: true,
+ state: state(),
+ actions,
+ getters,
+ mutations,
+});
diff --git a/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js
new file mode 100644
index 00000000000..abdebc4d913
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js
@@ -0,0 +1,3 @@
+export const SET_OPEN = 'SET_OPEN';
+export const SET_CURRENT_VIEW = 'SET_CURRENT_VIEW';
+export const KEEP_ALIVE_VIEW = 'KEEP_ALIVE_VIEW';
diff --git a/app/assets/javascripts/ide/stores/modules/pane/mutations.js b/app/assets/javascripts/ide/stores/modules/pane/mutations.js
new file mode 100644
index 00000000000..c16484b4402
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pane/mutations.js
@@ -0,0 +1,19 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_OPEN](state, isOpen) {
+ Object.assign(state, {
+ isOpen,
+ });
+ },
+ [types.SET_CURRENT_VIEW](state, currentView) {
+ Object.assign(state, {
+ currentView,
+ });
+ },
+ [types.KEEP_ALIVE_VIEW](state, viewName) {
+ Object.assign(state.keepAliveViews, {
+ [viewName]: true,
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/modules/pane/state.js b/app/assets/javascripts/ide/stores/modules/pane/state.js
new file mode 100644
index 00000000000..353065b5735
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pane/state.js
@@ -0,0 +1,5 @@
+export default () => ({
+ isOpen: false,
+ currentView: null,
+ keepAliveViews: {},
+});
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 3e67b222e66..8fa86995ef0 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -113,7 +113,7 @@ export const toggleStageCollapsed = ({ commit }, stageId) =>
export const setDetailJob = ({ commit, dispatch }, job) => {
commit(types.SET_DETAIL_JOB, job);
- dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, {
+ dispatch('rightPane/open', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, {
root: true,
});
};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
index 5a2213bbe89..b4be100cb07 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
import { normalizeJob } from './utils';
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 5a7991d2fa7..a5f8098dc17 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -68,8 +68,6 @@ export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
-export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
-
export const CLEAR_PROJECTS = 'CLEAR_PROJECTS';
export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 0347f803757..78cdfda74f0 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign */
+import Vue from 'vue';
import * as types from './mutation_types';
import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
@@ -166,11 +166,6 @@ export default {
unusedSeal: false,
});
},
- [types.SET_RIGHT_PANE](state, view) {
- Object.assign(state, {
- rightPane: state.rightPane === view ? null : view,
- });
- },
[types.SET_LINKS](state, links) {
Object.assign(state, { links });
},
@@ -227,7 +222,7 @@ export default {
path: newPath,
name: entryPath ? oldEntry.name : name,
tempFile: true,
- prevPath: oldEntry.path,
+ prevPath: oldEntry.tempFile ? null : oldEntry.path,
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
tree: [],
parentPath,
@@ -246,6 +241,20 @@ export default {
if (newEntry.type === 'blob') {
state.changedFiles = state.changedFiles.concat(newEntry);
}
+
+ if (state.entries[newPath].opened) {
+ state.openFiles.push(state.entries[newPath]);
+ }
+
+ if (oldEntry.tempFile) {
+ const filterMethod = f => f.path !== oldEntry.path;
+
+ state.openFiles = state.openFiles.filter(filterMethod);
+ state.changedFiles = state.changedFiles.filter(filterMethod);
+ parent.tree = parent.tree.filter(filterMethod);
+
+ Vue.delete(state.entries, oldEntry.path);
+ }
},
...projectMutations,
...mergeRequestMutation,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index a937fb157f8..6ca246c1d63 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
import { sortTree } from '../utils';
import { diffModes } from '../../constants';
@@ -56,7 +55,7 @@ export default {
f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath),
);
- if (file.tempFile) {
+ if (file.tempFile && file.content === '') {
Object.assign(state.entries[file.path], {
content: raw,
});
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 46b52fa00fc..d400b9831a9 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -23,7 +23,6 @@ export default () => ({
currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false,
- rightPane: null,
links: {},
errorMessage: null,
entryModal: {
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index 35eaf21a836..9e848699163 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -36,7 +36,7 @@ export default {
},
getSelectedIssues() {
- return this.issues.has('.selected_issue:checked');
+ return this.issues.has('.selected-issuable:checked');
},
getLabelsFromSelection() {
@@ -110,7 +110,7 @@ export default {
getOriginalCommonIds() {
const labelIds = [];
- this.getElement('.selected_issue:checked').each((i, el) => {
+ this.getElement('.selected-issuable:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
@@ -119,7 +119,7 @@ export default {
// From issuable's initial bulk selection
getOriginalMarkedIds() {
const labelIds = [];
- this.getElement('.selected_issue:checked').each((i, el) => {
+ this.getElement('.selected-issuable:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
@@ -132,7 +132,7 @@ export default {
let issuableLabels = [];
// Collect unique label IDs for all checked issues
- this.getElement('.selected_issue:checked').each((i, el) => {
+ this.getElement('.selected-issuable:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
issuableLabels.forEach((labelId) => {
// Store unique IDs
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index 2307c8e0d85..74150ce3a8b 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -30,7 +30,7 @@ export default class IssuableBulkUpdateSidebar {
this.$otherFilters = $('.issues-other-filters');
this.$checkAllContainer = $('.check-all-holder');
this.$issueChecks = $('.issue-check');
- this.$issuesList = $('.selected_issue');
+ this.$issuesList = $('.selected-issuable');
this.$issuableIdsInput = $('#update_issuable_ids');
}
@@ -55,7 +55,7 @@ export default class IssuableBulkUpdateSidebar {
}
updateFormState() {
- const noCheckedIssues = !$('.selected_issue:checked').length;
+ const noCheckedIssues = !$('.selected-issuable:checked').length;
this.toggleSubmitButtonDisabled(noCheckedIssues);
this.updateSelectedIssuableIds();
@@ -123,7 +123,7 @@ export default class IssuableBulkUpdateSidebar {
}
static getCheckedIssueIds() {
- const $checkedIssues = $('.selected_issue:checked');
+ const $checkedIssues = $('.selected-issuable:checked');
if ($checkedIssues.length > 0) {
return $.map($checkedIssues, value => $(value).data('id'));
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index ad928484952..c6ad3aa3e0d 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -293,6 +293,7 @@
:show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
+ :issuable-type="issuableType"
/>
<recaptcha-modal
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index 597c6d69a81..c3d39082714 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -1,7 +1,13 @@
<script>
+ import { __, sprintf } from '~/locale';
import updateMixin from '../mixins/update';
import eventHub from '../event_hub';
+ const issuableTypes = {
+ issue: __('Issue'),
+ epic: __('Epic'),
+ };
+
export default {
mixins: [updateMixin],
props: {
@@ -18,6 +24,10 @@
required: false,
default: true,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -37,8 +47,11 @@
eventHub.$emit('close.form');
},
deleteIssuable() {
+ const confirmMessage = sprintf(__('%{issuableType} will be removed! Are you sure?'), {
+ issuableType: issuableTypes[this.issuableType],
+ });
// eslint-disable-next-line no-alert
- if (window.confirm('Issue will be removed! Are you sure?')) {
+ if (window.confirm(confirmMessage)) {
this.deleteLoading = true;
eventHub.$emit('delete.issuable');
@@ -53,7 +66,7 @@
<button
:class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
:disabled="formState.updateLoading || !isSubmitEnabled"
- class="btn btn-save float-left"
+ class="btn btn-success float-left qa-save-button"
type="submit"
@click.prevent="updateIssuable">
Save changes
@@ -73,7 +86,7 @@
v-if="shouldShowDeleteButton"
:class="{ disabled: deleteLoading }"
:disabled="deleteLoading"
- class="btn btn-danger float-right append-right-default"
+ class="btn btn-danger float-right append-right-default qa-delete-button"
type="button"
@click="deleteIssuable">
Delete
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 97acc5ba385..1a78c59d715 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -61,7 +61,8 @@
ref="textarea"
slot="textarea"
v-model="formState.description"
- class="note-textarea js-gfm-input js-autosize markdown-area"
+ class="note-textarea js-gfm-input js-autosize markdown-area
+ qa-description-textarea"
data-supports-quick-actions="false"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue
index 7d1526a64b4..b7f2b1a6050 100644
--- a/app/assets/javascripts/issue_show/components/fields/title.vue
+++ b/app/assets/javascripts/issue_show/components/fields/title.vue
@@ -22,7 +22,7 @@
<input
id="issuable-title"
v-model="formState.title"
- class="form-control"
+ class="form-control qa-title-input"
type="text"
placeholder="Title"
aria-label="Title"
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index e509bb52f7d..03d8d0ec67c 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -27,6 +27,10 @@
required: false,
default: () => [],
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
markdownPreviewPath: {
type: String,
required: true,
@@ -110,6 +114,7 @@
:form-state="formState"
:can-destroy="canDestroy"
:show-delete-button="showDeleteButton"
+ :issuable-type="issuableType"
/>
</form>
</template>
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index b5e8e0ea44b..ed26e53ac0e 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -76,10 +76,11 @@ export default {
>
</h2>
<button
- v-tooltip
v-if="showInlineEditButton && canUpdate"
+ v-tooltip
type="button"
- class="btn btn-default btn-edit btn-svg js-issuable-edit"
+ class="btn btn-default btn-edit btn-svg js-issuable-edit
+ qa-edit-button"
title="Edit title and description"
data-placement="bottom"
data-container="body"
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index 75dfdedcf1b..d08e8ba0c4b 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
+import sanitize from 'sanitize-html';
import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor';
-document.addEventListener('DOMContentLoaded', () => {
+export default function initIssueableApp() {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
- const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
+ const props = JSON.parse(sanitize(initialDataEl.textContent).replace(/&quot;/g, '"'));
return new Vue({
el: document.getElementById('js-issuable-app'),
@@ -17,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
-});
+}
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index d4f2a3ef7d3..854445bd2a4 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -6,7 +6,7 @@ import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
-import { isScrolledToBottom, scrollDown } from './lib/utils/scroll_utils';
+import { isScrolledToBottom, scrollDown, scrollUp } from './lib/utils/scroll_utils';
import LogOutputBehaviours from './lib/utils/logoutput_behaviours';
export default class Job extends LogOutputBehaviours {
@@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours {
this.$document = $(document);
this.$window = $(window);
this.logBytes = 0;
- this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh');
@@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours {
clearTimeout(this.timeout);
this.initSidebar();
- this.populateJobs(this.buildStage);
- this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize();
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
- this.$document
- .off('click', '.stage-item')
- .on('click', '.stage-item', this.updateDropdown);
-
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window
@@ -80,7 +73,7 @@ export default class Job extends LogOutputBehaviours {
}
scrollToTop() {
- $(document).scrollTop(0);
+ scrollUp();
this.hasBeenScrolled = true;
this.toggleScroll();
}
@@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
}
- // eslint-disable-next-line class-methods-use-this
- populateJobs(stage) {
- $('.build-job').hide();
- $(`.build-job[data-stage="${stage}"]`).show();
- }
- // eslint-disable-next-line class-methods-use-this
- updateStageDropdownText(stage) {
- $('.stage-selection').text(stage);
- }
-
- updateDropdown(e) {
- e.preventDefault();
- const stage = e.currentTarget.text;
- this.updateStageDropdownText(stage);
- this.populateJobs(stage);
- }
}
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 525c5eec91a..d5866f9b9f1 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -1,40 +1,27 @@
<script>
- import TimeagoTooltiop from '~/vue_shared/components/time_ago_tooltip.vue';
+ import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+ import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
- TimeagoTooltiop,
+ TimeagoTooltip,
},
+ mixins: [
+ timeagoMixin,
+ ],
props: {
- // @build.artifacts_expired?
- haveArtifactsExpired: {
- type: Boolean,
+ artifact: {
+ type: Object,
required: true,
},
- // @build.has_expiring_artifacts?
- willArtifactsExpire: {
- type: Boolean,
- required: true,
- },
- expireAt: {
- type: String,
- required: false,
- default: null,
- },
- keepArtifactsPath: {
- type: String,
- required: false,
- default: null,
- },
- downloadArtifactsPath: {
- type: String,
- required: false,
- default: null,
+ },
+ computed: {
+ isExpired() {
+ return this.artifact.expired;
},
- browseArtifactsPath: {
- type: String,
- required: false,
- default: null,
+ // Only when the key is `false` we can render this block
+ willExpire() {
+ return this.artifact.expired === false;
},
},
};
@@ -46,21 +33,22 @@
</div>
<p
- v-if="haveArtifactsExpired"
+ v-if="isExpired"
class="js-artifacts-removed build-detail-row"
>
{{ s__('Job|The artifacts were removed') }}
</p>
+
<p
- v-else-if="willArtifactsExpire"
+ v-else-if="willExpire"
class="js-artifacts-will-be-removed build-detail-row"
>
- {{ s__('Job|The artifacts will be removed') }}
+ {{ s__('Job|The artifacts will be removed in') }}
</p>
- <timeago-tooltiop
- v-if="expireAt"
- :time="expireAt"
+ <timeago-tooltip
+ v-if="artifact.expire_at"
+ :time="artifact.expire_at"
/>
<div
@@ -68,8 +56,8 @@
role="group"
>
<a
- v-if="keepArtifactsPath"
- :href="keepArtifactsPath"
+ v-if="artifact.keep_path"
+ :href="artifact.keep_path"
class="js-keep-artifacts btn btn-sm btn-default"
data-method="post"
>
@@ -77,8 +65,8 @@
</a>
<a
- v-if="downloadArtifactsPath"
- :href="downloadArtifactsPath"
+ v-if="artifact.download_path"
+ :href="artifact.download_path"
class="js-download-artifacts btn btn-sm btn-default"
download
rel="nofollow"
@@ -87,8 +75,8 @@
</a>
<a
- v-if="browseArtifactsPath"
- :href="browseArtifactsPath"
+ v-if="artifact.browse_path"
+ :href="artifact.browse_path"
class="js-browse-artifacts btn btn-sm btn-default"
>
{{ s__('Job|Browse') }}
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
index 7f485295513..4b1788a1c16 100644
--- a/app/assets/javascripts/jobs/components/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -1,64 +1,56 @@
<script>
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-export default {
- components: {
- ClipboardButton,
- },
- props: {
- pipelineShortSha: {
- type: String,
- required: true,
+ export default {
+ components: {
+ ClipboardButton,
},
- pipelineShaPath: {
- type: String,
- required: true,
+ props: {
+ commit: {
+ type: Object,
+ required: true,
+ },
+ mergeRequest: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ isLastBlock: {
+ type: Boolean,
+ required: true,
+ },
},
- mergeRequestReference: {
- type: String,
- required: false,
- default: null,
- },
- mergeRequestPath: {
- type: String,
- required: false,
- default: null,
- },
- gitCommitTitlte: {
- type: String,
- required: true,
- },
- },
-};
+ };
</script>
<template>
- <div class="block">
+ <div
+ :class="{
+ 'block-last': isLastBlock,
+ block: !isLastBlock
+ }">
<p>
{{ __('Commit') }}
<a
- :href="pipelineShaPath"
+ :href="commit.commit_path"
class="js-commit-sha commit-sha link-commit"
- >
- {{ pipelineShortSha }}
- </a>
+ >{{ commit.short_id }}</a>
<clipboard-button
- :text="pipelineShortSha"
+ :text="commit.short_id"
:title="__('Copy commit SHA to clipboard')"
+ css-class="btn btn-clipboard btn-transparent"
/>
<a
- v-if="mergeRequestPath && mergeRequestReference"
- :href="mergeRequestPath"
+ v-if="mergeRequest"
+ :href="mergeRequest.path"
class="js-link-commit link-commit"
- >
- {{ mergeRequestReference }}
- </a>
+ >!{{ mergeRequest.iid }}</a>
</p>
<p class="build-light-text append-bottom-0">
- {{ gitCommitTitlte }}
+ {{ commit.title }}
</p>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index 4faf08387fb..ff500eddddb 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -25,9 +25,9 @@
validator(value) {
return (
value === null ||
- (Object.prototype.hasOwnProperty.call(value, 'link') &&
+ (Object.prototype.hasOwnProperty.call(value, 'path') &&
Object.prototype.hasOwnProperty.call(value, 'method') &&
- Object.prototype.hasOwnProperty.call(value, 'title'))
+ Object.prototype.hasOwnProperty.call(value, 'button_title'))
);
},
},
@@ -63,11 +63,11 @@
class="text-center"
>
<a
- :href="action.link"
+ :href="action.path"
:data-method="action.method"
class="js-job-empty-state-action btn btn-primary"
>
- {{ action.title }}
+ {{ action.button_title }}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue
index ca6386595c7..e6e1d418194 100644
--- a/app/assets/javascripts/jobs/components/environments_block.vue
+++ b/app/assets/javascripts/jobs/components/environments_block.vue
@@ -12,12 +12,16 @@
type: Object,
required: true,
},
+ iconStatus: {
+ type: Object,
+ required: true,
+ },
},
computed: {
environment() {
let environmentText;
switch (this.deploymentStatus.status) {
- case 'latest':
+ case 'last':
environmentText = sprintf(
__('This job is the most recent deployment to %{link}.'),
{ link: this.environmentLink },
@@ -32,7 +36,7 @@
),
{
environmentLink: this.environmentLink,
- deploymentLink: this.deploymentLink,
+ deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`),
},
false,
);
@@ -56,11 +60,11 @@
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
- 'This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}.',
+ 'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
- deploymentLink: this.deploymentLink,
+ deploymentLink: this.deploymentLink(__('latest deployment')),
},
false,
);
@@ -78,41 +82,57 @@
return environmentText;
},
environmentLink() {
- return sprintf(
- '%{startLink}%{name}%{endLink}',
- {
- startLink: `<a href="${this.deploymentStatus.environment.path}">`,
- name: _.escape(this.deploymentStatus.environment.name),
- endLink: '</a>',
- },
- false,
- );
+ if (this.hasEnvironment) {
+ return sprintf(
+ '%{startLink}%{name}%{endLink}',
+ {
+ startLink: `<a href="${
+ this.deploymentStatus.environment.environment_path
+ }" class="js-environment-link">`,
+ name: _.escape(this.deploymentStatus.environment.name),
+ endLink: '</a>',
+ },
+ false,
+ );
+ }
+ return '';
},
- deploymentLink() {
+ hasLastDeployment() {
+ return this.hasEnvironment && this.deploymentStatus.environment.last_deployment;
+ },
+ lastDeployment() {
+ return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {};
+ },
+ hasEnvironment() {
+ return !_.isEmpty(this.deploymentStatus.environment);
+ },
+ lastDeploymentPath() {
+ return !_.isEmpty(this.lastDeployment.deployable) ? this.lastDeployment.deployable.build_path : '';
+ },
+ },
+ methods: {
+ deploymentLink(name) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
- startLink: `<a href="${this.lastDeployment.path}">`,
- name: _.escape(this.lastDeployment.name),
+ startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`,
+ name,
endLink: '</a>',
},
false,
);
},
- hasLastDeployment() {
- return this.deploymentStatus.environment.last_deployment;
- },
- lastDeployment() {
- return this.deploymentStatus.environment.last_deployment;
- },
},
};
</script>
<template>
<div class="prepend-top-default js-environment-container">
<div class="environment-information">
- <ci-icon :status="deploymentStatus.icon" />
- <p v-html="environment"></p>
+ <ci-icon :status="iconStatus"/>
+ <p
+ class="inline append-bottom-0"
+ v-html="environment"
+ ></p>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue
index d688eebfa95..3d6d9ba4387 100644
--- a/app/assets/javascripts/jobs/components/erased_block.vue
+++ b/app/assets/javascripts/jobs/components/erased_block.vue
@@ -1,39 +1,36 @@
<script>
-import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+ import _ from 'underscore';
+ import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-export default {
- components: {
- TimeagoTooltip,
- },
- props: {
- erasedByUser: {
- type: Boolean,
- required: true,
+ export default {
+ components: {
+ TimeagoTooltip,
},
- username: {
- type: String,
- required: false,
- default: null,
+ props: {
+ user: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ erasedAt: {
+ type: String,
+ required: true,
+ },
},
- linkToUser: {
- type: String,
- required: false,
- default: null,
+ computed: {
+ isErasedByUser() {
+ return !_.isEmpty(this.user);
+ },
},
- erasedAt: {
- type: String,
- required: true,
- },
- },
-};
+ };
</script>
<template>
<div class="prepend-top-default js-build-erased">
<div class="erased alert alert-warning">
- <template v-if="erasedByUser">
+ <template v-if="isErasedByUser">
{{ s__("Job|Job has been erased by") }}
- <a :href="linkToUser">
- {{ username }}
+ <a :href="user.web_url">
+ {{ user.username }}
</a>
</template>
<template v-else>
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
deleted file mode 100644
index 1e7f4b2c3f7..00000000000
--- a/app/assets/javascripts/jobs/components/header.vue
+++ /dev/null
@@ -1,97 +0,0 @@
-<script>
-import ciHeader from '../../vue_shared/components/header_ci_component.vue';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-import callout from '../../vue_shared/components/callout.vue';
-
-export default {
- name: 'JobHeaderSection',
- components: {
- ciHeader,
- loadingIcon,
- callout,
- },
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- actions: this.getActions(),
- };
- },
- computed: {
- status() {
- return this.job && this.job.status;
- },
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length;
- },
- shouldRenderReason() {
- return !!(this.job.status && this.job.callout_message);
- },
- /**
- * When job has not started the key will be `false`
- * When job started the key will be a string with a date.
- */
- jobStarted() {
- return !this.job.started === false;
- },
- headerTime() {
- return this.jobStarted ? this.job.started : this.job.created_at;
- },
- },
- watch: {
- job() {
- this.actions = this.getActions();
- },
- },
- methods: {
- getActions() {
- const actions = [];
-
- if (this.job.new_issue_path) {
- actions.push({
- label: 'New issue',
- path: this.job.new_issue_path,
- cssClass: 'js-new-issue btn btn-new btn-inverted d-none d-md-block d-lg-block d-xl-block',
- type: 'link',
- });
- }
- return actions;
- },
- },
-};
-</script>
-<template>
- <header>
- <div class="js-build-header build-header top-area">
- <ci-header
- v-if="shouldRenderContent"
- :status="status"
- :item-id="job.id"
- :time="headerTime"
- :user="job.user"
- :actions="actions"
- :has-sidebar-button="true"
- :should-render-triggered-label="jobStarted"
- item-name="Job"
- />
- <loading-icon
- v-if="isLoading"
- size="2"
- class="prepend-top-default append-bottom-default"
- />
- </div>
-
- <callout
- v-if="shouldRenderReason"
- :message="job.callout_message"
- />
- </header>
-</template>
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
new file mode 100644
index 00000000000..047e55866ce
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -0,0 +1,114 @@
+<script>
+ import { mapGetters, mapState } from 'vuex';
+ import CiHeader from '~/vue_shared/components/header_ci_component.vue';
+ import Callout from '~/vue_shared/components/callout.vue';
+ import EmptyState from './empty_state.vue';
+ import EnvironmentsBlock from './environments_block.vue';
+ import ErasedBlock from './erased_block.vue';
+ import StuckBlock from './stuck_block.vue';
+
+ export default {
+ name: 'JobPageApp',
+ components: {
+ CiHeader,
+ Callout,
+ EmptyState,
+ EnvironmentsBlock,
+ ErasedBlock,
+ StuckBlock,
+ },
+ props: {
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ ...mapState(['isLoading', 'job']),
+ ...mapGetters([
+ 'headerActions',
+ 'headerTime',
+ 'shouldRenderCalloutMessage',
+ 'jobHasStarted',
+ 'hasEnvironment',
+ 'isJobStuck',
+ 'hasTrace',
+ 'emptyStateIllustration',
+ ]),
+ },
+ };
+</script>
+<template>
+ <div>
+ <gl-loading-icon
+ v-if="isLoading"
+ :size="2"
+ class="prepend-top-20"
+ />
+
+ <template v-else>
+ <!-- Header Section -->
+ <header>
+ <div class="js-build-header build-header top-area">
+ <ci-header
+ :status="job.status"
+ :item-id="job.id"
+ :time="headerTime"
+ :user="job.user"
+ :actions="headerActions"
+ :has-sidebar-button="true"
+ :should-render-triggered-label="jobHasStarted"
+ :item-name="__('Job')"
+ />
+ </div>
+
+ <callout
+ v-if="shouldRenderCalloutMessage"
+ :message="job.callout_message"
+ />
+ </header>
+ <!-- EO Header Section -->
+
+ <!-- Body Section -->
+ <stuck-block
+ v-if="isJobStuck"
+ class="js-job-stuck"
+ :has-no-runners-for-project="job.runners.available"
+ :tags="job.tags"
+ :runners-path="runnerHelpUrl"
+ />
+
+ <environments-block
+ v-if="hasEnvironment"
+ class="js-job-environment"
+ :deployment-status="job.deployment_status"
+ :icon-status="job.status"
+ />
+
+ <erased-block
+ v-if="job.erased"
+ class="js-job-erased"
+ :user="job.erased_by"
+ :erased-at="job.erased_at"
+ />
+
+ <!--job log -->
+ <!-- EO job log -->
+
+ <!--empty state -->
+ <empty-state
+ v-if="!hasTrace"
+ class="js-job-empty-state"
+ :illustration-path="emptyStateIllustration.image"
+ :illustration-size-class="emptyStateIllustration.size"
+ :title="emptyStateIllustration.title"
+ :content="emptyStateIllustration.content"
+ :action="job.status.action"
+ />
+ <!-- EO empty state -->
+
+ <!-- EO Body Section -->
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue
index 3c4749d996b..b12e963b60c 100644
--- a/app/assets/javascripts/jobs/components/job_log.vue
+++ b/app/assets/javascripts/jobs/components/job_log.vue
@@ -6,7 +6,7 @@
type: String,
required: true,
},
- isReceivingBuildTrace: {
+ isComplete: {
type: Boolean,
required: true,
},
@@ -22,7 +22,7 @@
</code>
<div
- v-if="isReceivingBuildTrace"
+ v-if="isComplete"
class="js-log-animation build-loader-animation"
>
<div class="dot"></div>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 513851e376f..3e62ababea3 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -1,8 +1,9 @@
<script>
+ import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
- import { s__, sprintf } from '~/locale';
+ import { sprintf } from '~/locale';
export default {
components: {
@@ -12,44 +13,48 @@
tooltip,
},
props: {
- canEraseJob: {
- type: Boolean,
- required: true,
+ erasePath: {
+ type: String,
+ required: false,
+ default: null,
},
size: {
type: Number,
required: true,
},
- rawTracePath: {
+ rawPath: {
type: String,
required: false,
default: null,
},
- canScrollToTop: {
+ isScrollTopDisabled: {
+ type: Boolean,
+ required: true,
+ },
+ isScrollBottomDisabled: {
+ type: Boolean,
+ required: true,
+ },
+ isScrollingDown: {
type: Boolean,
required: true,
},
- canScrollToBottom: {
+ isTraceSizeVisible: {
type: Boolean,
required: true,
},
},
computed: {
jobLogSize() {
- return sprintf('Showing last %{startSpanTag} %{size} %{endSpanTag} of log -', {
- startSpanTag: '<span class="s-truncated-info-size truncated-info-size">',
- endSpanTag: '</span>',
+ return sprintf('Showing last %{size} of log -', {
size: numberToHumanSize(this.size),
});
},
},
+ mounted() {
+ polyfillSticky(this.$el);
+ },
methods: {
- handleEraseJobClick() {
- // eslint-disable-next-line no-alert
- if (window.confirm(s__('Job|Are you sure you want to erase this job?'))) {
- this.$emit('eraseJob');
- }
- },
handleScrollToTop() {
this.$emit('scrollJobLogTop');
},
@@ -57,48 +62,52 @@
this.$emit('scrollJobLogBottom');
},
},
+
};
</script>
<template>
<div class="top-bar">
<!-- truncate information -->
<div class="js-truncated-info truncated-info d-none d-sm-block float-left">
- <p v-html="jobLogSize"></p>
+ <template v-if="isTraceSizeVisible">
+ {{ jobLogSize }}
- <a
- v-if="rawTracePath"
- :href="rawTracePath"
- class="js-raw-link raw-link"
- >
- {{ s__("Job|Complete Raw") }}
- </a>
+ <a
+ v-if="rawPath"
+ :href="rawPath"
+ class="js-raw-link raw-link"
+ >
+ {{ s__("Job|Complete Raw") }}
+ </a>
+ </template>
</div>
<!-- eo truncate information -->
<div class="controllers float-right">
<!-- links -->
<a
+ v-if="rawPath"
v-tooltip
- v-if="rawTracePath"
:title="s__('Job|Show complete raw')"
- :href="rawTracePath"
+ :href="rawPath"
class="js-raw-link-controller controllers-buttons"
data-container="body"
>
<icon name="doc-text" />
</a>
- <button
+ <a
+ v-if="erasePath"
v-tooltip
- v-if="canEraseJob"
:title="s__('Job|Erase job log')"
- type="button"
+ :href="erasePath"
+ data-confirm="__('Are you sure you want to erase this build?')"
class="js-erase-link controllers-buttons"
data-container="body"
- @click="handleEraseJobClick"
+ data-method="post"
>
<icon name="remove" />
- </button>
+ </a>
<!-- eo links -->
<!-- scroll buttons -->
@@ -109,7 +118,7 @@
data-container="body"
>
<button
- :disabled="!canScrollToTop"
+ :disabled="isScrollTopDisabled"
type="button"
class="js-scroll-top btn-scroll btn-transparent btn-blank"
@click="handleScrollToTop"
@@ -125,9 +134,10 @@
data-container="body"
>
<button
- :disabled="!canScrollToBottom"
+ :disabled="isScrollBottomDisabled"
type="button"
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
+ :class="{ animate: isScrollingDown }"
@click="handleScrollToBottom"
>
<icon name="scroll_down"/>
diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue
index b81109bdd06..271b7790d75 100644
--- a/app/assets/javascripts/jobs/components/jobs_container.vue
+++ b/app/assets/javascripts/jobs/components/jobs_container.vue
@@ -1,4 +1,5 @@
<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';
@@ -16,26 +17,39 @@
type: Array,
required: true,
},
+ jobId: {
+ type: Number,
+ required: true,
+ },
+ },
+ methods: {
+ isJobActive(currentJobId) {
+ return this.jobId === currentJobId;
+ },
+ tooltipText(job) {
+ return `${_.escape(job.name)} - ${job.status.tooltip}`;
+ },
},
};
</script>
<template>
- <div class="builds-container">
+ <div class="js-jobs-container builds-container">
<div
+ v-for="job in jobs"
+ :key="job.id"
class="build-job"
+ :class="{ retried: job.retried, active: isJobActive(job.id) }"
>
<a
v-tooltip
- v-for="job in jobs"
- :key="job.id"
- :href="job.path"
- :title="job.tooltip"
- :class="{ active: job.active, retried: job.retried }"
+ :href="job.status.details_path"
+ :title="tooltipText(job)"
+ data-container="body"
>
<icon
- v-if="job.active"
+ v-if="isJobActive(job.id)"
name="arrow-right"
- class="js-arrow-right"
+ class="js-arrow-right icon-arrow-right"
/>
<ci-icon :status="job.status" />
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
new file mode 100644
index 00000000000..22bcd402e72
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -0,0 +1,297 @@
+<script>
+ import _ from 'underscore';
+ import { mapActions, mapState } from 'vuex';
+ import timeagoMixin from '~/vue_shared/mixins/timeago';
+ import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
+ import Icon from '~/vue_shared/components/icon.vue';
+ import DetailRow from './sidebar_detail_row.vue';
+ import ArtifactsBlock from './artifacts_block.vue';
+ import TriggerBlock from './trigger_block.vue';
+ import CommitBlock from './commit_block.vue';
+ import StagesDropdown from './stages_dropdown.vue';
+ import JobsContainer from './jobs_container.vue';
+
+ export default {
+ name: 'JobSidebar',
+ components: {
+ ArtifactsBlock,
+ CommitBlock,
+ DetailRow,
+ Icon,
+ TriggerBlock,
+ StagesDropdown,
+ JobsContainer,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ terminalPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ ...mapState(['job', 'isLoading', 'stages', 'jobs']),
+ coverage() {
+ return `${this.job.coverage}%`;
+ },
+ duration() {
+ return timeIntervalInWords(this.job.duration);
+ },
+ queued() {
+ return timeIntervalInWords(this.job.queued);
+ },
+ runnerId() {
+ return `${this.job.runner.description} (#${this.job.runner.id})`;
+ },
+ retryButtonClass() {
+ let className =
+ 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
+ className +=
+ this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
+ return className;
+ },
+ hasTimeout() {
+ return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
+ },
+ timeout() {
+ if (this.job.metadata == null) {
+ return '';
+ }
+
+ let t = this.job.metadata.timeout_human_readable;
+ if (this.job.metadata.timeout_source !== '') {
+ t += ` (from ${this.job.metadata.timeout_source})`;
+ }
+
+ return t;
+ },
+ renderBlock() {
+ return (
+ this.job.merge_request ||
+ this.job.duration ||
+ this.job.finished_data ||
+ this.job.erased_at ||
+ this.job.queued ||
+ this.job.runner ||
+ this.job.coverage ||
+ this.job.tags.length ||
+ this.job.cancel_path
+ );
+ },
+ hasArtifact() {
+ return !_.isEmpty(this.job.artifact);
+ },
+ hasTriggers() {
+ return !_.isEmpty(this.job.trigger);
+ },
+ hasStages() {
+ return (
+ (this.job &&
+ this.job.pipeline &&
+ this.job.pipeline.stages &&
+ this.job.pipeline.stages.length > 0) ||
+ false
+ );
+ },
+ commit() {
+ return this.job.pipeline.commit || {};
+ },
+ },
+ methods: {
+ ...mapActions(['fetchJobsForStage']),
+ },
+ };
+</script>
+<template>
+ <aside
+ class="right-sidebar right-sidebar-expanded build-sidebar"
+ data-offset-top="101"
+ data-spy="affix"
+ >
+ <div class="sidebar-container">
+ <div class="blocks-container">
+ <template v-if="!isLoading">
+ <div class="block">
+ <strong class="inline prepend-top-8">
+ {{ job.name }}
+ </strong>
+ <a
+ v-if="job.retry_path"
+ :class="retryButtonClass"
+ :href="job.retry_path"
+ data-method="post"
+ rel="nofollow"
+ >
+ {{ __('Retry') }}
+ </a>
+ <a
+ v-if="terminalPath"
+ :href="terminalPath"
+ class="js-terminal-link pull-right btn btn-primary
+ btn-inverted visible-md-block visible-lg-block"
+ target="_blank"
+ >
+ {{ __('Debug') }}
+ <icon name="external-link" />
+ </a>
+ <button
+ :aria-label="__('Toggle Sidebar')"
+ type="button"
+ class="btn btn-blank gutter-toggle
+ float-right d-block d-md-none js-sidebar-build-toggle"
+ >
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-angle-double-right"
+ ></i>
+ </button>
+ </div>
+ <div
+ v-if="job.retry_path || job.new_issue_path"
+ class="block retry-link"
+ >
+ <a
+ v-if="job.new_issue_path"
+ :href="job.new_issue_path"
+ class="js-new-issue btn btn-success btn-inverted"
+ >
+ {{ __('New issue') }}
+ </a>
+ <a
+ v-if="job.retry_path"
+ :href="job.retry_path"
+ class="js-retry-job btn btn-inverted-secondary"
+ data-method="post"
+ rel="nofollow"
+ >
+ {{ __('Retry') }}
+ </a>
+ </div>
+ <div :class="{ block : renderBlock }">
+ <p
+ v-if="job.merge_request"
+ class="build-detail-row js-job-mr"
+ >
+ <span class="build-light-text">
+ {{ __('Merge Request:') }}
+ </span>
+ <a :href="job.merge_request.path">
+ !{{ job.merge_request.iid }}
+ </a>
+ </p>
+
+ <detail-row
+ v-if="job.duration"
+ :value="duration"
+ class="js-job-duration"
+ title="Duration"
+ />
+ <detail-row
+ v-if="job.finished_at"
+ :value="timeFormated(job.finished_at)"
+ class="js-job-finished"
+ title="Finished"
+ />
+ <detail-row
+ v-if="job.erased_at"
+ :value="timeFormated(job.erased_at)"
+ class="js-job-erased"
+ title="Erased"
+ />
+ <detail-row
+ v-if="job.queued"
+ :value="queued"
+ class="js-job-queued"
+ title="Queued"
+ />
+ <detail-row
+ v-if="hasTimeout"
+ :help-url="runnerHelpUrl"
+ :value="timeout"
+ class="js-job-timeout"
+ title="Timeout"
+ />
+ <detail-row
+ v-if="job.runner"
+ :value="runnerId"
+ class="js-job-runner"
+ title="Runner"
+ />
+ <detail-row
+ v-if="job.coverage"
+ :value="coverage"
+ class="js-job-coverage"
+ title="Coverage"
+ />
+ <p
+ v-if="job.tags.length"
+ class="build-detail-row js-job-tags"
+ >
+ <span class="build-light-text">
+ {{ __('Tags:') }}
+ </span>
+ <span
+ v-for="(tag, i) in job.tags"
+ :key="i"
+ class="label label-primary">
+ {{ tag }}
+ </span>
+ </p>
+
+ <div
+ v-if="job.cancel_path"
+ class="btn-group prepend-top-5"
+ role="group">
+ <a
+ :href="job.cancel_path"
+ class="js-cancel-job btn btn-sm btn-default"
+ data-method="post"
+ rel="nofollow"
+ >
+ {{ __('Cancel') }}
+ </a>
+ </div>
+ </div>
+ <artifacts-block
+ v-if="hasArtifact"
+ :artifact="job.artifact"
+ />
+ <trigger-block
+ v-if="hasTriggers"
+ :trigger="job.trigger"
+ />
+ <commit-block
+ :is-last-block="hasStages"
+ :commit="commit"
+ :merge-request="job.merge_request"
+ />
+
+ <stages-dropdown
+ :stages="stages"
+ :pipeline="job.pipeline"
+ @requestSidebarStageDropdown="fetchJobsForStage"
+ />
+
+ </template>
+ <gl-loading-icon
+ v-else
+ :size="2"
+ class="prepend-top-10"
+ />
+ </div>
+
+ <jobs-container
+ v-if="!isLoading && jobs.length"
+ :jobs="jobs"
+ :job-id="job.id"
+ />
+ </div>
+ </aside>
+</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
deleted file mode 100644
index 36d4a3e2bc9..00000000000
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ /dev/null
@@ -1,241 +0,0 @@
-<script>
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
-import Icon from '~/vue_shared/components/icon.vue';
-import DetailRow from './sidebar_detail_row.vue';
-
-export default {
- name: 'SidebarDetailsBlock',
- components: {
- DetailRow,
- LoadingIcon,
- Icon,
- },
- mixins: [timeagoMixin],
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- runnerHelpUrl: {
- type: String,
- required: false,
- default: '',
- },
- terminalPath: {
- type: String,
- required: false,
- default: null,
- },
- },
- computed: {
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length > 0;
- },
- coverage() {
- return `${this.job.coverage}%`;
- },
- duration() {
- return timeIntervalInWords(this.job.duration);
- },
- queued() {
- return timeIntervalInWords(this.job.queued);
- },
- runnerId() {
- return `${this.job.runner.description} (#${this.job.runner.id})`;
- },
- retryButtonClass() {
- let className =
- 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
- className +=
- this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
- return className;
- },
- hasTimeout() {
- return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
- },
- timeout() {
- if (this.job.metadata == null) {
- return '';
- }
-
- let t = this.job.metadata.timeout_human_readable;
- if (this.job.metadata.timeout_source !== '') {
- t += ` (from ${this.job.metadata.timeout_source})`;
- }
-
- return t;
- },
- renderBlock() {
- return (
- this.job.merge_request ||
- this.job.duration ||
- this.job.finished_data ||
- this.job.erased_at ||
- this.job.queued ||
- this.job.runner ||
- this.job.coverage ||
- this.job.tags.length ||
- this.job.cancel_path
- );
- },
- },
-};
-</script>
-<template>
- <div>
- <div class="block">
- <strong class="inline prepend-top-8">
- {{ job.name }}
- </strong>
- <a
- v-if="job.retry_path"
- :class="retryButtonClass"
- :href="job.retry_path"
- data-method="post"
- rel="nofollow"
- >
- {{ __('Retry') }}
- </a>
- <a
- v-if="terminalPath"
- :href="terminalPath"
- class="js-terminal-link pull-right btn btn-primary
- btn-inverted visible-md-block visible-lg-block"
- target="_blank"
- >
- {{ __('Debug') }}
- <icon name="external-link" />
- </a>
- <button
- :aria-label="__('Toggle Sidebar')"
- type="button"
- class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
- >
- <i
- aria-hidden="true"
- data-hidden="true"
- class="fa fa-angle-double-right"
- ></i>
- </button>
- </div>
- <template v-if="shouldRenderContent">
- <div
- v-if="job.retry_path || job.new_issue_path"
- class="block retry-link"
- >
- <a
- v-if="job.new_issue_path"
- :href="job.new_issue_path"
- class="js-new-issue btn btn-new btn-inverted"
- >
- {{ __('New issue') }}
- </a>
- <a
- v-if="job.retry_path"
- :href="job.retry_path"
- class="js-retry-job btn btn-inverted-secondary"
- data-method="post"
- rel="nofollow"
- >
- {{ __('Retry') }}
- </a>
- </div>
- <div :class="{block : renderBlock }">
- <p
- v-if="job.merge_request"
- class="build-detail-row js-job-mr"
- >
- <span class="build-light-text">
- {{ __('Merge Request:') }}
- </span>
- <a :href="job.merge_request.path">
- !{{ job.merge_request.iid }}
- </a>
- </p>
-
- <detail-row
- v-if="job.duration"
- :value="duration"
- class="js-job-duration"
- title="Duration"
- />
- <detail-row
- v-if="job.finished_at"
- :value="timeFormated(job.finished_at)"
- class="js-job-finished"
- title="Finished"
- />
- <detail-row
- v-if="job.erased_at"
- :value="timeFormated(job.erased_at)"
- class="js-job-erased"
- title="Erased"
- />
- <detail-row
- v-if="job.queued"
- :value="queued"
- class="js-job-queued"
- title="Queued"
- />
- <detail-row
- v-if="hasTimeout"
- :help-url="runnerHelpUrl"
- :value="timeout"
- class="js-job-timeout"
- title="Timeout"
- />
- <detail-row
- v-if="job.runner"
- :value="runnerId"
- class="js-job-runner"
- title="Runner"
- />
- <detail-row
- v-if="job.coverage"
- :value="coverage"
- class="js-job-coverage"
- title="Coverage"
- />
- <p
- v-if="job.tags.length"
- class="build-detail-row js-job-tags"
- >
- <span class="build-light-text">
- {{ __('Tags:') }}
- </span>
- <span
- v-for="(tag, i) in job.tags"
- :key="i"
- class="label label-primary">
- {{ tag }}
- </span>
- </p>
-
- <div
- v-if="job.cancel_path"
- class="btn-group prepend-top-5"
- role="group">
- <a
- :href="job.cancel_path"
- class="js-cancel-job btn btn-sm btn-default"
- data-method="post"
- rel="nofollow"
- >
- {{ __('Cancel') }}
- </a>
- </div>
- </div>
- </template>
- <loading-icon
- v-if="isLoading"
- class="prepend-top-10"
- size="2"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index d6d64fa32f7..1c15af55a8b 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -1,8 +1,8 @@
<script>
+ import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
-
- import { sprintf, __ } from '~/locale';
+ import { __ } from '~/locale';
export default {
components: {
@@ -10,30 +10,14 @@
Icon,
},
props: {
- pipelineId: {
- type: Number,
- required: true,
- },
- pipelinePath: {
- type: String,
- required: true,
- },
- pipelineRef: {
- type: String,
- required: true,
- },
- pipelineRefPath: {
- type: String,
+ pipeline: {
+ type: Object,
required: true,
},
stages: {
type: Array,
required: true,
},
- pipelineStatus: {
- type: Object,
- required: true,
- },
},
data() {
return {
@@ -41,57 +25,73 @@
};
},
computed: {
- pipelineLink() {
- return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), {
- pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`,
- pipelineId: this.pipelineId,
- pipelineLinkEnd: '</a>',
- pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`,
- pipelineRef: this.pipelineRef,
- pipelineLinkRefEnd: '</a>',
- }, false);
+ hasRef() {
+ return !_.isEmpty(this.pipeline.ref);
+ },
+ },
+ watch: {
+ // When the component is initially mounted it may start with an empty stages array.
+ // Once the prop is updated, we set the first stage as the selected one
+ stages(newVal) {
+ if (newVal.length) {
+ this.selectedStage = newVal[0].name;
+ }
},
},
methods: {
onStageClick(stage) {
- // todo: consider moving into store
- this.selectedStage = stage.name;
-
- // update dropdown with jobs
- // jobs container is a new component.
this.$emit('requestSidebarStageDropdown', stage);
+ this.selectedStage = stage.name;
},
},
};
</script>
<template>
- <div class="block-last">
- <ci-icon :status="pipelineStatus" />
+ <div class="block-last dropdown">
+ <ci-icon
+ :status="pipeline.details.status"
+ class="vertical-align-middle"
+ />
+
+ {{ __('Pipeline') }}
+ <a
+ :href="pipeline.path"
+ class="js-pipeline-path link-commit"
+ >
+ #{{ pipeline.id }}
+ </a>
+ <template v-if="hasRef">
+ {{ __('from') }}
+ <a
+ :href="pipeline.ref.path"
+ class="link-commit ref-name"
+ >
+ {{ pipeline.ref.name }}
+ </a>
+ </template>
- <p v-html="pipelineLink"></p>
+ <button
+ type="button"
+ data-toggle="dropdown"
+ class="js-selected-stage dropdown-menu-toggle prepend-top-8"
+ >
+ {{ selectedStage }}
+ <i class="fa fa-chevron-down" ></i>
+ </button>
- <div class="dropdown">
- <button
- type="button"
- data-toggle="dropdown"
+ <ul class="dropdown-menu">
+ <li
+ v-for="stage in stages"
+ :key="stage.name"
>
- {{ selectedStage }}
- <icon name="chevron-down" />
- </button>
- <ul class="dropdown-menu">
- <li
- v-for="(stage, index) in stages"
- :key="index"
+ <button
+ type="button"
+ class="js-stage-item stage-item"
+ @click="onStageClick(stage)"
>
- <button
- type="button"
- class="stage-item"
- @click="onStageClick(stage)"
- >
- {{ stage.name }}
- </button>
- </li>
- </ul>
- </div>
+ {{ stage.name }}
+ </button>
+ </li>
+ </ul>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index 18883fea950..a60643b2c65 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -24,14 +24,14 @@ export default {
<div class="bs-callout bs-callout-warning">
<p
v-if="hasNoRunnersForProject"
- class="js-stuck-no-runners"
+ 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"
- class="js-stuck-with-tags"
+ class="js-stuck-with-tags append-bottom-0"
>
{{ s__(`This job is stuck, because you don't have
any active runners online with any of these tags assigned to them:`) }}
@@ -45,7 +45,7 @@ export default {
</p>
<p
v-else
- class="js-stuck-no-active-runner"
+ class="js-stuck-no-active-runner append-bottom-0"
>
{{ s__(`This job is stuck, because you don't
have any active runners that can run this job.`) }}
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 8a88e5da6aa..d7b3c4fcb5b 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -1,16 +1,9 @@
<script>
export default {
props: {
- shortToken: {
- type: String,
- required: false,
- default: null,
- },
-
- variables: {
+ trigger: {
type: Object,
- required: false,
- default: () => ({}),
+ required: true,
},
},
data() {
@@ -20,7 +13,7 @@
},
computed: {
hasVariables() {
- return Object.keys(this.variables).length > 0;
+ return this.trigger.variables && this.trigger.variables.length > 0;
},
},
methods: {
@@ -38,17 +31,18 @@
</h4>
<p
- v-if="shortToken"
+ v-if="trigger.short_token"
class="js-short-token"
>
<span class="build-light-text">
{{ __('Token') }}
</span>
- {{ shortToken }}
+ {{ trigger.short_token }}
</p>
<p v-if="hasVariables">
<button
+ v-if="!areVariablesVisible"
type="button"
class="btn btn-default group js-reveal-variables"
@click="revealVariables"
@@ -63,20 +57,20 @@
class="js-build-variables trigger-build-variables"
>
<template
- v-for="(value, key) in variables"
+ v-for="variable in trigger.variables"
>
<dt
- :key="`${key}-variable`"
+ :key="`${variable.key}-variable`"
class="js-build-variable trigger-build-variable"
>
- {{ key }}
+ {{ variable.key }}
</dt>
<dd
- :key="`${key}-value`"
+ :key="`${variable.key}-value`"
class="js-build-value trigger-build-value"
>
- {{ value }}
+ {{ variable.value }}
</dd>
</template>
</dl>
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index a84324f14b2..3eb75e72506 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -1,34 +1,39 @@
+import _ from 'underscore';
+import { mapState, mapActions } from 'vuex';
import Vue from 'vue';
-import JobMediator from './job_details_mediator';
-import jobHeader from './components/header.vue';
-import detailsBlock from './components/sidebar_details_block.vue';
+import Job from '../job';
+import JobApp from './components/job_app.vue';
+import Sidebar from './components/sidebar.vue';
+import createStore from './store';
export default () => {
const { dataset } = document.getElementById('js-job-details-vue');
- const mediator = new JobMediator({ endpoint: dataset.endpoint });
- mediator.fetchJob();
+ // eslint-disable-next-line no-new
+ new Job();
+
+ const store = createStore();
+ store.dispatch('setJobEndpoint', dataset.endpoint);
+
+ store.dispatch('fetchJob');
// Header
// eslint-disable-next-line no-new
new Vue({
el: '#js-build-header-vue',
components: {
- jobHeader,
- },
- data() {
- return {
- mediator,
- };
+ JobApp,
},
- mounted() {
- this.mediator.initBuildClass();
+ store,
+ computed: {
+ ...mapState(['job', 'isLoading']),
},
render(createElement) {
- return createElement('job-header', {
+ return createElement('job-app', {
props: {
- isLoading: this.mediator.state.isLoading,
- job: this.mediator.store.state.job,
+ isLoading: this.isLoading,
+ job: this.job,
+ runnerHelpUrl: dataset.runnerHelpUrl,
},
});
},
@@ -41,18 +46,25 @@ export default () => {
new Vue({
el: detailsBlockElement,
components: {
- detailsBlock,
+ Sidebar,
+ },
+ computed: {
+ ...mapState(['job']),
+ },
+ watch: {
+ job(newVal, oldVal) {
+ if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
+ this.fetchStages();
+ }
+ },
},
- data() {
- return {
- mediator,
- };
+ methods: {
+ ...mapActions(['fetchStages']),
},
+ store,
render(createElement) {
- return createElement('details-block', {
+ return createElement('sidebar', {
props: {
- isLoading: this.mediator.state.isLoading,
- job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath,
},
diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js
deleted file mode 100644
index 89019da9d1e..00000000000
--- a/app/assets/javascripts/jobs/job_details_mediator.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import Visibility from 'visibilityjs';
-import Flash from '../flash';
-import Poll from '../lib/utils/poll';
-import JobStore from './stores/job_store';
-import JobService from './services/job_service';
-import Job from '../job';
-import handleRevealVariables from '../build_variables';
-
-export default class JobMediator {
- constructor(options = {}) {
- this.options = options;
-
- this.store = new JobStore();
- this.service = new JobService(options.endpoint);
-
- this.state = {
- isLoading: false,
- };
- }
-
- initBuildClass() {
- this.build = new Job();
- handleRevealVariables();
- }
-
- fetchJob() {
- this.poll = new Poll({
- resource: this.service,
- method: 'getJob',
- successCallback: response => this.successCallback(response),
- errorCallback: () => this.errorCallback(),
- });
-
- if (!Visibility.hidden()) {
- this.state.isLoading = true;
- this.poll.makeRequest();
- } else {
- this.getJob();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.poll.restart();
- } else {
- this.poll.stop();
- }
- });
- }
-
- getJob() {
- return this.service
- .getJob()
- .then(response => this.successCallback(response))
- .catch(() => this.errorCallback());
- }
-
- successCallback(response) {
- this.state.isLoading = false;
- return this.store.storeJob(response.data);
- }
-
- errorCallback() {
- this.state.isLoading = false;
-
- return new Flash('An error occurred while fetching the job.');
- }
-}
diff --git a/app/assets/javascripts/jobs/services/job_service.js b/app/assets/javascripts/jobs/services/job_service.js
deleted file mode 100644
index b746489c45c..00000000000
--- a/app/assets/javascripts/jobs/services/job_service.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import axios from '../../lib/utils/axios_utils';
-
-export default class JobService {
- constructor(endpoint) {
- this.job = endpoint;
- }
-
- getJob() {
- return axios.get(this.job);
- }
-}
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
new file mode 100644
index 00000000000..298367c9342
--- /dev/null
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -0,0 +1,187 @@
+import Visibility from 'visibilityjs';
+import * as types from './mutation_types';
+import axios from '../../lib/utils/axios_utils';
+import Poll from '../../lib/utils/poll';
+import { setCiStatusFavicon } from '../../lib/utils/common_utils';
+import flash from '../../flash';
+import { __ } from '../../locale';
+
+export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
+export const setTraceEndpoint = ({ commit }, endpoint) =>
+ commit(types.SET_TRACE_ENDPOINT, endpoint);
+export const setStagesEndpoint = ({ commit }, endpoint) =>
+ commit(types.SET_STAGES_ENDPOINT, endpoint);
+export const setJobsEndpoint = ({ commit }, endpoint) => commit(types.SET_JOBS_ENDPOINT, endpoint);
+
+let eTagPoll;
+
+export const clearEtagPoll = () => {
+ eTagPoll = null;
+};
+
+export const stopPolling = () => {
+ if (eTagPoll) eTagPoll.stop();
+};
+
+export const restartPolling = () => {
+ if (eTagPoll) eTagPoll.restart();
+};
+
+export const requestJob = ({ commit }) => commit(types.REQUEST_JOB);
+
+export const fetchJob = ({ state, dispatch }) => {
+ dispatch('requestJob');
+
+ eTagPoll = new Poll({
+ resource: {
+ getJob(endpoint) {
+ return axios.get(endpoint);
+ },
+ },
+ data: state.jobEndpoint,
+ method: 'getJob',
+ successCallback: ({ data }) => dispatch('receiveJobSuccess', data),
+ errorCallback: () => dispatch('receiveJobError'),
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ } else {
+ axios
+ .get(state.jobEndpoint)
+ .then(({ data }) => dispatch('receiveJobSuccess', data))
+ .catch(() => dispatch('receiveJobError'));
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ dispatch('restartPolling');
+ } else {
+ dispatch('stopPolling');
+ }
+ });
+};
+
+export const receiveJobSuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_JOB_SUCCESS, data);
+};
+export const receiveJobError = ({ commit }) => {
+ commit(types.RECEIVE_JOB_ERROR);
+ flash(__('An error occurred while fetching the job.'));
+};
+
+/**
+ * Job's Trace
+ */
+export const scrollTop = ({ commit }) => {
+ commit(types.SCROLL_TO_TOP);
+ window.scrollTo({ top: 0 });
+};
+
+export const scrollBottom = ({ commit }) => {
+ commit(types.SCROLL_TO_BOTTOM);
+ window.scrollTo({ top: document.height });
+};
+
+export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE);
+
+let traceTimeout;
+export const fetchTrace = ({ dispatch, state }) => {
+ dispatch('requestTrace');
+
+ axios
+ .get(`${state.traceEndpoint}/trace.json`, {
+ params: { state: state.traceState },
+ })
+ .then(({ data }) => {
+ if (!state.fetchingStatusFavicon) {
+ dispatch('fetchFavicon');
+ }
+ dispatch('receiveTraceSuccess', data);
+
+ if (!data.complete) {
+ traceTimeout = setTimeout(() => {
+ dispatch('fetchTrace');
+ }, 4000);
+ } else {
+ dispatch('stopPollingTrace');
+ }
+ })
+ .catch(() => dispatch('receiveTraceError'));
+};
+export const stopPollingTrace = ({ commit }) => {
+ commit(types.STOP_POLLING_TRACE);
+ clearTimeout(traceTimeout);
+};
+export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log);
+export const receiveTraceError = ({ commit }) => {
+ commit(types.RECEIVE_TRACE_ERROR);
+ clearTimeout(traceTimeout);
+ flash(__('An error occurred while fetching the job log.'));
+};
+
+export const fetchFavicon = ({ state, dispatch }) => {
+ dispatch('requestStatusFavicon');
+ setCiStatusFavicon(`${state.pagePath}/status.json`)
+ .then(() => dispatch('receiveStatusFaviconSuccess'))
+ .catch(() => dispatch('requestStatusFaviconError'));
+};
+export const requestStatusFavicon = ({ commit }) => commit(types.REQUEST_STATUS_FAVICON);
+export const receiveStatusFaviconSuccess = ({ commit }) =>
+ commit(types.RECEIVE_STATUS_FAVICON_SUCCESS);
+export const requestStatusFaviconError = ({ commit }) => commit(types.RECEIVE_STATUS_FAVICON_ERROR);
+
+/**
+ * Stages dropdown on sidebar
+ */
+export const requestStages = ({ commit }) => commit(types.REQUEST_STAGES);
+export const fetchStages = ({ state, dispatch }) => {
+ dispatch('requestStages');
+
+ axios
+ .get(state.job.pipeline.path)
+ .then(({ data }) => {
+ dispatch('receiveStagesSuccess', data.details.stages);
+ dispatch('fetchJobsForStage', data.details.stages[0]);
+ })
+ .catch(() => dispatch('receiveStagesError'));
+};
+export const receiveStagesSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_STAGES_SUCCESS, data);
+export const receiveStagesError = ({ commit }) => {
+ commit(types.RECEIVE_STAGES_ERROR);
+ flash(__('An error occurred while fetching stages.'));
+};
+
+/**
+ * Jobs list on sidebar - depend on stages dropdown
+ */
+export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE);
+
+// On stage click, set selected stage + fetch job
+export const fetchJobsForStage = ({ dispatch }, stage) => {
+ dispatch('requestJobsForStage');
+
+ axios
+ .get(stage.dropdown_path, {
+ params: {
+ retried: 1,
+ },
+ })
+ .then(({ data }) => {
+ const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true }));
+ const jobs = data.latest_statuses.concat(retriedJobs);
+
+ dispatch('receiveJobsForStageSuccess', jobs);
+ })
+ .catch(() => dispatch('receiveJobsForStageError'));
+};
+export const receiveJobsForStageSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, data);
+export const receiveJobsForStageError = ({ commit }) => {
+ commit(types.RECEIVE_JOBS_FOR_STAGE_ERROR);
+ flash(__('An error occurred while fetching the jobs.'));
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
new file mode 100644
index 00000000000..afe5f88b292
--- /dev/null
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -0,0 +1,53 @@
+import _ from 'underscore';
+import { __ } from '~/locale';
+
+export const headerActions = state => {
+ if (state.job.new_issue_path) {
+ return [
+ {
+ label: __('New issue'),
+ path: state.job.new_issue_path,
+ cssClass:
+ 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block',
+ type: 'link',
+ },
+ ];
+ }
+ return [];
+};
+
+export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at);
+
+export const shouldRenderCalloutMessage = state =>
+ !_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message);
+
+/**
+ * When job has not started the key will be `false`
+ * When job started the key will be a string with a date.
+ */
+export const jobHasStarted = state => !(state.job.started === false);
+
+export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
+
+/**
+ * Checks if it the job has trace.
+ * Used to check if it should render the job log or the empty state
+ * @returns {Boolean}
+ */
+export const hasTrace = state => state.job.has_trace || state.job.status.group === 'running';
+
+export const emptyStateIllustration = state =>
+ (state.job && state.job.status && state.job.status.illustration) || {};
+
+/**
+ * When the job is pending and there are no available runners
+ * we need to render the stuck block;
+ *
+ * @returns {Boolean}
+ */
+export const isJobStuck = state =>
+ state.job.status.group === 'pending' &&
+ (!_.isEmpty(state.job.runners) && state.job.runners.available === false);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/jobs/store/index.js b/app/assets/javascripts/jobs/store/index.js
new file mode 100644
index 00000000000..96e38f9a2fa
--- /dev/null
+++ b/app/assets/javascripts/jobs/store/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default () => new Vuex.Store({
+ actions,
+ mutations,
+ getters,
+ state: state(),
+});
diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js
new file mode 100644
index 00000000000..e66e1d4f116
--- /dev/null
+++ b/app/assets/javascripts/jobs/store/mutation_types.js
@@ -0,0 +1,29 @@
+export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT';
+export const SET_TRACE_ENDPOINT = 'SET_TRACE_ENDPOINT';
+export const SET_STAGES_ENDPOINT = 'SET_STAGES_ENDPOINT';
+export const SET_JOBS_ENDPOINT = 'SET_JOBS_ENDPOINT';
+
+export const SCROLL_TO_TOP = 'SCROLL_TO_TOP';
+export const SCROLL_TO_BOTTOM = 'SCROLL_TO_BOTTOM';
+
+export const REQUEST_JOB = 'REQUEST_JOB';
+export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS';
+export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR';
+
+export const REQUEST_TRACE = 'REQUEST_TRACE';
+export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE';
+export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS';
+export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR';
+
+export const REQUEST_STATUS_FAVICON = 'REQUEST_STATUS_FAVICON';
+export const RECEIVE_STATUS_FAVICON_SUCCESS = 'RECEIVE_STATUS_FAVICON_SUCCESS';
+export const RECEIVE_STATUS_FAVICON_ERROR = 'RECEIVE_STATUS_FAVICON_ERROR';
+
+export const REQUEST_STAGES = 'REQUEST_STAGES';
+export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS';
+export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR';
+
+export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
+export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE';
+export const RECEIVE_JOBS_FOR_STAGE_SUCCESS = 'RECEIVE_JOBS_FOR_STAGE_SUCCESS';
+export const RECEIVE_JOBS_FOR_STAGE_ERROR = 'RECEIVE_JOBS_FOR_STAGE_ERROR';
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
new file mode 100644
index 00000000000..c3f2359fa4d
--- /dev/null
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -0,0 +1,95 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_JOB_ENDPOINT](state, endpoint) {
+ state.jobEndpoint = endpoint;
+ },
+ [types.REQUEST_STATUS_FAVICON](state) {
+ state.fetchingStatusFavicon = true;
+ },
+ [types.RECEIVE_STATUS_FAVICON_SUCCESS](state) {
+ state.fetchingStatusFavicon = false;
+ },
+ [types.RECEIVE_STATUS_FAVICON_ERROR](state) {
+ state.fetchingStatusFavicon = false;
+ },
+
+ [types.RECEIVE_TRACE_SUCCESS](state, log) {
+ if (log.state) {
+ state.traceState = log.state;
+ }
+
+ if (log.append) {
+ state.trace += log.html;
+ state.traceSize += log.size;
+ } else {
+ state.trace = log.html;
+ state.traceSize = log.size;
+ }
+
+ if (state.traceSize < log.total) {
+ state.isTraceSizeVisible = true;
+ } else {
+ state.isTraceSizeVisible = false;
+ }
+
+ state.isTraceComplete = log.complete;
+ state.hasTraceError = false;
+ },
+ [types.STOP_POLLING_TRACE](state) {
+ state.isTraceComplete = true;
+ },
+ // todo_fl: check this.
+ [types.RECEIVE_TRACE_ERROR](state) {
+ state.isLoadingTrace = false;
+ state.isTraceComplete = true;
+ state.hasTraceError = true;
+ },
+
+ [types.REQUEST_JOB](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_JOB_SUCCESS](state, job) {
+ state.isLoading = false;
+ state.hasError = false;
+ state.job = job;
+ },
+ [types.RECEIVE_JOB_ERROR](state) {
+ state.isLoading = false;
+ state.hasError = true;
+ state.job = {};
+ },
+
+ [types.SCROLL_TO_TOP](state) {
+ state.isTraceScrolledToBottom = false;
+ state.hasBeenScrolled = true;
+ },
+ [types.SCROLL_TO_BOTTOM](state) {
+ state.isTraceScrolledToBottom = true;
+ state.hasBeenScrolled = true;
+ },
+
+ [types.REQUEST_STAGES](state) {
+ state.isLoadingStages = true;
+ },
+ [types.RECEIVE_STAGES_SUCCESS](state, stages) {
+ state.isLoadingStages = false;
+ state.stages = stages;
+ },
+ [types.RECEIVE_STAGES_ERROR](state) {
+ state.isLoadingStages = false;
+ state.stages = [];
+ },
+
+ [types.REQUEST_JOBS_FOR_STAGE](state) {
+ state.isLoadingJobs = true;
+ },
+ [types.RECEIVE_JOBS_FOR_STAGE_SUCCESS](state, jobs) {
+ state.isLoadingJobs = false;
+ state.jobs = jobs;
+ },
+ [types.RECEIVE_JOBS_FOR_STAGE_ERROR](state) {
+ state.isLoadingJobs = false;
+ state.jobs = [];
+ },
+};
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js
new file mode 100644
index 00000000000..509cb69a5d3
--- /dev/null
+++ b/app/assets/javascripts/jobs/store/state.js
@@ -0,0 +1,40 @@
+export default () => ({
+ jobEndpoint: null,
+ traceEndpoint: null,
+
+ // dropdown options
+ stagesEndpoint: null,
+ // list of jobs on sidebard
+ stageJobsEndpoint: null,
+
+ // job log
+ isLoading: false,
+ hasError: false,
+ job: {},
+
+ // trace
+ isLoadingTrace: false,
+ hasTraceError: false,
+
+ trace: '',
+
+ isTraceScrolledToBottom: false,
+ hasBeenScrolled: false,
+
+ isTraceComplete: false,
+ traceSize: 0, // todo_fl: needs to be converted into human readable format in components
+ isTraceSizeVisible: false,
+
+ fetchingStatusFavicon: false,
+ // used as a query parameter
+ traceState: null,
+ // used to check if we need to redirect the user - todo_fl: check if actually needed
+ traceStatus: null,
+
+ // sidebar dropdown
+ isLoadingStages: false,
+ isLoadingJobs: false,
+ selectedStage: null,
+ stages: [],
+ jobs: [],
+});
diff --git a/app/assets/javascripts/jobs/stores/job_store.js b/app/assets/javascripts/jobs/stores/job_store.js
deleted file mode 100644
index 766194b8387..00000000000
--- a/app/assets/javascripts/jobs/stores/job_store.js
+++ /dev/null
@@ -1,11 +0,0 @@
-export default class JobStore {
- constructor() {
- this.state = {
- job: {},
- };
- }
-
- storeJob(job = {}) {
- this.state.job = job;
- }
-}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 6499b919787..1c7bca78df3 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -449,11 +449,11 @@ export default class LabelsSelect {
}
bindEvents() {
- return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
+ return $('body').on('change', '.selected-issuable', this.onSelectCheckboxIssue);
}
// eslint-disable-next-line class-methods-use-this
onSelectCheckboxIssue() {
- if ($('.selected_issue:checked').length) {
+ if ($('.selected-issuable:checked').length) {
return;
}
return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index bd2212edec7..61b4862b4e3 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -2,54 +2,114 @@ import _ from 'underscore';
export const placeholderImage =
'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
-const SCROLL_THRESHOLD = 300;
+const SCROLL_THRESHOLD = 500;
export default class LazyLoader {
constructor(options = {}) {
+ this.intersectionObserver = null;
this.lazyImages = [];
this.observerNode = options.observerNode || '#content-body';
- const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
- const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
-
- window.addEventListener('scroll', throttledScrollCheck);
- window.addEventListener('resize', debouncedElementsInView);
-
const scrollContainer = options.scrollContainer || window;
- scrollContainer.addEventListener('load', () => this.loadCheck());
+ scrollContainer.addEventListener('load', () => this.register());
+ }
+
+ static supportsIntersectionObserver() {
+ return 'IntersectionObserver' in window;
}
+
searchLazyImages() {
- const that = this;
requestIdleCallback(
() => {
- that.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
+ const lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
- if (that.lazyImages.length) {
- that.checkElementsInView();
+ if (LazyLoader.supportsIntersectionObserver()) {
+ if (this.intersectionObserver) {
+ lazyImages.forEach(img => this.intersectionObserver.observe(img));
+ }
+ } else if (lazyImages.length) {
+ this.lazyImages = lazyImages;
+ this.checkElementsInView();
}
},
{ timeout: 500 },
);
}
+
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
-
if (contentNode) {
- const observer = new MutationObserver(() => this.searchLazyImages());
+ this.mutationObserver = new MutationObserver(() => this.searchLazyImages());
- observer.observe(contentNode, {
+ this.mutationObserver.observe(contentNode, {
childList: true,
subtree: true,
});
}
}
- loadCheck() {
- this.searchLazyImages();
+
+ stopContentObserver() {
+ if (this.mutationObserver) {
+ this.mutationObserver.takeRecords();
+ this.mutationObserver.disconnect();
+ this.mutationObserver = null;
+ }
+ }
+
+ unregister() {
+ this.stopContentObserver();
+ if (this.intersectionObserver) {
+ this.intersectionObserver.takeRecords();
+ this.intersectionObserver.disconnect();
+ this.intersectionObserver = null;
+ }
+ if (this.throttledScrollCheck) {
+ window.removeEventListener('scroll', this.throttledScrollCheck);
+ }
+ if (this.debouncedElementsInView) {
+ window.removeEventListener('resize', this.debouncedElementsInView);
+ }
+ }
+
+ register() {
+ if (LazyLoader.supportsIntersectionObserver()) {
+ this.startIntersectionObserver();
+ } else {
+ this.startLegacyObserver();
+ }
this.startContentObserver();
+ this.searchLazyImages();
}
+
+ startIntersectionObserver = () => {
+ this.throttledElementsInView = _.throttle(() => this.checkElementsInView(), 300);
+ this.intersectionObserver = new IntersectionObserver(this.onIntersection, {
+ rootMargin: `${SCROLL_THRESHOLD}px 0px`,
+ thresholds: 0.1,
+ });
+ };
+
+ onIntersection = entries => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ this.intersectionObserver.unobserve(entry.target);
+ this.lazyImages.push(entry.target);
+ }
+ });
+ this.throttledElementsInView();
+ };
+
+ startLegacyObserver() {
+ this.throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
+ this.debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
+ window.addEventListener('scroll', this.throttledScrollCheck);
+ window.addEventListener('resize', this.debouncedElementsInView);
+ }
+
scrollCheck() {
requestAnimationFrame(() => this.checkElementsInView());
}
+
checkElementsInView() {
const scrollTop = window.pageYOffset;
const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD;
@@ -61,18 +121,29 @@ export default class LazyLoader {
const imgTop = scrollTop + imgBoundRect.top;
const imgBound = imgTop + imgBoundRect.height;
- if (scrollTop < imgBound && visHeight > imgTop) {
+ if (scrollTop <= imgBound && visHeight >= imgTop) {
requestAnimationFrame(() => {
LazyLoader.loadImage(selectedImage);
});
return false;
}
+ /*
+ If we are scrolling fast, the img we watched intersecting could have left the view port.
+ So we are going watch for new intersections.
+ */
+ if (LazyLoader.supportsIntersectionObserver()) {
+ if (this.intersectionObserver) {
+ this.intersectionObserver.observe(selectedImage);
+ }
+ return false;
+ }
return true;
}
return false;
});
}
+
static loadImage(img) {
if (img.getAttribute('data-src')) {
let imgUrl = img.getAttribute('data-src');
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 2f3dd6f6cbc..e14fff7a610 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -56,7 +56,8 @@ export const rstrip = val => {
return val;
};
-export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('_fixTitle');
+export const updateTooltipTitle = ($tooltipEl, newTitle) =>
+ $tooltipEl.attr('title', newTitle).tooltip('_fixTitle');
export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
const field = $(fieldSelector);
@@ -86,6 +87,8 @@ export const handleLocationHash = () => {
const fixedTabs = document.querySelector('.js-tabs-affix');
const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab');
+ const performanceBar = document.querySelector('#js-peek');
+ const topPadding = 8;
let adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
@@ -102,6 +105,14 @@ export const handleLocationHash = () => {
adjustment -= fixedDiffStats.offsetHeight;
}
+ if (performanceBar) {
+ adjustment -= performanceBar.offsetHeight;
+ }
+
+ if (isInMRPage()) {
+ adjustment -= topPadding;
+ }
+
window.scrollBy(0, adjustment);
};
@@ -131,17 +142,43 @@ export const parseUrlPathname = url => {
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`;
};
-// We can trust that each param has one & since values containing & will be encoded
-// Remove the first character of search as it is always ?
-export const getUrlParamsArray = () =>
- window.location.search
- .slice(1)
- .split('&')
+const splitPath = (path = '') => path.replace(/^\?/, '').split('&');
+
+export const urlParamsToArray = (path = '') =>
+ splitPath(path)
+ .filter(param => param.length > 0)
.map(param => {
const split = param.split('=');
return [decodeURI(split[0]), split[1]].join('=');
});
+export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
+
+export const urlParamsToObject = (path = '') =>
+ splitPath(path).reduce((dataParam, filterParam) => {
+ if (filterParam === '') {
+ return dataParam;
+ }
+
+ const data = dataParam;
+ let [key, value] = filterParam.split('=');
+ const isArray = key.includes('[]');
+ key = key.replace('[]', '');
+ value = decodeURIComponent(value.replace(/\+/g, ' '));
+
+ if (isArray) {
+ if (!data[key]) {
+ data[key] = [];
+ }
+
+ data[key].push(value);
+ } else {
+ data[key] = value;
+ }
+
+ return data;
+ }, {});
+
export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// Identify following special clicks
@@ -189,7 +226,7 @@ export const getParameterByName = (name, urlToParse) => {
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
-const handleSelectedRange = (range) => {
+const handleSelectedRange = range => {
const container = range.commonAncestorContainer;
// add context to fragment if needed
if (container.tagName === 'OL') {
@@ -349,8 +386,11 @@ export const objectToQueryString = (params = {}) =>
.map(param => `${param}=${params[param]}`)
.join('&');
-export const buildUrlWithCurrentLocation = param =>
- (param ? `${window.location.pathname}${param}` : window.location.pathname);
+export const buildUrlWithCurrentLocation = param => {
+ if (param) return `${window.location.pathname}${param}`;
+
+ return window.location.pathname;
+};
/**
* Based on the current location and the string parameters provided
@@ -426,7 +466,7 @@ export const backOff = (fn, timeout = 60000) => {
export const createOverlayIcon = (iconPath, overlayPath) => {
const faviconImage = document.createElement('img');
- return new Promise((resolve) => {
+ return new Promise(resolve => {
faviconImage.onload = () => {
const size = 32;
@@ -437,13 +477,29 @@ export const createOverlayIcon = (iconPath, overlayPath) => {
const context = canvas.getContext('2d');
context.clearRect(0, 0, size, size);
context.drawImage(
- faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size,
+ faviconImage,
+ 0,
+ 0,
+ faviconImage.width,
+ faviconImage.height,
+ 0,
+ 0,
+ size,
+ size,
);
const overlayImage = document.createElement('img');
overlayImage.onload = () => {
context.drawImage(
- overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size,
+ overlayImage,
+ 0,
+ 0,
+ overlayImage.width,
+ overlayImage.height,
+ 0,
+ 0,
+ size,
+ size,
);
const faviconWithOverlayUrl = canvas.toDataURL();
@@ -456,17 +512,21 @@ export const createOverlayIcon = (iconPath, overlayPath) => {
});
};
-export const setFaviconOverlay = (overlayPath) => {
+export const setFaviconOverlay = overlayPath => {
const faviconEl = document.getElementById('favicon');
- if (!faviconEl) { return null; }
+ if (!faviconEl) {
+ return null;
+ }
const iconPath = faviconEl.getAttribute('data-original-href');
- return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl));
+ return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl =>
+ faviconEl.setAttribute('href', faviconWithOverlayUrl),
+ );
};
-export const setFavicon = (faviconPath) => {
+export const setFavicon = faviconPath => {
const faviconEl = document.getElementById('favicon');
if (faviconEl && faviconPath) {
faviconEl.setAttribute('href', faviconPath);
@@ -491,7 +551,10 @@ export const setCiStatusFavicon = pageUrl =>
}
return resetFavicon();
})
- .catch(resetFavicon);
+ .catch(error => {
+ resetFavicon();
+ throw error;
+ });
export const spriteIcon = (icon, className = '') => {
const classAttribute = className.length > 0 ? `class="${className}"` : '';
@@ -561,6 +624,17 @@ export const roundOffFloat = (number, precision = 0) => {
return Math.round(number * multiplier) / multiplier;
};
+/**
+ * Represents navigation type constants of the Performance Navigation API.
+ * Detailed explanation see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigation.
+ */
+export const NavigationType = {
+ TYPE_NAVIGATE: 0,
+ TYPE_RELOAD: 1,
+ TYPE_BACK_FORWARD: 2,
+ TYPE_RESERVED: 255,
+};
+
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 1f66fa811ea..833dbefd3dc 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -370,3 +370,24 @@ window.gl.utils = {
getTimeago,
localTimeAgo,
};
+
+/**
+ * Formats milliseconds as timestamp (e.g. 01:02:03).
+ * This takes durations longer than a day into account (e.g. two days would be 48:00:00).
+ *
+ * @param milliseconds
+ * @returns {string}
+ */
+export const formatTime = milliseconds => {
+ const remainingSeconds = Math.floor(milliseconds / 1000) % 60;
+ const remainingMinutes = Math.floor(milliseconds / 1000 / 60) % 60;
+ const remainingHours = Math.floor(milliseconds / 1000 / 60 / 60);
+ let formattedTime = '';
+ if (remainingHours < 10) formattedTime += '0';
+ formattedTime += `${remainingHours}:`;
+ if (remainingMinutes < 10) formattedTime += '0';
+ formattedTime += `${remainingMinutes}:`;
+ if (remainingSeconds < 10) formattedTime += '0';
+ formattedTime += remainingSeconds;
+ return formattedTime;
+};
diff --git a/app/assets/javascripts/lib/utils/logoutput_behaviours.js b/app/assets/javascripts/lib/utils/logoutput_behaviours.js
index 1bf99d935ef..41b57025cc9 100644
--- a/app/assets/javascripts/lib/utils/logoutput_behaviours.js
+++ b/app/assets/javascripts/lib/utils/logoutput_behaviours.js
@@ -1,5 +1,11 @@
import $ from 'jquery';
-import { canScroll, isScrolledToBottom, toggleDisableButton } from './scroll_utils';
+import {
+ canScroll,
+ isScrolledToBottom,
+ isScrolledToTop,
+ isScrolledToMiddle,
+ toggleDisableButton,
+} from './scroll_utils';
export default class LogOutputBehaviours {
constructor() {
@@ -12,18 +18,13 @@ export default class LogOutputBehaviours {
}
toggleScroll() {
- const $document = $(document);
- const currentPosition = $document.scrollTop();
- const scrollHeight = $document.height();
-
- const windowHeight = $(window).height();
if (canScroll()) {
- if (currentPosition > 0 && scrollHeight - currentPosition !== windowHeight) {
+ if (isScrolledToMiddle()) {
// User is in the middle of the log
toggleDisableButton(this.$scrollTopBtn, false);
toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (currentPosition === 0) {
+ } else if (isScrolledToTop()) {
// User is at Top of Log
toggleDisableButton(this.$scrollTopBtn, true);
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/lib/utils/navigation_utility.js
index 9f69f110d06..1579b225e44 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/lib/utils/navigation_utility.js
@@ -1,4 +1,4 @@
-import { visitUrl } from './lib/utils/url_utility';
+import { visitUrl } from './url_utility';
/**
* Helper function that finds the href of the fiven selector and updates the location.
diff --git a/app/assets/javascripts/lib/utils/scroll_utils.js b/app/assets/javascripts/lib/utils/scroll_utils.js
index 9313b570863..b4da1e16f08 100644
--- a/app/assets/javascripts/lib/utils/scroll_utils.js
+++ b/app/assets/javascripts/lib/utils/scroll_utils.js
@@ -4,6 +4,7 @@ export const canScroll = () => $(document).height() > $(window).height();
/**
* Checks if the entire page is scrolled down all the way to the bottom
+ * @returns {Boolean}
*/
export const isScrolledToBottom = () => {
const $document = $(document);
@@ -16,11 +17,34 @@ export const isScrolledToBottom = () => {
return scrollHeight - currentPosition === windowHeight;
};
+/**
+ * Checks if page is scrolled to the top
+ * @returns {Boolean}
+ */
+export const isScrolledToTop = () => $(document).scrollTop() === 0;
+
export const scrollDown = () => {
const $document = $(document);
$document.scrollTop($document.height());
};
+export const scrollUp = () => {
+ $(document).scrollTop(0);
+};
+
+/**
+ * Checks if scroll position is in the middle of the page
+ * @returns {Boolean}
+ */
+export const isScrolledToMiddle = () => {
+ const $document = $(document);
+ const currentPosition = $document.scrollTop();
+ const scrollHeight = $document.height();
+ const windowHeight = $(window).height();
+
+ return currentPosition > 0 && scrollHeight - currentPosition !== windowHeight;
+};
+
export const toggleDisableButton = ($button, disable) => {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index ce0bc4d40e9..f7429601afa 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -31,11 +31,17 @@ function blockTagText(text, textArea, blockTag, selected) {
}
}
-function moveCursor(textArea, tag, wrapped, removedLastNewLine) {
+function moveCursor({ textArea, tag, wrapped, removedLastNewLine, select }) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
+ if (select && select.length > 0) {
+ // calculate the part of the text to be selected
+ const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
+ const endPosition = startPosition + select.length;
+ return textArea.setSelectionRange(startPosition, endPosition);
+ }
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
@@ -51,7 +57,7 @@ function moveCursor(textArea, tag, wrapped, removedLastNewLine) {
}
}
-export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap) {
+export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select }) {
var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
@@ -82,11 +88,16 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+ const textPlaceholder = '{text}';
+
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
textToInsert = blockTagText(text, textArea, blockTag, selected);
} else {
textToInsert = selectedSplit.map(function(val) {
+ if (tag.indexOf(textPlaceholder) > -1) {
+ return tag.replace(textPlaceholder, val);
+ }
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
@@ -94,6 +105,8 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
}
}).join('\n');
}
+ } else if (tag.indexOf(textPlaceholder) > -1) {
+ textToInsert = tag.replace(textPlaceholder, selected);
} else {
textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
@@ -107,17 +120,17 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
}
insertText(textArea, textToInsert);
- return moveCursor(textArea, tag, wrap, removedLastNewLine);
+ return moveCursor({ textArea, tag: tag.replace(textPlaceholder, selected), wrap, removedLastNewLine, select });
}
-function updateText(textArea, tag, blockTag, wrap) {
+function updateText({ textArea, tag, blockTag, wrap, select }) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
selected = selectedText(text, textArea);
$textArea.focus();
- return insertMarkdownText(textArea, text, tag, blockTag, selected, wrap);
+ return insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select });
}
function replaceRange(s, start, end, substitute) {
@@ -127,7 +140,12 @@ function replaceRange(s, start, end, substitute) {
export function addMarkdownListeners(form) {
return $('.js-md', form).off('click').on('click', function() {
const $this = $(this);
- return updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend'));
+ return updateText({
+ textArea: $this.closest('.md-area').find('textarea'),
+ tag: $this.data('mdTag'),
+ blockTag: $this.data('mdBlock'),
+ wrap: !$this.data('mdPrepend'),
+ select: $this.data('mdSelect') });
});
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 2be3c97bd95..879f94a26ec 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -49,6 +49,16 @@ export const dasherize = str => str.replace(/[_\s]+/g, '-');
export const slugify = str => str.trim().toLowerCase();
/**
+ * Replaces whitespaces with hyphens and converts to lower case
+ * @param {String} str
+ * @returns {String}
+ */
+export const slugifyWithHyphens = str => {
+ const regex = new RegExp(/\s+/, 'g');
+ return str.toLowerCase().replace(regex, '-');
+};
+
+/**
* Truncates given text
*
* @param {String} string
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 72b72f4247d..a282c2df441 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -47,9 +47,9 @@ export function removeParamQueryString(url, param) {
return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&');
}
-export function removeParams(params) {
+export function removeParams(params, source = window.location.href) {
const url = document.createElement('a');
- url.href = window.location.href;
+ url.href = source;
params.forEach(param => {
url.search = removeParamQueryString(url.search, param);
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 2718f73a830..e8aac51a299 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -2,7 +2,6 @@
import jQuery from 'jquery';
import Cookies from 'js-cookie';
-import svg4everybody from 'svg4everybody';
// bootstrap webpack, common libs, polyfills, and behaviors
import './webpack';
@@ -25,10 +24,12 @@ import initLayoutNav from './layout_nav';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
-import './milestone_select';
import './frequent_items';
import initBreadcrumbs from './breadcrumb';
-import initDispatcher from './dispatcher';
+import initUsagePingConsent from './usage_ping_consent';
+import initPerformanceBar from './performance_bar';
+import initSearchAutocomplete from './search_autocomplete';
+import GlFieldErrors from './gl_field_errors';
// expose jQuery as global (TODO: remove these)
window.jQuery = jQuery;
@@ -40,8 +41,6 @@ if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
import(/* webpackMode: "eager" */ './test_utils/');
}
-svg4everybody();
-
document.addEventListener('beforeunload', () => {
// Unbind scroll events
$(document).off('scroll');
@@ -78,6 +77,10 @@ document.addEventListener('DOMContentLoaded', () => {
initImporterStatus();
initTodoToggle();
initLogoAnimation();
+ initUsagePingConsent();
+
+ if (document.querySelector('.search')) initSearchAutocomplete();
+ if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' });
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
@@ -268,5 +271,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
- initDispatcher();
+ // initialize field errors
+ $('.gl-show-field-errors').each((i, form) => new GlFieldErrors(form));
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 53d7504de35..78f56ab57ff 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -115,8 +115,9 @@ export default class MergeRequestTabs {
this.mergeRequestTabs &&
this.mergeRequestTabs.querySelector(`a[data-action='${action}']`) &&
this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click
- )
+ ) {
this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click();
+ }
this.initAffix();
}
@@ -193,9 +194,7 @@ export default class MergeRequestTabs {
if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
- if (this.diffViewType() === 'parallel') {
- this.expandViewContainer();
- }
+ this.expandViewContainer();
this.destroyPipelinesView();
this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
@@ -354,7 +353,7 @@ export default class MergeRequestTabs {
localTimeAgo($('.js-timeago', 'div#diffs'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
- if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
+ if (this.isDiffAction(this.currentAction)) {
this.expandViewContainer();
}
this.diffsLoaded = true;
@@ -407,19 +406,23 @@ export default class MergeRequestTabs {
}
diffViewType() {
- return $('.inline-parallel-buttons a.active').data('viewType');
+ return $('.inline-parallel-buttons button.active').data('viewType');
}
isDiffAction(action) {
return action === 'diffs' || action === 'new/diffs';
}
- expandViewContainer() {
+ expandViewContainer(removeLimited = true) {
const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
- $wrapper.removeClass('container-limited');
+ if (this.diffViewType() === 'parallel' || removeLimited) {
+ $wrapper.removeClass('container-limited');
+ } else {
+ $wrapper.addClass('container-limited');
+ }
}
resetViewContainer() {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index ae96ac3b80c..67338aa96c3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -97,33 +97,45 @@ export default {
store: new MonitoringStore(),
state: 'gettingStarted',
showEmptyState: true,
- updateAspectRatio: false,
- updatedAspectRatios: 0,
hoverData: {},
- resizeThrottled: {},
+ elWidth: 0,
};
},
+ computed: {
+ forceRedraw() {
+ return this.elWidth;
+ },
+ },
created() {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
});
- eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
+ this.mutationObserverConfig = {
+ attributes: true,
+ childList: false,
+ subtree: false,
+ };
eventHub.$on('hoverChanged', this.hoverChanged);
},
beforeDestroy() {
- eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false);
+ this.sidebarMutationObserver.disconnect();
},
mounted() {
- this.resizeThrottled = _.throttle(this.resize, 600);
+ this.resizeThrottled = _.debounce(this.resize, 100);
if (!this.hasMetrics) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
window.addEventListener('resize', this.resizeThrottled, false);
+
+ const sidebarEl = document.querySelector('.nav-sidebar');
+ // The sidebar listener
+ this.sidebarMutationObserver = new MutationObserver(this.resizeThrottled);
+ this.sidebarMutationObserver.observe(sidebarEl, this.mutationObserverConfig);
}
},
methods: {
@@ -153,14 +165,7 @@ export default {
});
},
resize() {
- this.updateAspectRatio = true;
- },
- toggleAspectRatio() {
- this.updatedAspectRatios += 1;
- if (this.store.getMetricsCount() === this.updatedAspectRatios) {
- this.updateAspectRatio = !this.updateAspectRatio;
- this.updatedAspectRatios = 0;
- }
+ this.elWidth = this.$el.clientWidth;
},
hoverChanged(data) {
this.hoverData = data;
@@ -172,6 +177,7 @@ export default {
<template>
<div
v-if="!showEmptyState"
+ :key="forceRedraw"
class="prometheus-graphs prepend-top-default"
>
<div class="environments d-flex align-items-center">
@@ -214,11 +220,10 @@ export default {
:show-panels="showPanels"
>
<graph
- v-for="(graphData, index) in groupData.metrics"
- :key="index"
+ v-for="(graphData, graphIndex) in groupData.metrics"
+ :key="graphIndex"
:graph-data="graphData"
:hover-data="hoverData"
- :update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
:project-path="projectPath"
:tags-path="tagsPath"
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index e5680a0499f..3cccaf72ed7 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -32,10 +32,6 @@ export default {
type: Object,
required: true,
},
- updateAspectRatio: {
- type: Boolean,
- required: true,
- },
deploymentData: {
type: Array,
required: true,
@@ -82,11 +78,13 @@ export default {
value: 0,
},
currentXCoordinate: 0,
- currentCoordinates: [],
+ currentCoordinates: {},
showFlag: false,
showFlagContent: false,
timeSeries: [],
+ graphDrawData: {},
realPixelRatio: 1,
+ seriesUnderMouse: [],
};
},
computed: {
@@ -109,15 +107,6 @@ export default {
},
},
watch: {
- updateAspectRatio() {
- if (this.updateAspectRatio) {
- this.graphHeight = 450;
- this.graphWidth = 600;
- this.measurements = measurements.large;
- this.draw();
- eventHub.$emit('toggleAspectRatio');
- }
- },
hoverData() {
this.positionFlag();
},
@@ -126,6 +115,9 @@ export default {
this.draw();
},
methods: {
+ showDot(path) {
+ return this.showFlagContent && this.seriesUnderMouse.includes(path);
+ },
draw() {
const breakpointSize = bp.getBreakpointSize();
const query = this.graphData.queries[0];
@@ -155,7 +147,24 @@ export default {
point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x += 7;
- const firstTimeSeries = this.timeSeries[0];
+
+ this.seriesUnderMouse = this.timeSeries.filter((series) => {
+ const mouseX = series.timeSeriesScaleX.invert(point.x);
+ let minDistance = Infinity;
+
+ const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => {
+ const distance = Math.abs(Number(new Date(x)) - Number(mouseX));
+ if (distance < minDistance) {
+ minDistance = distance;
+ return x;
+ }
+ return closest;
+ });
+
+ return series.values.find(v => v.time.toString() === closestTickMark);
+ });
+
+ const firstTimeSeries = this.seriesUnderMouse[0];
const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
const d0 = firstTimeSeries.values[overlayIndex - 1];
@@ -172,12 +181,12 @@ export default {
});
},
renderAxesPaths() {
- this.timeSeries = createTimeSeries(
+ ({ timeSeries: this.timeSeries, graphDrawData: this.graphDrawData } = createTimeSeries(
this.graphData.queries,
this.graphWidth,
this.graphHeight,
this.graphHeightOffset,
- );
+ ));
if (_.findWhere(this.timeSeries, { renderCanary: true })) {
this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
@@ -190,6 +199,17 @@ export default {
axisXScale.domain(d3.extent(allValues, d => d.time));
axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
+ this.allXAxisValues = this.timeSeries.reduce((obj, series) => {
+ const seriesKeys = {};
+ series.values.forEach(v => {
+ seriesKeys[v.time] = true;
+ });
+ return {
+ ...obj,
+ ...seriesKeys,
+ };
+ }, {});
+
const xAxis = d3
.axisBottom()
.scale(axisXScale)
@@ -269,6 +289,10 @@ export default {
:viewBox="innerViewBox"
class="graph-data"
>
+ <slot
+ name="additionalSvgContent"
+ :graphDrawData="graphDrawData"
+ />
<graph-path
v-for="(path, index) in timeSeries"
:key="index"
@@ -277,9 +301,8 @@ export default {
:line-style="path.lineStyle"
:line-color="path.lineColor"
:area-color="path.areaColor"
- :current-coordinates="currentCoordinates[index]"
- :current-time-series-index="index"
- :show-dot="showFlagContent"
+ :current-coordinates="currentCoordinates[path.metricTag]"
+ :show-dot="showDot(path)"
/>
<graph-deployment
:deployment-data="reducedDeploymentData"
@@ -303,7 +326,7 @@ export default {
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent"
- :time-series="timeSeries"
+ :time-series="seriesUnderMouse"
:unit-of-display="unitOfDisplay"
:legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData"
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index 1e6803abf3a..5f00d20ca3f 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -52,7 +52,7 @@ export default {
required: true,
},
currentCoordinates: {
- type: Array,
+ type: Object,
required: true,
},
},
@@ -91,8 +91,8 @@ export default {
},
methods: {
seriesMetricValue(seriesIndex, series) {
- const indexFromCoordinates = this.currentCoordinates[seriesIndex]
- ? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
+ const indexFromCoordinates = this.currentCoordinates[series.metricTag]
+ ? this.currentCoordinates[series.metricTag].currentDataIndex : 0;
const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex
: indexFromCoordinates;
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index 3276f3a1ceb..ef18ae5c2c8 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -58,8 +58,8 @@ export default {
</td>
<template v-for="(track, trackIndex) in series.tracksLegend">
<track-line
- :track="track"
- :key="`track-line-${trackIndex}`"/>
+ :key="`track-line-${trackIndex}`"
+ :track="track"/>
<td :key="`track-info-${trackIndex}`">
<track-info
:track="track"
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 4f23814ff3e..007451d5c7a 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -50,19 +50,24 @@ const mixins = {
},
positionFlag() {
- const timeSeries = this.timeSeries[0];
- const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
+ const timeSeries = this.seriesUnderMouse[0];
+ if (!timeSeries) {
+ return;
+ }
+ const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate);
this.currentData = timeSeries.values[hoveredDataIndex];
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
- this.currentCoordinates = this.timeSeries.map((series) => {
- const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
+ this.currentCoordinates = {};
+
+ this.seriesUnderMouse.forEach((series) => {
+ const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate);
const currentData = series.values[currentDataIndex];
const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
- return {
+ this.currentCoordinates[series.metricTag] = {
currentX,
currentY,
currentDataIndex,
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index cee39fd0559..d5971730e31 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -2,7 +2,7 @@ import _ from 'underscore';
import { scaleLinear, scaleTime } from 'd3-scale';
import { line, area, curveLinear } from 'd3-shape';
import { extent, max, sum } from 'd3-array';
-import { timeMinute } from 'd3-time';
+import { timeMinute, timeSecond } from 'd3-time';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
const d3 = {
@@ -14,6 +14,7 @@ const d3 = {
extent,
max,
timeMinute,
+ timeSecond,
sum,
};
@@ -29,7 +30,7 @@ const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
-function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
+function queryTimeSeries(query, graphDrawData, lineStyle) {
let usedColors = [];
let renderCanary = false;
const timeSeriesParsed = [];
@@ -51,6 +52,24 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
return defaultColorPalette[pick];
}
+ function findByDate(series, time) {
+ const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60);
+ if (val) {
+ return val.value;
+ }
+ return NaN;
+ }
+
+ // The timeseries data may have gaps in it
+ // but we need a regularly-spaced set of time/value pairs
+ // this gives us a complete range of one minute intervals
+ // offset the same amount as the original data
+ const [minX, maxX] = graphDrawData.xDom;
+ const offset = d3.timeMinute(minX) - Number(minX);
+ const datesWithoutGaps = d3.timeSecond.every(60)
+ .range(d3.timeMinute.offset(minX, -1), maxX)
+ .map(d => d - offset);
+
query.result.forEach((timeSeries, timeSeriesNumber) => {
let metricTag = '';
let lineColor = '';
@@ -65,31 +84,6 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
renderCanary = true;
}
- const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
-
- const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
-
- timeSeriesScaleX.domain(xDom);
- timeSeriesScaleX.ticks(d3.timeMinute, 60);
- timeSeriesScaleY.domain(yDom);
-
- const defined = d => !Number.isNaN(d.value) && d.value != null;
-
- const lineFunction = d3
- .line()
- .defined(defined)
- .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
- .x(d => timeSeriesScaleX(d.time))
- .y(d => timeSeriesScaleY(d.value));
-
- const areaFunction = d3
- .area()
- .defined(defined)
- .curve(d3.curveLinear)
- .x(d => timeSeriesScaleX(d.time))
- .y0(graphHeight - graphHeightOffset)
- .y1(d => timeSeriesScaleY(d.value));
-
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
const seriesCustomizationData =
query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
@@ -119,11 +113,16 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
});
}
+ const values = datesWithoutGaps.map(time => ({
+ time,
+ value: findByDate(timeSeries.values, time),
+ }));
+
timeSeriesParsed.push({
- linePath: lineFunction(timeSeries.values),
- areaPath: areaFunction(timeSeries.values),
- timeSeriesScaleX,
- timeSeriesScaleY,
+ linePath: graphDrawData.lineFunction(values),
+ areaPath: graphDrawData.areaBelowLine(values),
+ timeSeriesScaleX: graphDrawData.timeSeriesScaleX,
+ timeSeriesScaleY: graphDrawData.timeSeriesScaleY,
values: timeSeries.values,
max: maximumValue,
average: accum / timeSeries.values.length,
@@ -140,7 +139,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
return timeSeriesParsed;
}
-export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
+function xyDomain(queries) {
const allValues = queries.reduce(
(allQueryResults, query) =>
allQueryResults.concat(
@@ -152,10 +151,70 @@ export default function createTimeSeries(queries, graphWidth, graphHeight, graph
const xDom = d3.extent(allValues, d => d.time);
const yDom = [0, d3.max(allValues.map(d => d.value))];
- return queries.reduce((series, query, index) => {
+ return {
+ xDom,
+ yDom,
+ };
+}
+
+export function generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset) {
+ const { xDom, yDom } = xyDomain(queries);
+
+ const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
+ const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
+
+ timeSeriesScaleX.domain(xDom);
+ timeSeriesScaleX.ticks(d3.timeMinute, 60);
+ timeSeriesScaleY.domain(yDom);
+
+ const defined = d => !Number.isNaN(d.value) && d.value != null;
+
+ const lineFunction = d3
+ .line()
+ .defined(defined)
+ .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
+ .x(d => timeSeriesScaleX(d.time))
+ .y(d => timeSeriesScaleY(d.value));
+
+ const areaBelowLine = d3
+ .area()
+ .defined(defined)
+ .curve(d3.curveLinear)
+ .x(d => timeSeriesScaleX(d.time))
+ .y0(graphHeight - graphHeightOffset)
+ .y1(d => timeSeriesScaleY(d.value));
+
+ const areaAboveLine = d3
+ .area()
+ .defined(defined)
+ .curve(d3.curveLinear)
+ .x(d => timeSeriesScaleX(d.time))
+ .y0(0)
+ .y1(d => timeSeriesScaleY(d.value));
+
+ return {
+ lineFunction,
+ areaBelowLine,
+ areaAboveLine,
+ xDom,
+ yDom,
+ timeSeriesScaleX,
+ timeSeriesScaleY,
+ };
+}
+
+export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
+ const graphDrawData = generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset);
+
+ const timeSeries = queries.reduce((series, query, index) => {
const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
return series.concat(
- queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle),
+ queryTimeSeries(query, graphDrawData, lineStyle),
);
}, []);
+
+ return {
+ timeSeries,
+ graphDrawData,
+ };
}
diff --git a/app/assets/javascripts/mr_notes/stores/index.js b/app/assets/javascripts/mr_notes/stores/index.js
index dd2019001db..c4225c8ec08 100644
--- a/app/assets/javascripts/mr_notes/stores/index.js
+++ b/app/assets/javascripts/mr_notes/stores/index.js
@@ -6,10 +6,13 @@ import mrPageModule from './modules';
Vue.use(Vuex);
-export default new Vuex.Store({
- modules: {
- page: mrPageModule,
- notes: notesModule,
- diffs: diffsModule,
- },
-});
+export const createStore = () =>
+ new Vuex.Store({
+ modules: {
+ page: mrPageModule,
+ notes: notesModule(),
+ diffs: diffsModule(),
+ },
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue
index e2e3b08c77f..f241df9620d 100644
--- a/app/assets/javascripts/notebook/index.vue
+++ b/app/assets/javascripts/notebook/index.vue
@@ -51,10 +51,10 @@
<template>
<div v-if="hasNotebook">
<component
- v-for="(cell, index) in cells"
:is="cellType(cell.cell_type)"
- :cell="cell"
+ v-for="(cell, index) in cells"
:key="index"
+ :cell="cell"
:code-css-class="codeCssClass" />
</div>
</template>
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 8124ae6201f..f301f093ef4 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -16,7 +16,7 @@ import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
import syntaxHighlight from '~/syntax_highlight';
-import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
@@ -154,7 +154,11 @@ export default class Notes {
this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
- this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
+ this.$wrapperEl.on(
+ 'click',
+ '.js-toggle-lazy-diff-retry-button',
+ this.onClickRetryLazyLoad.bind(this),
+ );
// fetch notes when tab becomes visible
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
@@ -252,9 +256,7 @@ export default class Notes {
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) {
if ($textarea.val() !== '') {
- if (
- !window.confirm('Are you sure you want to cancel creating this comment?')
- ) {
+ if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
return;
}
}
@@ -266,9 +268,7 @@ export default class Notes {
originalText = $textarea.closest('form').data('originalNote');
newText = $textarea.val();
if (originalText !== newText) {
- if (
- !window.confirm('Are you sure you want to cancel editing this comment?')
- ) {
+ if (!window.confirm('Are you sure you want to cancel editing this comment?')) {
return;
}
}
@@ -631,7 +631,7 @@ export default class Notes {
*
* deactivates the submit button when text is empty
* hides the preview button when text is empty
- * setup GFM auto complete
+ * set up GFM auto complete
* show the form
*/
setupNoteForm(form, enableGFM = defaultAutocompleteConfig) {
@@ -954,7 +954,7 @@ export default class Notes {
* Note: dataHolder must have the "discussionId" and "lineCode" data attributes set.
*/
setupDiscussionNoteForm(dataHolder, form) {
- // setup note target
+ // set up note target
let diffFileData = dataHolder.closest('.text-file');
if (diffFileData.length === 0) {
@@ -1036,7 +1036,7 @@ export default class Notes {
$diffFile[0].dispatchEvent(clickEvent);
- // Setup comment form
+ // Set up comment form
let newForm;
const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
const $form = $noteContainer.find('> .discussion-form');
@@ -1074,7 +1074,7 @@ export default class Notes {
addForm = false;
let lineTypeSelector = '';
rowCssToAdd =
- '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
+ '<tr class="notes_holder js-temp-notes-holder"><td class="notes_content" colspan="3"><div class="content"></div></td></tr>';
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`;
@@ -1293,10 +1293,10 @@ export default class Notes {
new Vue({
el,
components: {
- SkeletonLoadingContainer,
+ SkeletonLoading,
},
render(createElement) {
- return createElement('skeleton-loading-container');
+ return createElement('skeleton-loading');
},
});
}
@@ -1316,8 +1316,7 @@ export default class Notes {
$retryButton.prop('disabled', true);
- return this.loadLazyDiff(e)
- .then(() => {
+ return this.loadLazyDiff(e).then(() => {
$retryButton.prop('disabled', false);
});
}
@@ -1343,18 +1342,18 @@ export default class Notes {
*/
if (url) {
return axios
- .get(url)
- .then(({ data }) => {
- // Reset state in case last request returned error
- $successContainer.removeClass('hidden');
- $errorContainer.addClass('hidden');
-
- Notes.renderDiffContent($container, data);
- })
- .catch(() => {
- $successContainer.addClass('hidden');
- $errorContainer.removeClass('hidden');
- });
+ .get(url)
+ .then(({ data }) => {
+ // Reset state in case last request returned error
+ $successContainer.removeClass('hidden');
+ $errorContainer.addClass('hidden');
+
+ Notes.renderDiffContent($container, data);
+ })
+ .catch(() => {
+ $successContainer.addClass('hidden');
+ $errorContainer.removeClass('hidden');
+ });
}
return Promise.resolve();
}
@@ -1545,12 +1544,8 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/${_.escape(currentUsername)}">
- <span class="d-none d-sm-inline-block">${_.escape(
- currentUsername,
- )}</span>
- <span class="note-headline-light">${_.escape(
- currentUsername,
- )}</span>
+ <span class="d-none d-sm-inline-block">${_.escape(currentUsername)}</span>
+ <span class="note-headline-light">${_.escape(currentUsername)}</span>
</a>
</div>
</div>
@@ -1565,9 +1560,7 @@ export default class Notes {
);
$tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname));
- $tempNote
- .find('.note-headline-light')
- .text(`@${_.escape(currentUsername)}`);
+ $tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`);
return $tempNote;
}
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 6612bc44e0b..b980e43b898 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -7,7 +7,11 @@ import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
-import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase } from '../../lib/utils/text_utility';
+import {
+ capitalizeFirstCharacter,
+ convertToCamelCase,
+ splitCamelCase,
+} from '../../lib/utils/text_utility';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
@@ -122,7 +126,9 @@ export default {
return this.getNoteableData.create_note_path;
},
issuableTypeTitle() {
- return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE ? 'merge request' : 'issue';
+ return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
+ ? 'merge request'
+ : 'issue';
},
},
watch: {
@@ -359,7 +365,7 @@ Please check your network connection and try again.`;
:disabled="isSubmitting"
name="note[note]"
class="note-textarea js-vue-comment-form js-note-text
-js-gfm-input js-autosize markdown-area js-vue-textarea"
+js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
data-supports-quick-actions="true"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
@@ -374,7 +380,8 @@ js-gfm-input js-autosize markdown-area js-vue-textarea"
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
<button
:disabled="isSubmitButtonDisabled"
- class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
+ class="btn btn-create comment-btn js-comment-button js-comment-submit-button
+ qa-comment-button"
type="submit"
@click.prevent="handleSave()">
{{ __(commentButtonTitle) }}
diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue
deleted file mode 100644
index fc7b52be241..00000000000
--- a/app/assets/javascripts/notes/components/diff_file_header.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-<script>
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
-
-export default {
- components: {
- ClipboardButton,
- Icon,
- },
- props: {
- diffFile: {
- type: Object,
- required: true,
- },
- },
- computed: {
- titleTag() {
- return this.diffFile.discussionPath ? 'a' : 'span';
- },
- },
-};
-</script>
-
-<template>
- <div class="file-header-content">
- <div
- v-if="diffFile.submodule"
- >
- <span>
- <icon name="archive" />
- <strong
- class="file-title-name"
- v-html="diffFile.submoduleLink"
- ></strong>
- <clipboard-button
- :text="diffFile.submoduleLink"
- title="Copy file path to clipboard"
- css-class="btn-default btn-transparent btn-clipboard"
- />
- </span>
- </div>
- <template v-else>
- <component
- ref="titleWrapper"
- :is="titleTag"
- :href="diffFile.discussionPath"
- >
- <span v-html="diffFile.blobIcon"></span>
- <span v-if="diffFile.renamedFile">
- <strong
- :title="diffFile.oldPath"
- class="file-title-name has-tooltip"
- data-container="body"
- >
- {{ diffFile.oldPath }}
- </strong>
- &rarr;
- <strong
- :title="diffFile.newPath"
- class="file-title-name has-tooltip"
- data-container="body"
- >
- {{ diffFile.newPath }}
- </strong>
- </span>
-
- <strong
- v-else
- :title="diffFile.oldPath"
- class="file-title-name has-tooltip"
- data-container="body"
- >
- {{ diffFile.filePath }}
- <span v-if="diffFile.deletedFile">
- deleted
- </span>
- </strong>
- </component>
-
- <clipboard-button
- :text="diffFile.filePath"
- title="Copy file path to clipboard"
- css-class="btn-default btn-transparent btn-clipboard"
- />
-
- <small
- v-if="diffFile.modeChanged"
- ref="fileMode"
- >
- {{ diffFile.aMode }} → {{ diffFile.bMode }}
- </small>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index 27ff7dea909..353aa790743 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -3,13 +3,13 @@ import { mapState, mapActions } from 'vuex';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
-import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
export default {
components: {
DiffFileHeader,
- SkeletonLoadingContainer,
+ SkeletonLoading,
},
props: {
discussion: {
@@ -142,16 +142,15 @@ export default {
class="line_content js-success-lazy-load"
>
<span></span>
- <skeleton-loading-container />
+ <skeleton-loading />
<span></span>
</td>
</tr>
<tr class="notes_holder">
<td
- class="notes_line"
- colspan="2"
- ></td>
- <td class="notes_content">
+ class="notes_content"
+ colspan="3"
+ >
<slot></slot>
</td>
</tr>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index cdbbb342331..e075f94b82b 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -7,29 +7,30 @@ import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg';
-import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'NoteActions',
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
- components: {
- loadingIcon,
- },
props: {
authorId: {
type: Number,
required: true,
},
noteId: {
- type: Number,
+ type: [String, Number],
required: true,
},
noteUrl: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
accessLevel: {
type: String,
@@ -38,7 +39,8 @@ export default {
},
reportAbusePath: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
canEdit: {
type: Boolean,
@@ -87,6 +89,9 @@ export default {
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
+ showDeleteAction() {
+ return this.canDelete && !this.canReportAsAbuse && !this.noteUrl;
+ },
isAuthoredByCurrentUser() {
return this.authorId === this.currentUserId;
},
@@ -152,9 +157,9 @@ export default {
v-else
v-html="resolveDiscussionSvg"></div>
</template>
- <loading-icon
+ <gl-loading-icon
v-else
- :inline="true"
+ inline
/>
</button>
</div>
@@ -171,7 +176,7 @@ export default {
href="#"
title="Add reaction"
>
- <loading-icon :inline="true" />
+ <gl-loading-icon inline/>
<span
class="link-highlight award-control-icon-neutral"
v-html="emojiSmiling">
@@ -204,7 +209,26 @@ export default {
</button>
</div>
<div
- v-if="shouldShowActionsDropdown"
+ v-if="showDeleteAction"
+ class="note-actions-item"
+ >
+ <button
+ v-tooltip
+ type="button"
+ title="Delete comment"
+ class="note-action-button js-note-delete btn btn-transparent"
+ data-container="body"
+ data-placement="bottom"
+ @click="onDelete"
+ >
+ <icon
+ name="remove"
+ class="link-highlight"
+ />
+ </button>
+ </div>
+ <div
+ v-else-if="shouldShowActionsDropdown"
class="dropdown more-actions note-actions-item">
<button
v-tooltip
@@ -225,11 +249,11 @@ export default {
Report as abuse
</a>
</li>
- <li>
+ <li v-if="noteUrl">
<button
:data-clipboard-text="noteUrl"
type="button"
- css-class="btn-default btn-transparent"
+ class="btn-default btn-transparent js-btn-copy-note-link"
>
Copy link
</button>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index e111d3b9ac2..c68860d98ae 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -25,7 +25,7 @@ export default {
required: true,
},
noteId: {
- type: Number,
+ type: String,
required: true,
},
canAwardEmoji: {
@@ -182,9 +182,9 @@ export default {
<div class="note-awards">
<div class="awards js-awards-block">
<button
- v-tooltip
v-for="(awardList, awardName, index) in groupedAwards"
:key="index"
+ v-tooltip
:class="getAwardClassBindings(awardList)"
:title="awardTitle(awardList)"
class="btn award-control"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 6f4a0709825..cf4c35de42c 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -109,7 +109,7 @@ export default {
class="note_edited_ago"
/>
<note-awards-list
- v-if="note.award_emoji.length"
+ v-if="note.award_emoji && note.award_emoji.length"
:note-id="note.id"
:note-author-id="note.author.id"
:awards="note.award_emoji"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index abcd4422d7c..33998394a69 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -20,9 +20,9 @@ export default {
default: '',
},
noteId: {
- type: Number,
+ type: [String, Number],
required: false,
- default: 0,
+ default: '',
},
markdownVersion: {
type: Number,
@@ -67,7 +67,10 @@ export default {
'getUserDataByProp',
]),
noteHash() {
- return `#note_${this.noteId}`;
+ if (this.noteId) {
+ return `#note_${this.noteId}`;
+ }
+ return '#';
},
markdownPreviewPath() {
return this.getNoteableDataByProp('preview_note_path');
@@ -168,8 +171,8 @@ export default {
id="note_note"
ref="textarea"
slot="textarea"
- :data-supports-quick-actions="!isEditing"
v-model="updatedNoteBody"
+ :data-supports-quick-actions="!isEditing"
name="note[note]"
class="note-textarea js-gfm-input js-note-text
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
@@ -185,7 +188,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
<button
:disabled="isDisabled"
type="button"
- class="js-vue-issue-save btn btn-save js-comment-button "
+ class="js-vue-issue-save btn btn-success js-comment-button "
@click="handleUpdate()">
{{ saveButtonTitle }}
</button>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index a621418cf72..7b6e7b72caf 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -9,11 +9,13 @@ export default {
props: {
author: {
type: Object,
- required: true,
+ required: false,
+ default: () => ({}),
},
createdAt: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
actionText: {
type: String,
@@ -21,8 +23,9 @@ export default {
default: '',
},
noteId: {
- type: Number,
- required: true,
+ type: [String, Number],
+ required: false,
+ default: null,
},
includeToggle: {
type: Boolean,
@@ -72,7 +75,10 @@ export default {
{{ __('Toggle discussion') }}
</button>
</div>
- <a :href="author.path">
+ <a
+ v-if="Object.keys(author).length"
+ :href="author.path"
+ >
<span class="note-header-author-name">{{ author.name }}</span>
<span
v-if="author.status_tooltip_html"
@@ -81,6 +87,9 @@ export default {
@{{ author.username }}
</span>
</a>
+ <span v-else>
+ {{ __('A deleted user') }}
+ </span>
<span class="note-headline-light">
<span class="note-headline-meta">
<template v-if="actionText">
@@ -89,18 +98,22 @@ export default {
<span class="system-note-message">
<slot></slot>
</span>
- <span class="system-note-separator">
- &middot;
- </span>
- <a
- :href="noteTimestampLink"
- class="note-timestamp system-note-separator"
- @click="updateTargetNoteHash">
- <time-ago-tooltip
- :time="createdAt"
- tooltip-placement="bottom"
- />
- </a>
+ <template
+ v-if="createdAt"
+ >
+ <span class="system-note-separator">
+ &middot;
+ </span>
+ <a
+ :href="noteTimestampLink"
+ class="note-timestamp system-note-separator"
+ @click="updateTargetNoteHash">
+ <time-ago-tooltip
+ :time="createdAt"
+ tooltip-placement="bottom"
+ />
+ </a>
+ </template>
<i
class="fa fa-spinner fa-spin editing-spinner"
aria-label="Comment is being updated"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 0fe1c16854a..e9218723149 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -137,8 +137,10 @@ export default {
return this.unresolvedDiscussions.length > 1;
},
showJumpToNextDiscussion() {
- return this.hasMultipleUnresolvedDiscussions &&
- !this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder);
+ return (
+ this.hasMultipleUnresolvedDiscussions &&
+ !this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder)
+ );
},
shouldRenderDiffs() {
const { diffDiscussion, diffFile } = this.transformedDiscussion;
@@ -189,6 +191,7 @@ export default {
if (note.placeholderType === SYSTEM_NOTE) {
return placeholderSystemNote;
}
+
return placeholderNote;
}
@@ -199,7 +202,7 @@ export default {
return noteableNote;
},
componentData(note) {
- return note.isPlaceholderNote ? this.discussion.notes[0] : note;
+ return note.isPlaceholderNote ? note.notes[0] : note;
},
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id });
@@ -256,11 +259,16 @@ Please check your network connection and try again.`;
});
},
jumpToNextDiscussion() {
- const nextId =
- this.nextUnresolvedDiscussionId(this.discussion.id, this.discussionsByDiffOrder);
+ const nextId = this.nextUnresolvedDiscussionId(
+ this.discussion.id,
+ this.discussionsByDiffOrder,
+ );
this.jumpToDiscussion(nextId);
},
+ deleteNoteHandler(note) {
+ this.$emit('noteDeleted', this.discussion, note);
+ },
},
};
</script>
@@ -270,6 +278,7 @@ Please check your network connection and try again.`;
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
+ v-if="author"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="author.name"
@@ -340,10 +349,11 @@ Please check your network connection and try again.`;
<div class="discussion-notes">
<ul class="notes">
<component
- v-for="note in discussion.notes"
:is="componentName(note)"
- :note="componentData(note)"
+ v-for="note in discussion.notes"
:key="note.id"
+ :note="componentData(note)"
+ @handleDeleteNote="deleteNoteHandler"
/>
</ul>
<div
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 4ebeb5599f2..f391ed848a4 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -52,7 +52,7 @@ export default {
return this.note.resolvable && !!this.getUserData.id;
},
canReportAsAbuse() {
- return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
+ return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
@@ -81,11 +81,16 @@ export default {
...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']),
editHandler() {
this.isEditing = true;
+ this.$emit('handleEdit');
},
deleteHandler() {
+ const typeOfComment = this.note.isDraft ? 'pending comment' : 'comment';
// eslint-disable-next-line no-alert
- if (window.confirm('Are you sure you want to delete this comment?')) {
+ if (window.confirm(`Are you sure you want to delete this ${typeOfComment}?`)) {
this.isDeleting = true;
+ this.$emit('handleDeleteNote', this.note);
+
+ if (this.note.isDraft) return;
this.deleteNote(this.note)
.then(() => {
@@ -97,7 +102,20 @@ export default {
});
}
},
+ updateSuccess() {
+ this.isEditing = false;
+ this.isRequesting = false;
+ this.oldContent = null;
+ $(this.$refs.noteBody.$el).renderGFM();
+ this.$refs.noteBody.resetAutoSave();
+ this.$emit('updateSuccess');
+ },
formUpdateHandler(noteText, parentElement, callback) {
+ this.$emit('handleUpdateNote', {
+ note: this.note,
+ noteText,
+ callback: () => this.updateSuccess(),
+ });
const data = {
endpoint: this.note.path,
note: {
@@ -112,11 +130,7 @@ export default {
this.updateNote(data)
.then(() => {
- this.isEditing = false;
- this.isRequesting = false;
- this.oldContent = null;
- $(this.$refs.noteBody.$el).renderGFM();
- this.$refs.noteBody.resetAutoSave();
+ this.updateSuccess();
callback();
})
.catch(() => {
@@ -141,6 +155,7 @@ export default {
this.oldContent = null;
}
this.isEditing = false;
+ this.$emit('cancelForm');
},
recoverNoteContent(noteText) {
// we need to do this to prevent noteForm inconsistent content warning
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 9b8713b40fb..618a1581d8f 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -10,8 +10,8 @@ import systemNote from '../../vue_shared/components/notes/system_note.vue';
import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
+import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
export default {
name: 'NotesApp',
@@ -20,7 +20,6 @@ export default {
noteableDiscussion,
systemNote,
commentForm,
- loadingIcon,
placeholderNote,
placeholderSystemNote,
},
@@ -98,6 +97,9 @@ export default {
});
}
},
+ updated() {
+ this.$nextTick(() => highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')));
+ },
methods: {
...mapActions({
fetchDiscussions: 'fetchDiscussions',
@@ -138,6 +140,7 @@ export default {
.then(() => {
this.isLoading = false;
this.setNotesFetchedState(true);
+ eventHub.$emit('fetchedNotesData');
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())
@@ -188,10 +191,10 @@ export default {
class="notes main-notes-list timeline"
>
<component
- v-for="discussion in allDiscussions"
:is="getComponentName(discussion)"
- v-bind="getComponentData(discussion)"
+ v-for="discussion in allDiscussions"
:key="discussion.id"
+ v-bind="getComponentData(discussion)"
/>
</ul>
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 3eefbe11c37..7ab7e5a9abb 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -10,6 +10,7 @@ import service from '../services/notes_service';
import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
+import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
let eTagPoll;
@@ -43,18 +44,17 @@ export const fetchDiscussions = ({ commit }, path) =>
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
});
-export const refetchDiscussionById = ({ commit }, { path, discussionId }) =>
- service
- .fetchDiscussions(path)
- .then(res => res.json())
- .then(discussions => {
- const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId);
- if (selectedDiscussion) commit(types.UPDATE_DISCUSSION, selectedDiscussion);
- });
+export const updateDiscussion = ({ commit, state }, discussion) => {
+ commit(types.UPDATE_DISCUSSION, discussion);
+
+ return utils.findNoteObjectById(state.discussions, discussion.id);
+};
-export const deleteNote = ({ commit }, note) =>
+export const deleteNote = ({ commit, dispatch }, note) =>
service.deleteNote(note.path).then(() => {
commit(types.DELETE_NOTE, note);
+
+ dispatch('updateMergeRequestWidget');
});
export const updateNote = ({ commit }, { endpoint, note }) =>
@@ -75,20 +75,22 @@ export const replyToDiscussion = ({ commit }, { endpoint, data }) =>
return res;
});
-export const createNewNote = ({ commit }, { endpoint, data }) =>
+export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
service
.createNewNote(endpoint, data)
.then(res => res.json())
.then(res => {
if (!res.errors) {
commit(types.ADD_NEW_NOTE, res);
+
+ dispatch('updateMergeRequestWidget');
}
return res;
});
export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
-export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) =>
+export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) =>
service
.toggleResolveNote(endpoint, isResolved)
.then(res => res.json())
@@ -96,6 +98,8 @@ export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion
const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
commit(mutationType, res);
+
+ dispatch('updateMergeRequestWidget');
});
export const closeIssue = ({ commit, dispatch, state }) => {
@@ -146,35 +150,50 @@ export const toggleIssueLocalState = ({ commit }, newState) => {
export const saveNote = ({ commit, dispatch }, noteData) => {
// For MR discussuions we need to post as `note[note]` and issue we use `note.note`.
- const note = noteData.data['note[note]'] || noteData.data.note.note;
+ // For batch comments, we use draft_note
+ const note = noteData.data.draft_note || noteData.data['note[note]'] || noteData.data.note.note;
let placeholderText = note;
const hasQuickActions = utils.hasQuickActions(placeholderText);
const replyId = noteData.data.in_reply_to_discussion_id;
- const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
+ let methodToDispatch;
+ const postData = Object.assign({}, noteData);
+ if (postData.isDraft === true) {
+ methodToDispatch = replyId
+ ? 'batchComments/addDraftToDiscussion'
+ : 'batchComments/createNewDraft';
+ if (!postData.draft_note && noteData.note) {
+ postData.draft_note = postData.note;
+ delete postData.note;
+ }
+ } else {
+ methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
+ }
- commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
$('.notes-form .flash-container').hide(); // hide previous flash notification
+ commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
- if (hasQuickActions) {
- placeholderText = utils.stripQuickActions(placeholderText);
- }
+ if (replyId) {
+ if (hasQuickActions) {
+ placeholderText = utils.stripQuickActions(placeholderText);
+ }
- if (placeholderText.length) {
- commit(types.SHOW_PLACEHOLDER_NOTE, {
- noteBody: placeholderText,
- replyId,
- });
- }
+ if (placeholderText.length) {
+ commit(types.SHOW_PLACEHOLDER_NOTE, {
+ noteBody: placeholderText,
+ replyId,
+ });
+ }
- if (hasQuickActions) {
- commit(types.SHOW_PLACEHOLDER_NOTE, {
- isSystemNote: true,
- noteBody: utils.getQuickActionText(note),
- replyId,
- });
+ if (hasQuickActions) {
+ commit(types.SHOW_PLACEHOLDER_NOTE, {
+ isSystemNote: true,
+ noteBody: utils.getQuickActionText(note),
+ replyId,
+ });
+ }
}
- return dispatch(methodToDispatch, noteData).then(res => {
+ return dispatch(methodToDispatch, postData, { root: true }).then(res => {
const { errors } = res;
const commandsChanges = res.commands_changes;
@@ -211,7 +230,9 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
if (errors && errors.commands_only) {
Flash(errors.commands_only, 'notice', noteData.flashContainer);
}
- commit(types.REMOVE_PLACEHOLDER_NOTES);
+ if (replyId) {
+ commit(types.REMOVE_PLACEHOLDER_NOTES);
+ }
return res;
});
@@ -320,5 +341,9 @@ export const fetchDiscussionDiffLines = ({ commit }, discussion) =>
});
});
+export const updateMergeRequestWidget = () => {
+ mrWidgetEventHub.$emit('mr.discussion.updated');
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 5b3b9f8776f..a829149a17e 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,5 +1,6 @@
import _ from 'underscore';
import * as constants from '../constants';
+import { reduceDiscussionsToLineCodes } from './utils';
import { collapseSystemNotes } from './collapse_utils';
export const discussions = state => collapseSystemNotes(state.discussions);
@@ -28,17 +29,8 @@ export const notesById = state =>
return acc;
}, {});
-export const discussionsByLineCode = state =>
- state.discussions.reduce((acc, note) => {
- if (note.diff_discussion && note.line_code && note.resolvable) {
- // 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 discussionsStructuredByLineCode = state =>
+ reduceDiscussionsToLineCodes(state.discussions);
export const noteableType = state => {
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
@@ -82,6 +74,9 @@ export const allDiscussions = (state, getters) => {
return Object.values(resolved).concat(unresolved);
};
+export const isDiscussionResolved = (state, getters) => discussionId =>
+ getters.resolvedDiscussionsById[discussionId] !== undefined;
+
export const allResolvableDiscussions = (state, getters) =>
getters.allDiscussions.filter(d => !d.individual_note && d.resolvable);
@@ -134,8 +129,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path);
// Get the line numbers, to compare within the same file
- const aLines = [a.position.formatter.new_line, a.position.formatter.old_line];
- const bLines = [b.position.formatter.new_line, b.position.formatter.old_line];
+ const aLines = [a.position.new_line, a.position.old_line];
+ const bLines = [b.position.new_line, b.position.old_line];
return filenameComparison < 0 ||
(filenameComparison === 0 &&
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
index 0f48b8880f4..f105b7d0d11 100644
--- a/app/assets/javascripts/notes/stores/index.js
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -1,16 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import module from './modules';
+import notesModule from './modules';
Vue.use(Vuex);
export default () =>
- new Vuex.Store({
- state: module.state,
- actions,
- getters,
- mutations,
- });
+ new Vuex.Store(notesModule());
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index b4cb9267e0f..61dbb075586 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -2,7 +2,7 @@ import * as actions from '../actions';
import * as getters from '../getters';
import mutations from '../mutations';
-export default {
+export default () => ({
state: {
discussions: [],
targetNoteHash: null,
@@ -24,4 +24,4 @@ export default {
actions,
getters,
mutations,
-};
+});
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index ab6a95e2601..73e55705f39 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -4,7 +4,8 @@ import * as constants from '../constants';
import { isInMRPage } from '../../lib/utils/common_utils';
export default {
- [types.ADD_NEW_NOTE](state, note) {
+ [types.ADD_NEW_NOTE](state, data) {
+ const note = data.discussion ? data.discussion.notes[0] : data;
const { discussion_id, type } = note;
const [exists] = state.discussions.filter(n => n.id === note.discussion_id);
const isDiscussion = type === constants.DISCUSSION_NOTE || type === constants.DIFF_NOTE;
@@ -54,13 +55,12 @@ export default {
[types.EXPAND_DISCUSSION](state, { discussionId }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
-
- discussion.expanded = true;
+ Object.assign(discussion, { expanded: true });
},
[types.COLLAPSE_DISCUSSION](state, { discussionId }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
- discussion.expanded = false;
+ Object.assign(discussion, { expanded: false });
},
[types.REMOVE_PLACEHOLDER_NOTES](state) {
@@ -95,10 +95,18 @@ export default {
[types.SET_USER_DATA](state, data) {
Object.assign(state, { userData: data });
},
+
[types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
const discussions = [];
discussionsData.forEach(discussion => {
+ if (discussion.diff_file) {
+ Object.assign(discussion, {
+ fileHash: discussion.diff_file.file_hash,
+ truncated_diff_lines: discussion.truncated_diff_lines || [],
+ });
+ }
+
// To support legacy notes, should be very rare case.
if (discussion.individual_note && discussion.notes.length > 1) {
discussion.notes.forEach(n => {
@@ -168,8 +176,7 @@ export default {
[types.TOGGLE_DISCUSSION](state, { discussionId }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
-
- discussion.expanded = !discussion.expanded;
+ Object.assign(discussion, { expanded: !discussion.expanded });
},
[types.UPDATE_NOTE](state, note) {
@@ -185,16 +192,12 @@ export default {
[types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData;
- let index = 0;
-
- state.discussions.forEach((n, i) => {
- if (n.id === note.id) {
- index = i;
- }
- });
-
+ const selectedDiscussion = state.discussions.find(disc => disc.id === note.id);
note.expanded = true; // override expand flag to prevent collapse
- state.discussions.splice(index, 1, note);
+ if (note.diff_file) {
+ Object.assign(note, { fileHash: note.diff_file.file_hash });
+ }
+ Object.assign(selectedDiscussion, { ...note });
},
[types.CLOSE_ISSUE](state) {
@@ -215,12 +218,7 @@ export default {
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
- const index = state.discussions.indexOf(discussion);
-
- const discussionWithDiffLines = Object.assign({}, discussion, {
- truncated_diff_lines: diffLines,
- });
- state.discussions.splice(index, 1, discussionWithDiffLines);
+ discussion.truncated_diff_lines = diffLines;
},
};
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index a0e096ebfaf..0e41ff03d67 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -2,13 +2,11 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
-export const findNoteObjectById = (notes, id) =>
- notes.filter(n => n.id === id)[0];
+export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
export const getQuickActionText = note => {
let text = 'Applying command';
- const quickActions =
- AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
+ const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter(command => {
const commandRegex = new RegExp(`/${command.name}`);
@@ -27,7 +25,18 @@ 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();
+export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
new file mode 100644
index 00000000000..c40503603be
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
@@ -0,0 +1,8 @@
+import UsagePingPayload from './../usage_ping_payload';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new UsagePingPayload(
+ document.querySelector('.js-usage-ping-payload-trigger'),
+ document.querySelector('.js-usage-ping-payload'),
+ ).init();
+});
diff --git a/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js
new file mode 100644
index 00000000000..9a1bc46bf4a
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js
@@ -0,0 +1,62 @@
+import axios from '../../../lib/utils/axios_utils';
+import { __ } from '../../../locale';
+import flash from '../../../flash';
+
+export default class UsagePingPayload {
+ constructor(trigger, container) {
+ this.trigger = trigger;
+ this.container = container;
+ this.isVisible = false;
+ this.isInserted = false;
+ }
+
+ init() {
+ this.spinner = this.trigger.querySelector('.js-spinner');
+ this.text = this.trigger.querySelector('.js-text');
+
+ this.trigger.addEventListener('click', event => {
+ event.preventDefault();
+
+ if (this.isVisible) return this.hidePayload();
+
+ return this.requestPayload();
+ });
+ }
+
+ requestPayload() {
+ if (this.isInserted) return this.showPayload();
+
+ this.spinner.classList.add('d-inline');
+
+ return axios
+ .get(this.container.dataset.endpoint, {
+ responseType: 'text',
+ })
+ .then(({ data }) => {
+ this.spinner.classList.remove('d-inline');
+ this.insertPayload(data);
+ })
+ .catch(() => {
+ this.spinner.classList.remove('d-inline');
+ flash(__('Error fetching usage ping data.'));
+ });
+ }
+
+ hidePayload() {
+ this.isVisible = false;
+ this.container.classList.add('d-none');
+ this.text.textContent = __('Preview payload');
+ }
+
+ showPayload() {
+ this.isVisible = true;
+ this.container.classList.remove('d-none');
+ this.text.textContent = __('Hide payload');
+ }
+
+ insertPayload(data) {
+ this.isInserted = true;
+ this.container.innerHTML = data;
+ this.showPayload();
+ }
+}
diff --git a/app/assets/javascripts/pages/admin/runners/index.js b/app/assets/javascripts/pages/admin/runners/index.js
new file mode 100644
index 00000000000..ce8fd18b6a2
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/runners/index.js
@@ -0,0 +1,10 @@
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys';
+import { FILTERED_SEARCH } from '~/pages/constants';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initFilteredSearch({
+ page: FILTERED_SEARCH.ADMIN_RUNNERS,
+ filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys,
+ });
+});
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 d6aa4bb95d2..8d5efcdcd96 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
@@ -155,10 +155,7 @@
/>
</form>
</template>
- <template
- slot="secondary-button"
- slot-scope="props"
- >
+ <template slot="secondary-button">
<button
:disabled="!canSubmit"
type="button"
diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js
index 328b6541636..5e119454ce1 100644
--- a/app/assets/javascripts/pages/constants.js
+++ b/app/assets/javascripts/pages/constants.js
@@ -3,4 +3,5 @@
export const FILTERED_SEARCH = {
MERGE_REQUESTS: 'merge_requests',
ISSUES: 'issues',
+ ADMIN_RUNNERS: 'admin/runners',
};
diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js
index 79987642796..b9277106a71 100644
--- a/app/assets/javascripts/pages/dashboard/groups/index/index.js
+++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js
@@ -1,3 +1,5 @@
import initGroupsList from '~/groups';
-document.addEventListener('DOMContentLoaded', initGroupsList);
+document.addEventListener('DOMContentLoaded', () => {
+ initGroupsList();
+});
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 9aa83ce6269..72f3f70b98f 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -79,10 +79,13 @@ export default class Todos {
.then(({ data }) => {
this.updateRowState(target);
this.updateBadges(data);
- }).catch(() => flash(__('Error updating todo status.')));
+ }).catch(() => {
+ this.updateRowState(target, true);
+ return flash(__('Error updating todo status.'));
+ });
}
- updateRowState(target) {
+ updateRowState(target, isInactive = false) {
const row = target.closest('li');
const restoreBtn = row.querySelector('.js-undo-todo');
const doneBtn = row.querySelector('.js-done-todo');
@@ -91,7 +94,10 @@ export default class Todos {
target.removeAttribute('disabled');
target.classList.remove('disabled');
- if (target === doneBtn) {
+ if (isInactive === true) {
+ restoreBtn.classList.add('hidden');
+ doneBtn.classList.remove('hidden');
+ } else if (target === doneBtn) {
row.classList.add('done-reversible');
restoreBtn.classList.remove('hidden');
} else if (target === restoreBtn) {
diff --git a/app/assets/javascripts/pages/groups/boards/index.js b/app/assets/javascripts/pages/groups/boards/index.js
index 5cfe8723204..79c3be771d0 100644
--- a/app/assets/javascripts/pages/groups/boards/index.js
+++ b/app/assets/javascripts/pages/groups/boards/index.js
@@ -1,5 +1,5 @@
import UsersSelect from '~/users_select';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => {
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 8737f537296..002b2279fcc 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -2,14 +2,13 @@ import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
import initSettingsPanels from '~/settings_panels';
+import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
+import { GROUP_BADGE } from '~/badges/constants';
document.addEventListener('DOMContentLoaded', () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal();
-});
-
-document.addEventListener('DOMContentLoaded', () => {
- // Initialize expandable settings panels
initSettingsPanels();
+ mountBadgeSettings(GROUP_BADGE);
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 914f804fdd3..736c6a62610 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,11 +1,13 @@
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
isGroupDecendent: true,
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
projectSelect();
});
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index 1600faa3611..339ce67438a 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -1,11 +1,15 @@
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
+ IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests();
+
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
isGroupDecendent: true,
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
projectSelect();
});
diff --git a/app/assets/javascripts/pages/groups/show/group_tabs.js b/app/assets/javascripts/pages/groups/show/group_tabs.js
new file mode 100644
index 00000000000..c6fe61d2bd9
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/show/group_tabs.js
@@ -0,0 +1,136 @@
+import $ from 'jquery';
+import { removeParams } from '~/lib/utils/url_utility';
+import createGroupTree from '~/groups';
+import {
+ ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ ACTIVE_TAB_SHARED,
+ ACTIVE_TAB_ARCHIVED,
+ CONTENT_LIST_CLASS,
+ GROUPS_LIST_HOLDER_CLASS,
+ GROUPS_FILTER_FORM_CLASS,
+} from '~/groups/constants';
+import UserTabs from '~/pages/users/user_tabs';
+import GroupFilterableList from '~/groups/groups_filterable_list';
+
+export default class GroupTabs extends UserTabs {
+ constructor({ defaultAction = 'subgroups_and_projects', action, parentEl }) {
+ super({ defaultAction, action, parentEl });
+ }
+
+ bindEvents() {
+ this.$parentEl
+ .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
+ .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
+ }
+
+ tabShown(event) {
+ const $target = $(event.target);
+ const action = $target.data('action') || $target.data('targetSection');
+ const source = $target.attr('href') || $target.data('targetPath');
+
+ document.querySelector(GROUPS_FILTER_FORM_CLASS).action = source;
+
+ this.setTab(action);
+ return this.setCurrentAction(source);
+ }
+
+ setTab(action) {
+ const loadableActions = [
+ ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ ACTIVE_TAB_SHARED,
+ ACTIVE_TAB_ARCHIVED,
+ ];
+ this.enableSearchBar(action);
+ this.action = action;
+
+ if (this.loaded[action]) {
+ return;
+ }
+
+ if (loadableActions.includes(action)) {
+ this.cleanFilterState();
+ this.loadTab(action);
+ }
+ }
+
+ loadTab(action) {
+ const elId = `js-groups-${action}-tree`;
+ const endpoint = this.getEndpoint(action);
+
+ this.toggleLoading(true);
+
+ createGroupTree(elId, endpoint, action);
+ this.loaded[action] = true;
+
+ this.toggleLoading(false);
+ }
+
+ getEndpoint(action) {
+ const { endpointsDefault, endpointsShared } = this.$parentEl.data();
+ let endpoint;
+
+ switch (action) {
+ case ACTIVE_TAB_ARCHIVED:
+ endpoint = `${endpointsDefault}?archived=only`;
+ break;
+ case ACTIVE_TAB_SHARED:
+ endpoint = endpointsShared;
+ break;
+ default:
+ // ACTIVE_TAB_SUBGROUPS_AND_PROJECTS
+ endpoint = endpointsDefault;
+ break;
+ }
+
+ return endpoint;
+ }
+
+ enableSearchBar(action) {
+ const containerEl = document.getElementById(action);
+ const form = document.querySelector(GROUPS_FILTER_FORM_CLASS);
+ const filter = form.querySelector('.js-groups-list-filter');
+ const holder = containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS);
+ const dataEl = containerEl.querySelector(CONTENT_LIST_CLASS);
+ const endpoint = this.getEndpoint(action);
+
+ if (!dataEl) {
+ return;
+ }
+
+ const { dataset } = dataEl;
+ const opts = {
+ form,
+ filter,
+ holder,
+ filterEndpoint: endpoint || dataset.endpoint,
+ pagePath: null,
+ dropdownSel: '.js-group-filter-dropdown-wrap',
+ filterInputField: 'filter',
+ action,
+ };
+
+ if (!this.loaded[action]) {
+ const filterableList = new GroupFilterableList(opts);
+ filterableList.initSearch();
+ }
+ }
+
+ cleanFilterState() {
+ const values = Object.values(this.loaded);
+ const loadedTabs = values.filter(e => e === true);
+
+ if (!loadedTabs.length) {
+ return;
+ }
+
+ const newState = removeParams(['page'], window.location.search);
+
+ window.history.replaceState(
+ {
+ url: newState,
+ },
+ document.title,
+ newState,
+ );
+ }
+}
diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js
index d7b35d2b26b..3a45fd70d02 100644
--- a/app/assets/javascripts/pages/groups/show/index.js
+++ b/app/assets/javascripts/pages/groups/show/index.js
@@ -1,14 +1,22 @@
/* eslint-disable no-new */
+import { getPagePath } from '~/lib/utils/common_utils';
+import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
import NewGroupChild from '~/groups/new_group_child';
import notificationsDropdown from '~/notifications_dropdown';
import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
-import ShortcutsNavigation from '~/shortcuts_navigation';
-import initGroupsList from '~/groups';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
+import GroupTabs from './group_tabs';
document.addEventListener('DOMContentLoaded', () => {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
+ const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED];
+ const paths = window.location.pathname.split('/');
+ const subpath = paths[paths.length - 1];
+ const action = loadableActions.includes(subpath) ? subpath : getPagePath(1);
+
+ new GroupTabs({ parentEl: '.groups-listing', action });
new ShortcutsNavigation();
new NotificationsForm();
notificationsDropdown();
@@ -17,6 +25,4 @@ document.addEventListener('DOMContentLoaded', () => {
if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper);
}
-
- initGroupsList();
});
diff --git a/app/assets/javascripts/pages/ide/index.js b/app/assets/javascripts/pages/ide/index.js
index efadf6967aa..d192df3561e 100644
--- a/app/assets/javascripts/pages/ide/index.js
+++ b/app/assets/javascripts/pages/ide/index.js
@@ -1,9 +1,3 @@
-import { initIde, resetServiceWorkersPublicPath } from '~/ide/index';
+import { startIde } from '~/ide/index';
-document.addEventListener('DOMContentLoaded', () => {
- const ideElement = document.getElementById('ide');
- if (ideElement) {
- resetServiceWorkersPublicPath();
- initIde(ideElement);
- }
-});
+startIde();
diff --git a/app/assets/javascripts/pages/instance_statistics/cohorts/index.js b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js
deleted file mode 100644
index 2d5020dbef4..00000000000
--- a/app/assets/javascripts/pages/instance_statistics/cohorts/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initUsagePing from './usage_ping';
-
-document.addEventListener('DOMContentLoaded', initUsagePing);
diff --git a/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js
deleted file mode 100644
index 914a9661c27..00000000000
--- a/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import axios from '../../../lib/utils/axios_utils';
-import { __ } from '../../../locale';
-import flash from '../../../flash';
-
-export default function UsagePing() {
- const el = document.querySelector('.usage-data');
-
- axios.get(el.dataset.endpoint, {
- responseType: 'text',
- }).then(({ data }) => {
- el.innerHTML = data;
- }).catch(() => flash(__('Error fetching usage ping data.')));
-}
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index aea7b649c20..c7ce4675573 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field');
- const toggleNoEmojiPlaceholder = (isVisible) => {
+ const toggleNoEmojiPlaceholder = isVisible => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible);
};
@@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
})
- .catch(() => createFlash('Failed to load emoji list!'));
+ .catch(() => createFlash('Failed to load emoji list.'));
});
diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js
index 5543ad82428..d39ea3d10bf 100644
--- a/app/assets/javascripts/pages/projects/activity/index.js
+++ b/app/assets/javascripts/pages/projects/activity/index.js
@@ -1,5 +1,5 @@
import Activities from '~/activities';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
document.addEventListener('DOMContentLoaded', () => {
new Activities(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/artifacts/browse/index.js b/app/assets/javascripts/pages/projects/artifacts/browse/index.js
index ea7458fe9b8..26dc90a56d7 100644
--- a/app/assets/javascripts/pages/projects/artifacts/browse/index.js
+++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js
@@ -1,5 +1,5 @@
import BuildArtifacts from '~/build_artifacts';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js
index 8484e5e9848..249900d6cb7 100644
--- a/app/assets/javascripts/pages/projects/artifacts/file/index.js
+++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js
@@ -1,5 +1,5 @@
import BlobViewer from '~/blob/viewer/index';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js
index 5cfe8723204..79c3be771d0 100644
--- a/app/assets/javascripts/pages/projects/boards/index.js
+++ b/app/assets/javascripts/pages/projects/boards/index.js
@@ -1,5 +1,5 @@
import UsersSelect from '~/users_select';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => {
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 2e23cce11ce..f477424811d 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import Diff from '~/diff';
import ZenMode from '~/zen_mode';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown';
diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js
index 3682020579b..ad671ce9351 100644
--- a/app/assets/javascripts/pages/projects/commits/show/index.js
+++ b/app/assets/javascripts/pages/projects/commits/show/index.js
@@ -1,6 +1,6 @@
import CommitsList from '~/commits';
import GpgBadges from '~/gpg_badges';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
document.addEventListener('DOMContentLoaded', () => {
new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 628913483c6..f5b1cf85e68 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,6 +1,8 @@
+import { PROJECT_BADGE } from '~/badges/constants';
import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
+import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import initProjectLoadingSpinner from '../shared/save_project_loader';
import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
@@ -13,4 +15,5 @@ document.addEventListener('DOMContentLoaded', () => {
projectAvatar();
initProjectPermissionsSettings();
initConfirmDangerModal();
+ mountBadgeSettings(PROJECT_BADGE);
});
diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js
index 24630c2aa05..388d7d7bdda 100644
--- a/app/assets/javascripts/pages/projects/find_file/show/index.js
+++ b/app/assets/javascripts/pages/projects/find_file/show/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import ProjectFindFile from '~/project_find_file';
-import ShortcutsFindFile from '~/shortcuts_find_file';
+import ShortcutsFindFile from '~/behaviors/shortcuts/shortcuts_find_file';
document.addEventListener('DOMContentLoaded', () => {
const findElement = document.querySelector('.js-file-finder');
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index cc0e6553e83..b0345b4e50d 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,7 +1,7 @@
-import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+import PersistentUserCallout from '../../persistent_user_callout';
import Project from './project';
-import ShortcutsNavigation from '../../shortcuts_navigation';
+import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
document.addEventListener('DOMContentLoaded', () => {
const { page } = document.body.dataset;
@@ -12,7 +12,9 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
- gcpSignupOffer();
+ const callout = document.querySelector('.gcp-signup-offer');
+ if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
+
initGkeDropdowns();
}
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index 56ab3fcdfcb..bc08ccf3584 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -1,7 +1,7 @@
import LineHighlighter from '~/line_highlighter';
import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
-import ShortcutsNavigation from '~/shortcuts_navigation';
-import ShortcutsBlob from '~/shortcuts_blob';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
+import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
import BlobForkSuggestion from '~/blob/blob_fork_suggestion';
import initBlobBundle from '~/blob_edit/blob_bundle';
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js
index b2b8e5d2300..197bfa8a394 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/pages/projects/issues/form.js
@@ -5,7 +5,7 @@ import GLForm from '~/gl_form';
import IssuableForm from '~/issuable_form';
import LabelsSelect from '~/labels_select';
import MilestoneSelect from '~/milestone_select';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => {
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 70fdb0ef40d..a56c0bb6be8 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -1,15 +1,17 @@
/* eslint-disable no-new */
import IssuableIndex from '~/issuable_index';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
new IssuableIndex(ISSUABLE_INDEX.ISSUE);
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 500fbd27340..ef65196872c 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -1,11 +1,12 @@
import initIssuableSidebar from '~/init_issuable_sidebar';
import Issue from '~/issue';
-import ShortcutsIssuable from '~/shortcuts_issuable';
+import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
-import '~/issue_show/index';
+import initIssueableApp from '~/issue_show';
export default function () {
+ initIssueableApp();
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index a7aa616319f..ec39db12e74 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -1,14 +1,19 @@
import IssuableIndex from '~/issuable_index';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
document.addEventListener('DOMContentLoaded', () => {
+ IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests();
+
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
+
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index 3a3c21f2202..e3971618da5 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import Diff from '~/diff';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
import IssuableForm from '~/issuable_form';
import LabelsSelect from '~/labels_select';
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 26ead75cec4..7bfb83a2204 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -1,6 +1,6 @@
import ZenMode from '~/zen_mode';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import ShortcutsIssuable from '~/shortcuts_issuable';
+import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js
index a0b14fed10f..9f05f63b742 100644
--- a/app/assets/javascripts/pages/projects/network/show/index.js
+++ b/app/assets/javascripts/pages/projects/network/show/index.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import ShortcutsNetwork from '../../../../shortcuts_network';
+import ShortcutsNetwork from '~/behaviors/shortcuts/shortcuts_network';
import Network from '../network';
document.addEventListener('DOMContentLoaded', () => {
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 0d05668b285..ef53d67e7cb 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
@@ -147,8 +147,8 @@
<div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
- :placeholder="__('Define a custom pattern with cron syntax')"
v-model="cronInterval"
+ :placeholder="__('Define a custom pattern with cron syntax')"
:name="inputNameAttribute"
:disabled="!isEditable"
class="form-control inline cron-interval-input"
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index a853624e944..34a13eb3251 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -13,40 +13,59 @@ export default class Project {
constructor() {
const $cloneOptions = $('ul.clone-options-dropdown');
const $projectCloneField = $('#project_clone');
- const $cloneBtnText = $('a.clone-dropdown-btn span');
+ const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
- const selectedCloneOption = $cloneBtnText.text().trim();
+ const selectedCloneOption = $cloneBtnLabel.text().trim();
if (selectedCloneOption.length > 0) {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
- $('a', $cloneOptions).on('click', (e) => {
+ $('a', $cloneOptions).on('click', e => {
+ e.preventDefault();
const $this = $(e.currentTarget);
const url = $this.attr('href');
- const activeText = $this.find('.dropdown-menu-inner-title').text();
+ const cloneType = $this.data('cloneType');
- e.preventDefault();
+ $('.is-active', $cloneOptions).removeClass('is-active');
+ $(`a[data-clone-type="${cloneType}"]`).each(function() {
+ const $el = $(this);
+ const activeText = $el.find('.dropdown-menu-inner-title').text();
+ const $container = $el.closest('.project-clone-holder');
+ const $label = $container.find('.js-clone-dropdown-label');
- $('.is-active', $cloneOptions).not($this).removeClass('is-active');
- $this.toggleClass('is-active');
- $projectCloneField.val(url);
- $cloneBtnText.text(activeText);
+ $el.toggleClass('is-active');
+ $label.text(activeText);
+ });
- return $('.clone').text(url);
+ $projectCloneField.val(url);
+ $('.js-git-empty .js-clone').text(url);
});
// Ref switcher
Project.initRefSwitcher();
$('.project-refs-select').on('change', function() {
- return $(this).parents('form').submit();
+ return $(this)
+ .parents('form')
+ .submit();
});
$('.hide-no-ssh-message').on('click', function(e) {
Cookies.set('hide_no_ssh_message', 'false');
- $(this).parents('.no-ssh-key-message').remove();
+ $(this)
+ .parents('.no-ssh-key-message')
+ .remove();
return e.preventDefault();
});
$('.hide-no-password-message').on('click', function(e) {
Cookies.set('hide_no_password_message', 'false');
- $(this).parents('.no-password-message').remove();
+ $(this)
+ .parents('.no-password-message')
+ .remove();
+ return e.preventDefault();
+ });
+ $('.hide-auto-devops-implicitly-enabled-banner').on('click', function(e) {
+ const projectId = $(this).data('project-id');
+ const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`;
+ Cookies.set(cookieKey, 'false');
+ $(this).parents('.auto-devops-implicitly-enabled-banner').remove();
return e.preventDefault();
});
Project.projectSelectDropdown();
@@ -58,7 +77,7 @@ export default class Project {
}
static changeProject(url) {
- return window.location = url;
+ return (window.location = url);
}
static initRefSwitcher() {
@@ -73,14 +92,15 @@ export default class Project {
selected = $dropdown.data('selected');
return $dropdown.glDropdown({
data(term, callback) {
- axios.get($dropdown.data('refsUrl'), {
- params: {
- ref: $dropdown.data('ref'),
- search: term,
- },
- })
- .then(({ data }) => callback(data))
- .catch(() => flash(__('An error occurred while getting projects')));
+ axios
+ .get($dropdown.data('refsUrl'), {
+ params: {
+ ref: $dropdown.data('ref'),
+ search: term,
+ },
+ })
+ .then(({ data }) => callback(data))
+ .catch(() => flash(__('An error occurred while getting projects')));
},
selectable: true,
filterable: true,
diff --git a/app/assets/javascripts/pages/projects/settings/badges/index/index.js b/app/assets/javascripts/pages/projects/settings/badges/index/index.js
deleted file mode 100644
index 30469550866..00000000000
--- a/app/assets/javascripts/pages/projects/settings/badges/index/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
-import { PROJECT_BADGE } from '~/badges/constants';
-import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
-
-Vue.use(Translate);
-
-document.addEventListener('DOMContentLoaded', () => {
- mountBadgeSettings(PROJECT_BADGE);
-});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index ae88b765abf..a16f7e6b77c 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -52,6 +52,21 @@
required: false,
default: '',
},
+ pagesAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pagesAccessControlEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pagesHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
@@ -64,6 +79,7 @@
buildsAccessLevel: 20,
wikiAccessLevel: 20,
snippetsAccessLevel: 20,
+ pagesAccessLevel: 20,
containerRegistryEnabled: true,
lfsEnabled: true,
requestAccessEnabled: true,
@@ -90,6 +106,13 @@
);
},
+ pagesFeatureAccessLevelOptions() {
+ if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
+ return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
+ }
+ return this.featureAccessLevelOptions;
+ },
+
repositoryEnabled() {
return this.repositoryAccessLevel > 0;
},
@@ -109,6 +132,10 @@
this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
+ if (this.pagesAccessLevel === 20) {
+ // When from Internal->Private narrow access for only members
+ this.pagesAccessLevel = 10;
+ }
this.highlightChanges();
} else if (oldValue === visibilityOptions.PRIVATE) {
// if changing away from private, make enabled features more permissive
@@ -118,6 +145,7 @@
if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
+ if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
this.highlightChanges();
}
},
@@ -240,8 +268,8 @@
help-text="Lightweight issue tracking system for this project"
>
<project-feature-setting
- :options="featureAccessLevelOptions"
v-model="issuesAccessLevel"
+ :options="featureAccessLevelOptions"
name="project[project_feature_attributes][issues_access_level]"
/>
</project-setting-row>
@@ -250,8 +278,8 @@
help-text="View and edit files in this project"
>
<project-feature-setting
- :options="featureAccessLevelOptions"
v-model="repositoryAccessLevel"
+ :options="featureAccessLevelOptions"
name="project[project_feature_attributes][repository_access_level]"
/>
</project-setting-row>
@@ -261,8 +289,8 @@
help-text="Submit changes to be merged upstream"
>
<project-feature-setting
- :options="repoFeatureAccessLevelOptions"
v-model="mergeRequestsAccessLevel"
+ :options="repoFeatureAccessLevelOptions"
:disabled-input="!repositoryEnabled"
name="project[project_feature_attributes][merge_requests_access_level]"
/>
@@ -272,8 +300,8 @@
help-text="Build, test, and deploy your changes"
>
<project-feature-setting
- :options="repoFeatureAccessLevelOptions"
v-model="buildsAccessLevel"
+ :options="repoFeatureAccessLevelOptions"
:disabled-input="!repositoryEnabled"
name="project[project_feature_attributes][builds_access_level]"
/>
@@ -308,8 +336,8 @@
help-text="Pages for project documentation"
>
<project-feature-setting
- :options="featureAccessLevelOptions"
v-model="wikiAccessLevel"
+ :options="featureAccessLevelOptions"
name="project[project_feature_attributes][wiki_access_level]"
/>
</project-setting-row>
@@ -318,11 +346,23 @@
help-text="Share code pastes with others out of Git repository"
>
<project-feature-setting
- :options="featureAccessLevelOptions"
v-model="snippetsAccessLevel"
+ :options="featureAccessLevelOptions"
name="project[project_feature_attributes][snippets_access_level]"
/>
</project-setting-row>
+ <project-setting-row
+ v-if="pagesAvailable && pagesAccessControlEnabled"
+ :help-path="pagesHelpPath"
+ label="Pages"
+ help-text="Static website for the project."
+ >
+ <project-feature-setting
+ v-model="pagesAccessLevel"
+ :options="pagesFeatureAccessLevelOptions"
+ name="project[project_feature_attributes][pages_access_level]"
+ />
+ </project-setting-row>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index b76f2f76449..7302c1ab202 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import initBlob from '~/blob_edit/blob_bundle';
-import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import NotificationsForm from '~/notifications_form';
import UserCallout from '~/user_callout';
import TreeView from '~/tree';
@@ -8,15 +8,18 @@ import BlobViewer from '~/blob/viewer/index';
import Activities from '~/activities';
import { ajaxGet } from '~/lib/utils/common_utils';
import GpgBadges from '~/gpg_badges';
+import initReadMore from '~/read_more';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
document.addEventListener('DOMContentLoaded', () => {
+ initReadMore();
new Star(); // eslint-disable-line no-new
notificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
new NotificationsForm(); // eslint-disable-line no-new
- new UserCallout({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new UserCallout({
setCalloutPerProject: false,
className: 'js-autodevops-banner',
});
diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js
index 33d69d891d8..400aed35e32 100644
--- a/app/assets/javascripts/pages/projects/tree/show/index.js
+++ b/app/assets/javascripts/pages/projects/tree/show/index.js
@@ -4,7 +4,7 @@ import initBlob from '~/blob_edit/blob_bundle';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import GpgBadges from '~/gpg_badges';
import TreeView from '../../../../tree';
-import ShortcutsNavigation from '../../../../shortcuts_navigation';
+import ShortcutsNavigation from '../../../../behaviors/shortcuts/shortcuts_navigation';
import BlobViewer from '../../../../blob/viewer';
import NewCommitForm from '../../../../new_commit_form';
import { ajaxGet } from '../../../../lib/utils/common_utils';
diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
index 0289209ff1e..75cb6374ad5 100644
--- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -1,12 +1,8 @@
<script>
import _ from 'underscore';
-import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
export default {
- components: {
- GlModal,
- },
props: {
deleteWikiUrl: {
type: String,
@@ -25,6 +21,9 @@ export default {
},
},
computed: {
+ modalId() {
+ return 'delete-wiki-modal';
+ },
message() {
return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?');
},
@@ -47,31 +46,41 @@ export default {
</script>
<template>
- <gl-modal
- id="delete-wiki-modal"
- :header-title-text="title"
- :footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')"
- footer-primary-button-variant="danger"
- @submit="onSubmit"
- >
- {{ message }}
- <form
- ref="form"
- :action="deleteWikiUrl"
- method="post"
- class="js-requires-input"
+ <div class="d-inline-block">
+ <button
+ v-gl-modal="modalId"
+ type="button"
+ class="btn btn-danger"
+ >
+ {{ __('Delete') }}
+ </button>
+ <gl-ui-modal
+ :title="title"
+ :ok-title="s__('WikiPageConfirmDelete|Delete page')"
+ :modal-id="modalId"
+ title-tag="h4"
+ ok-variant="danger"
+ @ok="onSubmit"
>
- <input
- ref="method"
- type="hidden"
- name="_method"
- value="delete"
- />
- <input
- :value="csrfToken"
- type="hidden"
- name="authenticity_token"
- />
- </form>
- </gl-modal>
+ {{ message }}
+ <form
+ ref="form"
+ :action="deleteWikiUrl"
+ method="post"
+ class="js-requires-input"
+ >
+ <input
+ ref="method"
+ type="hidden"
+ name="_method"
+ value="delete"
+ />
+ <input
+ :value="csrfToken"
+ type="hidden"
+ name="authenticity_token"
+ />
+ </form>
+ </gl-ui-modal>
+ </div>
</template>
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index 0a0fe3fc137..c2629090f01 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -2,8 +2,8 @@ import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import csrf from '~/lib/utils/csrf';
+import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki';
import Wikis from './wikis';
-import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
import deleteWikiModal from './components/delete_wiki_modal.vue';
@@ -14,15 +14,15 @@ document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form')); // eslint-disable-line no-new
- const deleteWikiButton = document.getElementById('delete-wiki-button');
+ const deleteWikiModalWrapperEl = document.getElementById('delete-wiki-modal-wrapper');
- if (deleteWikiButton) {
+ if (deleteWikiModalWrapperEl) {
Vue.use(Translate);
- const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset;
- const deleteWikiModalEl = document.getElementById('delete-wiki-modal');
- const deleteModal = new Vue({ // eslint-disable-line
- el: deleteWikiModalEl,
+ const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset;
+
+ new Vue({ // eslint-disable-line no-new
+ el: deleteWikiModalWrapperEl,
data: {
deleteWikiUrl: '',
},
diff --git a/app/assets/javascripts/pages/root/index.js b/app/assets/javascripts/pages/root/index.js
new file mode 100644
index 00000000000..09f8185d3b5
--- /dev/null
+++ b/app/assets/javascripts/pages/root/index.js
@@ -0,0 +1,5 @@
+// if the "projects dashboard" is a user's default dashboard, when they visit the
+// instance root index, the dashboard will be served by the root controller instead
+// of a dashboard controller. The root index redirects for all other default dashboards.
+
+import '../dashboard/projects/index';
diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js
index f369c7ef9a6..8859557e62d 100644
--- a/app/assets/javascripts/pages/snippets/form.js
+++ b/app/assets/javascripts/pages/snippets/form.js
@@ -11,6 +11,7 @@ export default () => {
epics: false,
milestones: false,
labels: false,
+ snippets: false,
});
new ZenMode(); // eslint-disable-line no-new
};
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 9892a039941..bf592ba7a3c 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -43,7 +43,15 @@ const initColorKey = () =>
.domain([0, 3]);
export default class ActivityCalendar {
- constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
+ constructor(
+ container,
+ activitiesContainer,
+ timestamps,
+ calendarActivitiesPath,
+ utcOffset = 0,
+ firstDayOfWeek = 0,
+ monthsAgo = 12,
+ ) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
@@ -66,6 +74,8 @@ export default class ActivityCalendar {
];
this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
+ this.activitiesContainer = activitiesContainer;
+ this.container = container;
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
@@ -75,13 +85,13 @@ export default class ActivityCalendar {
const today = getSystemDate(utcOffset);
today.setHours(0, 0, 0, 0, 0);
- const oneYearAgo = new Date(today);
- oneYearAgo.setFullYear(today.getFullYear() - 1);
+ const timeAgo = new Date(today);
+ timeAgo.setMonth(today.getMonth() - monthsAgo);
- const days = getDayDifference(oneYearAgo, today);
+ const days = getDayDifference(timeAgo, today);
for (let i = 0; i <= days; i += 1) {
- const date = new Date(oneYearAgo);
+ const date = new Date(timeAgo);
date.setDate(date.getDate() + i);
const day = date.getDay();
@@ -280,7 +290,7 @@ export default class ActivityCalendar {
this.currentSelectedDate.getDate(),
].join('-');
- $('.user-calendar-activities').html(LOADING_HTML);
+ $(this.activitiesContainer).html(LOADING_HTML);
axios
.get(this.calendarActivitiesPath, {
@@ -289,11 +299,11 @@ export default class ActivityCalendar {
},
responseType: 'text',
})
- .then(({ data }) => $('.user-calendar-activities').html(data))
+ .then(({ data }) => $(this.activitiesContainer).html(data))
.catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else {
this.currentSelectedDate = '';
- $('.user-calendar-activities').html('');
+ $(this.activitiesContainer).html('');
}
}
}
diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js
new file mode 100644
index 00000000000..0009419cd0c
--- /dev/null
+++ b/app/assets/javascripts/pages/users/user_overview_block.js
@@ -0,0 +1,42 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default class UserOverviewBlock {
+ constructor(options = {}) {
+ this.container = options.container;
+ this.url = options.url;
+ this.limit = options.limit || 20;
+ this.loadData();
+ }
+
+ loadData() {
+ const loadingEl = document.querySelector(`${this.container} .loading`);
+
+ loadingEl.classList.remove('hide');
+
+ axios
+ .get(this.url, {
+ params: {
+ limit: this.limit,
+ },
+ })
+ .then(({ data }) => this.render(data))
+ .catch(() => loadingEl.classList.add('hide'));
+ }
+
+ render(data) {
+ const { html, count } = data;
+ const contentList = document.querySelector(`${this.container} .overview-content-list`);
+
+ contentList.innerHTML += html;
+
+ const loadingEl = document.querySelector(`${this.container} .loading`);
+
+ if (count && count > 0) {
+ document.querySelector(`${this.container} .js-view-all`).classList.remove('hide');
+ } else {
+ document.querySelector(`${this.container} .nothing-here-block`).classList.add('text-left', 'p-0');
+ }
+
+ loadingEl.classList.add('hide');
+ }
+}
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index a2ca03536f2..23b0348a99f 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -2,9 +2,10 @@ import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import Activities from '~/activities';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import flash from '~/flash';
import ActivityCalendar from './activity_calendar';
+import UserOverviewBlock from './user_overview_block';
/**
* UserTabs
@@ -61,19 +62,28 @@ import ActivityCalendar from './activity_calendar';
* </div>
*/
-const CALENDAR_TEMPLATE = `
- <div class="clearfix calendar">
- <div class="js-contrib-calendar"></div>
- <div class="calendar-hint">
- Summary of issues, merge requests, push events, and comments
+const CALENDAR_TEMPLATES = {
+ activity: `
+ <div class="clearfix calendar">
+ <div class="js-contrib-calendar"></div>
+ <div class="calendar-hint bottom-right"></div>
</div>
- </div>
-`;
+ `,
+ overview: `
+ <div class="clearfix calendar">
+ <div class="calendar-hint"></div>
+ <div class="js-contrib-calendar prepend-top-20"></div>
+ </div>
+ `,
+};
+
+const CALENDAR_PERIOD_6_MONTHS = 6;
+const CALENDAR_PERIOD_12_MONTHS = 12;
export default class UserTabs {
constructor({ defaultAction, action, parentEl }) {
this.loaded = {};
- this.defaultAction = defaultAction || 'activity';
+ this.defaultAction = defaultAction || 'overview';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this.windowLocation = window.location;
@@ -124,6 +134,8 @@ export default class UserTabs {
}
if (action === 'activity') {
this.loadActivities();
+ } else if (action === 'overview') {
+ this.loadOverviewTab();
}
const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
@@ -154,7 +166,40 @@ export default class UserTabs {
if (this.loaded.activity) {
return;
}
- const $calendarWrap = this.$parentEl.find('.user-calendar');
+
+ this.loadActivityCalendar('activity');
+
+ // eslint-disable-next-line no-new
+ new Activities();
+
+ this.loaded.activity = true;
+ }
+
+ loadOverviewTab() {
+ if (this.loaded.overview) {
+ return;
+ }
+
+ this.loadActivityCalendar('overview');
+
+ UserTabs.renderMostRecentBlocks('#js-overview .activities-block', 5);
+ UserTabs.renderMostRecentBlocks('#js-overview .projects-block', 10);
+
+ this.loaded.overview = true;
+ }
+
+ static renderMostRecentBlocks(container, limit) {
+ // eslint-disable-next-line no-new
+ new UserOverviewBlock({
+ container,
+ url: $(`${container} .overview-content-list`).data('href'),
+ limit,
+ });
+ }
+
+ loadActivityCalendar(action) {
+ const monthsAgo = action === 'overview' ? CALENDAR_PERIOD_6_MONTHS : CALENDAR_PERIOD_12_MONTHS;
+ const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar');
const calendarPath = $calendarWrap.data('calendarPath');
const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
const utcOffset = $calendarWrap.data('utcOffset');
@@ -166,17 +211,22 @@ export default class UserTabs {
axios
.get(calendarPath)
.then(({ data }) => {
- $calendarWrap.html(CALENDAR_TEMPLATE);
- $calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
+ $calendarWrap.html(CALENDAR_TEMPLATES[action]);
+
+ let calendarHint = '';
+
+ if (action === 'activity') {
+ calendarHint = sprintf(__('Summary of issues, merge requests, push events, and comments (Timezone: %{utcFormatted})'), { utcFormatted });
+ } else if (action === 'overview') {
+ calendarHint = __('Issues, merge requests, pushes and comments.');
+ }
+
+ $calendarWrap.find('.calendar-hint').text(calendarHint);
// eslint-disable-next-line no-new
- new ActivityCalendar('.js-contrib-calendar', data, calendarActivitiesPath, utcOffset);
+ new ActivityCalendar('.tab-pane.active .js-contrib-calendar', '.tab-pane.active .user-calendar-activities', data, calendarActivitiesPath, utcOffset, 0, monthsAgo);
})
.catch(() => flash(__('There was an error loading users activity calendar.')));
-
- // eslint-disable-next-line no-new
- new Activities();
- this.loaded.activity = true;
}
toggleLoading(status) {
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 0fdb0a080cf..1522e2227e4 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -42,7 +42,7 @@ export default {
keys: ['feature', 'request'],
},
],
- simpleMetrics: ['redis', 'sidekiq'],
+ simpleMetrics: ['redis'],
data() {
return { currentRequestId: '' };
},
@@ -130,8 +130,8 @@ export default {
</div>
<simple-metric
v-for="metric in $options.simpleMetrics"
- :current-request="currentRequest"
:key="metric"
+ :current-request="currentRequest"
:metric="metric"
/>
<div
diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue
index b654bc66249..760ea8fe1e6 100644
--- a/app/assets/javascripts/performance_bar/components/simple_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue
@@ -1,16 +1,29 @@
<script>
-export default {
- props: {
- currentRequest: {
- type: Object,
- required: true,
+ export default {
+ props: {
+ currentRequest: {
+ type: Object,
+ required: true,
+ },
+ metric: {
+ type: String,
+ required: true,
+ },
},
- metric: {
- type: String,
- required: true,
+ computed: {
+ duration() {
+ return (
+ this.currentRequest.details[this.metric] &&
+ this.currentRequest.details[this.metric].duration
+ );
+ },
+ calls() {
+ return (
+ this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls
+ );
+ },
},
- },
-};
+ };
</script>
<template>
<div
@@ -21,9 +34,9 @@ export default {
v-if="currentRequest.details"
class="bold"
>
- {{ currentRequest.details[metric].duration }}
+ {{ duration }}
/
- {{ currentRequest.details[metric].calls }}
+ {{ calls }}
</span>
{{ metric }}
</div>
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
new file mode 100644
index 00000000000..1e34e74a152
--- /dev/null
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -0,0 +1,34 @@
+import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
+import Flash from './flash';
+
+export default class PersistentUserCallout {
+ constructor(container) {
+ const { dismissEndpoint, featureId } = container.dataset;
+ this.container = container;
+ this.dismissEndpoint = dismissEndpoint;
+ this.featureId = featureId;
+
+ this.init();
+ }
+
+ init() {
+ const closeButton = this.container.querySelector('.js-close');
+ closeButton.addEventListener('click', event => this.dismiss(event));
+ }
+
+ dismiss(event) {
+ event.preventDefault();
+
+ axios
+ .post(this.dismissEndpoint, {
+ feature_name: this.featureId,
+ })
+ .then(() => {
+ this.container.remove();
+ })
+ .catch(() => {
+ Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
+ });
+ }
+}
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 1952dd453f4..9b4ba0c1a9a 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,12 +1,10 @@
<script>
import _ from 'underscore';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import StageColumnComponent from './stage_column_component.vue';
export default {
components: {
StageColumnComponent,
- LoadingIcon,
},
props: {
isLoading: {
@@ -59,9 +57,9 @@ export default {
<div class="build-content middle-block js-pipeline-graph">
<div class="pipeline-visualization pipeline-graph pipeline-tab-content">
<div class="text-center">
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
- size="3"
+ :size="3"
/>
</div>
@@ -70,9 +68,9 @@ export default {
class="stage-column-list">
<stage-column-component
v-for="(stage, index) in graph"
+ :key="stage.name"
:title="capitalizeStageName(stage.name)"
:jobs="stage.groups"
- :key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
@refreshPipelineGraph="refreshPipelineGraph"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 9ac16b7e541..a1504592bbc 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -98,8 +98,8 @@ export default {
<template>
<div class="ci-job-component">
<a
- v-tooltip
v-if="status.has_details"
+ v-tooltip
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
@@ -115,8 +115,8 @@ export default {
</a>
<div
- v-tooltip
v-else
+ v-tooltip
:title="tooltipText"
:class="cssClassJobName"
class="js-job-component-tooltip non-details-job-component"
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index e7b2de52f76..567ea119343 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -62,9 +62,9 @@ export default {
<ul>
<li
v-for="(job, index) in jobs"
+ :id="jobId(job)"
:key="job.id"
:class="buildConnnectorClass(index)"
- :id="jobId(job)"
class="build"
>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 001eaeaa065..1f9187c3d65 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -1,13 +1,11 @@
<script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
name: 'PipelineHeaderSection',
components: {
ciHeader,
- loadingIcon,
},
props: {
pipeline: {
@@ -89,9 +87,9 @@ export default {
item-name="Pipeline"
@actionClicked="postAction"
/>
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
- size="2"
+ :size="2"
class="prepend-top-default append-bottom-default"
/>
</div>
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
index 9501afb7493..efb80d3a3c0 100644
--- a/app/assets/javascripts/pipelines/components/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/nav_controls.vue
@@ -43,7 +43,7 @@ export default {
<a
v-if="newPipelinePath"
:href="newPipelinePath"
- class="btn btn-create js-run-pipeline"
+ class="btn btn-success js-run-pipeline"
>
{{ s__('Pipelines|Run Pipeline') }}
</a>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 75db1e9ae7c..40df07650c9 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -67,29 +67,29 @@ export default {
</span>
<div class="label-container">
<span
- v-tooltip
v-if="pipeline.flags.latest"
+ v-tooltip
class="js-pipeline-url-latest badge badge-success"
title="Latest pipeline for this branch">
latest
</span>
<span
- v-tooltip
v-if="pipeline.flags.yaml_errors"
+ v-tooltip
:title="pipeline.yaml_errors"
class="js-pipeline-url-yaml badge badge-danger">
yaml invalid
</span>
<span
- v-tooltip
v-if="pipeline.flags.failure_reason"
+ v-tooltip
:title="pipeline.failure_reason"
class="js-pipeline-url-failure badge badge-danger">
error
</span>
<a
- v-popover="popoverOptions"
v-if="pipeline.flags.auto_devops"
+ v-popover="popoverOptions"
tabindex="0"
class="js-pipeline-url-autodevops badge badge-info autodevops-badge"
role="button">
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index c9d2dc3a3c5..ea526cf1309 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -319,10 +319,10 @@ export default {
<div class="content-list pipelines">
- <loading-icon
+ <gl-loading-icon
v-if="stateToRender === $options.stateMap.loading"
:label="s__('Pipelines|Loading Pipelines')"
- size="3"
+ :size="3"
class="prepend-top-20"
/>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 1c8d7303c52..16e69759091 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,6 +1,7 @@
<script>
+import { s__, sprintf } from '~/locale';
+import { formatTime } from '~/lib/utils/datetime_utility';
import eventHub from '../event_hub';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -9,7 +10,6 @@ export default {
tooltip,
},
components: {
- loadingIcon,
icon,
},
props: {
@@ -24,10 +24,24 @@ export default {
};
},
methods: {
- onClickAction(endpoint) {
+ onClickAction(action) {
+ if (action.scheduled_at) {
+ const confirmationMessage = sprintf(
+ s__(
+ "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes.",
+ ),
+ { jobName: action.name },
+ );
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/52156
+ // eslint-disable-next-line no-alert
+ if (!window.confirm(confirmationMessage)) {
+ return;
+ }
+ }
+
this.isLoading = true;
- eventHub.$emit('postAction', endpoint);
+ eventHub.$emit('postAction', action.path);
},
isActionDisabled(action) {
@@ -37,6 +51,11 @@ export default {
return !action.playable;
},
+
+ remainingTime(action) {
+ const remainingMilliseconds = new Date(action.scheduled_at).getTime() - Date.now();
+ return formatTime(Math.max(0, remainingMilliseconds));
+ },
},
};
</script>
@@ -60,22 +79,29 @@ export default {
class="fa fa-caret-down"
aria-hidden="true">
</i>
- <loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" />
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li
- v-for="(action, i) in actions"
- :key="i"
+ v-for="action in actions"
+ :key="action.path"
>
<button
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
type="button"
class="js-pipeline-action-link no-btn btn"
- @click="onClickAction(action.path)"
+ @click="onClickAction(action)"
>
{{ action.name }}
+ <span
+ v-if="action.scheduled_at"
+ class="pull-right"
+ >
+ <icon name="clock" />
+ {{ remainingTime(action) }}
+ </span>
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 29b347824de..09ee190b8ca 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -59,6 +59,16 @@ export default {
};
},
computed: {
+ actions() {
+ if (!this.pipeline || !this.pipeline.details) {
+ return [];
+ }
+ const { details } = this.pipeline;
+ return [
+ ...(details.manual_actions || []),
+ ...(details.scheduled_actions || []),
+ ];
+ },
/**
* If provided, returns the commit tag.
* Needed to render the commit component column.
@@ -132,10 +142,8 @@ export default {
if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') {
- // eslint-disable-next-line no-param-reassign
accumulator.ref_url = this.pipeline.ref[prop];
} else {
- // eslint-disable-next-line no-param-reassign
accumulator[prop] = this.pipeline.ref[prop];
}
return accumulator;
@@ -323,8 +331,8 @@ export default {
>
<div class="btn-group table-action-buttons">
<pipelines-actions-component
- v-if="pipeline.details.manual_actions.length"
- :actions="pipeline.details.manual_actions"
+ v-if="actions.length > 0"
+ :actions="actions"
/>
<pipelines-artifacts-component
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index c7df69c69ed..47c15b1a9c4 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -18,14 +18,12 @@ import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils';
import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue';
-import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import JobComponent from './graph/job_component.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { PIPELINES_TABLE } from '../constants';
export default {
components: {
- LoadingIcon,
Icon,
JobComponent,
},
@@ -157,9 +155,9 @@ export default {
<template>
<div class="dropdown">
<button
- v-tooltip
id="stageDropdown"
ref="dropdown"
+ v-tooltip
:class="triggerButtonClass"
:title="stage.title"
class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
@@ -191,7 +189,7 @@ export default {
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown"
>
- <loading-icon v-if="isLoading"/>
+ <gl-loading-icon v-if="isLoading"/>
<ul
v-else
class="js-builds-dropdown-list scrollable-menu"
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 2cb558b0dec..8929b397f6c 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -4,7 +4,6 @@ import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import EmptyState from '../components/empty_state.vue';
import SvgBlankState from '../components/blank_state.vue';
-import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub';
import { CANCEL_REQUEST } from '../constants';
@@ -14,7 +13,6 @@ export default {
PipelinesTableComponent,
SvgBlankState,
EmptyState,
- LoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
index c15d8ba49e1..d5266544307 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
@@ -1,5 +1,4 @@
import _ from 'underscore';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
@@ -9,7 +8,6 @@ import store from '../store';
export default {
store,
components: {
- LoadingIcon,
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
index d4497924ad8..2c02f436b69 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
@@ -126,7 +126,7 @@ export default {
</ul>
</div>
<div class="dropdown-loading">
- <loading-icon />
+ <gl-loading-icon />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
index 08d0a122579..fc17e2fab49 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
@@ -187,7 +187,7 @@ export default {
</ul>
</div>
<div class="dropdown-loading">
- <loading-icon />
+ <gl-loading-icon />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
index b5476684c6a..ca7c79f75f0 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
@@ -100,7 +100,7 @@ export default {
</ul>
</div>
<div class="dropdown-loading">
- <loading-icon />
+ <gl-loading-icon />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js
index 4e20fce1460..fbef3a0b059 100644
--- a/app/assets/javascripts/projects/project_import_gitlab_project.js
+++ b/app/assets/javascripts/projects/project_import_gitlab_project.js
@@ -1,9 +1,19 @@
import $ from 'jquery';
import { getParameterValues } from '../lib/utils/url_utility';
+import projectNew from './project_new';
export default () => {
- const path = getParameterValues('path')[0];
+ const pathParam = getParameterValues('path')[0];
+ const nameParam = getParameterValues('name')[0];
+ const $projectPath = $('.js-path-name');
+ const $projectName = $('.js-project-name');
- // get the path url and append it in the inputS
- $('.js-path-name').val(path);
+ // get the path url and append it in the input
+ $projectPath.val(pathParam);
+
+ // get the project name from the URL and set it as input value
+ $projectName.val(nameParam);
+
+ // generate slug when project name changes
+ $projectName.keyup(() => projectNew.onProjectNameChange($projectName, $projectPath));
};
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 04badad0f34..ebe18b47e4e 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
+import { slugifyWithHyphens } from '../lib/utils/text_utility';
let hasUserDefinedProjectPath = false;
@@ -29,18 +30,23 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => {
}
};
+const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
+ const slug = slugifyWithHyphens($projectNameInput.val());
+ $projectPathInput.val(slug);
+};
+
const bindEvents = () => {
const $newProjectForm = $('#new_project');
const $projectImportUrl = $('#project_import_url');
- const $projectPath = $('#project_path');
+ const $projectPath = $('.tab-pane.active #project_path');
const $useTemplateBtn = $('.template-button > input');
const $projectFieldsForm = $('.project-fields-form');
const $selectedTemplateText = $('.selected-template');
const $changeTemplateBtn = $('.change-template');
const $selectedIcon = $('.selected-icon');
- const $templateProjectNameInput = $('#template-project-name #project_path');
const $pushNewProjectTipTrigger = $('.push-new-project-tip');
const $projectTemplateButtons = $('.project-templates-buttons');
+ const $projectName = $('.tab-pane.active #project_name');
if ($newProjectForm.length !== 1) {
return;
@@ -57,7 +63,8 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').on('click', () => {
const importHref = $('a.btn_import_gitlab_project').attr('href');
- $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);
+ $('.btn_import_gitlab_project')
+ .attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&name=${$projectName.val()}&path=${$projectPath.val()}`);
});
if ($pushNewProjectTipTrigger) {
@@ -111,7 +118,15 @@ const bindEvents = () => {
const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text);
$(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon);
- $templateProjectNameInput.focus();
+
+ const $activeTabProjectName = $('.tab-pane.active #project_name');
+ const $activeTabProjectPath = $('.tab-pane.active #project_path');
+ $activeTabProjectName.focus();
+ $activeTabProjectName
+ .keyup(() => {
+ onProjectNameChange($activeTabProjectName, $activeTabProjectPath);
+ hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0;
+ });
}
$useTemplateBtn.on('change', chooseTemplate);
@@ -131,9 +146,15 @@ const bindEvents = () => {
});
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
+
+ $projectName.on('keyup change', () => {
+ onProjectNameChange($projectName, $projectPath);
+ hasUserDefinedProjectPath = $projectPath.val().trim().length > 0;
+ });
};
export default {
bindEvents,
deriveProjectPathFromUrl,
+ onProjectNameChange,
};
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index 1c1e17563a1..120b4fc2f2b 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -1,7 +1,6 @@
<script>
import Visibility from 'visibilityjs';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
-import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import Poll from '~/lib/utils/poll';
import Flash from '~/flash';
import { s__, sprintf } from '~/locale';
@@ -14,7 +13,6 @@ export default {
},
components: {
ciIcon,
- loadingIcon,
},
props: {
endpoint: {
@@ -100,10 +98,10 @@ export default {
</script>
<template>
<div class="ci-status-link">
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
+ :size="3"
label="Loading pipeline status"
- size="3"
/>
<a
v-else
diff --git a/app/assets/javascripts/read_more.js b/app/assets/javascripts/read_more.js
new file mode 100644
index 00000000000..d2d1ac8c76a
--- /dev/null
+++ b/app/assets/javascripts/read_more.js
@@ -0,0 +1,41 @@
+/**
+ * ReadMore
+ *
+ * Adds "read more" functionality to elements.
+ *
+ * Specifically, it looks for a trigger, by default ".js-read-more-trigger", and adds the class
+ * "is-expanded" to the previous element in order to provide a click to expand functionality.
+ *
+ * This is useful for long text elements that you would like to truncate, especially for mobile.
+ *
+ * Example Markup
+ * <div class="read-more-container">
+ * <p>Some text that should be long enough to have to truncate within a specified container.</p>
+ * <p>This text will not appear in the container, as only the first line can be truncated.</p>
+ * <p>This should also not appear, if everything is working correctly!</p>
+ * </div>
+ * <button class="js-read-more-trigger">Read more</button>
+ *
+ */
+export default function initReadMore(triggerSelector = '.js-read-more-trigger') {
+ const triggerEls = document.querySelectorAll(triggerSelector);
+
+ if (!triggerEls) return;
+
+ triggerEls.forEach(triggerEl => {
+ const targetEl = triggerEl.previousElementSibling;
+
+ if (!targetEl) {
+ return;
+ }
+
+ triggerEl.addEventListener(
+ 'click',
+ e => {
+ targetEl.classList.add('is-expanded');
+ e.target.remove();
+ },
+ { once: true },
+ );
+ });
+}
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index 31f88675912..7e2287ac4db 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -1,7 +1,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import Flash from '../../flash';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import store from '../stores';
import collapsibleContainer from './collapsible_container.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
@@ -10,7 +9,6 @@
name: 'RegistryListApp',
components: {
collapsibleContainer,
- loadingIcon,
},
props: {
endpoint: {
@@ -42,9 +40,9 @@
</script>
<template>
<div>
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
- size="3"
+ :size="3"
/>
<collapsible-container
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 4116c4a0489..d9bf41924d1 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -2,16 +2,15 @@
import { mapActions } from 'vuex';
import Flash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import tableRegistry from './table_registry.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
+ import { __ } from '../../locale';
export default {
name: 'CollapsibeContainerRegisty',
components: {
clipboardButton,
- loadingIcon,
tableRegistry,
},
directives: {
@@ -46,7 +45,10 @@
handleDeleteRepository() {
this.deleteRepo(this.repo)
- .then(() => this.fetchRepos())
+ .then(() => {
+ Flash(__('This container registry has been scheduled for deletion.'), 'notice');
+ this.fetchRepos();
+ })
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
},
@@ -86,8 +88,8 @@
<div class="controls d-none d-sm-block float-right">
<button
- v-tooltip
v-if="repo.canDelete"
+ v-tooltip
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
type="button"
@@ -103,10 +105,10 @@
</div>
</div>
- <loading-icon
+ <gl-loading-icon
v-if="repo.isLoading"
+ :size="2"
class="append-bottom-20"
- size="2"
/>
<div
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index 9f4973c3490..fafb35bd69a 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -118,8 +118,8 @@
<td class="content">
<button
- v-tooltip
v-if="item.canDelete"
+ v-tooltip
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
type="button"
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
index 7b37f4e9a97..fb8c6402d02 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -92,16 +92,16 @@
v-for="(report, i) in reports"
>
<summary-row
+ :key="`summary-row-${i}`"
:summary="reportText(report)"
:status-icon="getReportIcon(report)"
- :key="`summary-row-${i}`"
/>
<issues-list
v-if="shouldRenderIssuesList(report)"
+ :key="`issues-list-${i}`"
:unresolved-issues="report.existing_failures"
:new-issues="report.new_failures"
:resolved-issues="report.resolved_failures"
- :key="`issues-list-${i}`"
:component="$options.componentNames.TestIssueBody"
class="report-block-group-list"
/>
diff --git a/app/assets/javascripts/reports/components/report_issues.vue b/app/assets/javascripts/reports/components/report_issues.vue
index c553a374f66..a2a03945ae3 100644
--- a/app/assets/javascripts/reports/components/report_issues.vue
+++ b/app/assets/javascripts/reports/components/report_issues.vue
@@ -37,8 +37,8 @@ export default {
<ul class="report-block-list">
<li
v-for="(issue, index) in issues"
- :class="{ 'is-dismissed': issue.isDismissed }"
:key="index"
+ :class="{ 'is-dismissed': issue.isDismissed }"
class="report-block-list-issue"
>
<issue-status-icon
@@ -47,8 +47,8 @@ export default {
/>
<component
- v-if="component"
:is="component"
+ v-if="component"
:issue="issue"
:status="issue.status || status"
:is-new="isNew"
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index dc609d6f90e..d196f497362 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -139,7 +139,7 @@ export default {
<section class="media-section">
<div class="media">
<status-icon :status="statusIconName" />
- <div class="media-body space-children d-flex flex-align-self-center">
+ <div class="media-body d-flex flex-align-self-center">
<span class="js-code-text code-text">
{{ headerText }}
diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue
index 4456d84c968..51188981bed 100644
--- a/app/assets/javascripts/reports/components/summary_row.vue
+++ b/app/assets/javascripts/reports/components/summary_row.vue
@@ -1,6 +1,5 @@
<script>
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
/**
@@ -15,7 +14,6 @@ export default {
name: 'ReportSummaryRow',
components: {
CiIcon,
- LoadingIcon,
Popover,
},
props: {
@@ -46,7 +44,7 @@ export default {
<template>
<div class="report-block-list-issue report-block-list-issue-parent">
<div class="report-block-list-icon append-right-10 prepend-left-5">
- <loading-icon
+ <gl-loading-icon
v-if="statusIcon === 'loading'"
css-class="report-block-list-loading-icon"
/>
diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js
index 1983a8c9e56..b88bff97075 100644
--- a/app/assets/javascripts/reports/store/mutations.js
+++ b/app/assets/javascripts/reports/store/mutations.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index aec09b8bc0a..50dd3c12382 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -68,7 +68,7 @@ function setSearchOptions() {
}
}
-export default class SearchAutocomplete {
+export class SearchAutocomplete {
constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
setSearchOptions();
this.bindEventContext();
@@ -499,3 +499,7 @@ export default class SearchAutocomplete {
this.dropdownMenu.toggleClass('fade-out', !this.isScrolledUp());
}
}
+
+export default function initSearchAutocomplete(opts) {
+ return new SearchAutocomplete(opts);
+}
diff --git a/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js
new file mode 100644
index 00000000000..14a89ef9293
--- /dev/null
+++ b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js
@@ -0,0 +1,21 @@
+import { AwardsHandler } from '~/awards_handler';
+
+class EmojiMenuInModal extends AwardsHandler {
+ constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback, targetContainerEl) {
+ super(emoji);
+
+ this.selectEmojiCallback = selectEmojiCallback;
+ this.toggleButtonSelector = toggleButtonSelector;
+ this.menuClass = menuClass;
+ this.targetContainerEl = targetContainerEl;
+
+ this.bindEvents();
+ }
+
+ postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
+ this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
+ callback();
+ }
+}
+
+export default EmojiMenuInModal;
diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/set_status_modal/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
new file mode 100644
index 00000000000..48e5ede80f2
--- /dev/null
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
@@ -0,0 +1,33 @@
+<script>
+import { s__ } from '~/locale';
+import eventHub from './event_hub';
+
+export default {
+ props: {
+ hasStatus: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ buttonText() {
+ return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status');
+ },
+ },
+ methods: {
+ openModal() {
+ eventHub.$emit('openModal');
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ class="btn menu-item"
+ @click="openModal"
+ >
+ {{ buttonText }}
+ </button>
+</template>
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
new file mode 100644
index 00000000000..43f0b6651b9
--- /dev/null
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -0,0 +1,241 @@
+<script>
+import $ from 'jquery';
+import createFlash from '~/flash';
+import Icon from '~/vue_shared/components/icon.vue';
+import GfmAutoComplete from '~/gfm_auto_complete';
+import { __, s__ } from '~/locale';
+import Api from '~/api';
+import eventHub from './event_hub';
+import EmojiMenuInModal from './emoji_menu_in_modal';
+
+const emojiMenuClass = 'js-modal-status-emoji-menu';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ currentEmoji: {
+ type: String,
+ required: true,
+ },
+ currentMessage: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ defaultEmojiTag: '',
+ emoji: this.currentEmoji,
+ emojiMenu: null,
+ emojiTag: '',
+ isEmojiMenuVisible: false,
+ message: this.currentMessage,
+ modalId: 'set-user-status-modal',
+ noEmoji: true,
+ };
+ },
+ computed: {
+ isDirty() {
+ return this.message.length || this.emoji.length;
+ },
+ },
+ mounted() {
+ eventHub.$on('openModal', this.openModal);
+ },
+ beforeDestroy() {
+ this.emojiMenu.destroy();
+ },
+ methods: {
+ openModal() {
+ this.$root.$emit('bv::show::modal', this.modalId);
+ },
+ closeModal() {
+ this.$root.$emit('bv::hide::modal', this.modalId);
+ },
+ setupEmojiListAndAutocomplete() {
+ const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu';
+ const emojiAutocomplete = new GfmAutoComplete();
+ emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true });
+
+ import(/* webpackChunkName: 'emoji' */ '~/emoji')
+ .then(Emoji => {
+ if (this.emoji) {
+ this.emojiTag = Emoji.glEmojiTag(this.emoji);
+ }
+ this.noEmoji = this.emoji === '';
+ this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon');
+
+ this.emojiMenu = new EmojiMenuInModal(
+ Emoji,
+ toggleEmojiMenuButtonSelector,
+ emojiMenuClass,
+ this.setEmoji,
+ this.$refs.userStatusForm,
+ );
+ })
+ .catch(() => createFlash(__('Failed to load emoji list.')));
+ },
+ showEmojiMenu() {
+ this.isEmojiMenuVisible = true;
+ this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton));
+ },
+ hideEmojiMenu() {
+ if (!this.isEmojiMenuVisible) {
+ return;
+ }
+
+ this.isEmojiMenuVisible = false;
+ this.emojiMenu.hideMenuElement($(`.${emojiMenuClass}`));
+ },
+ setDefaultEmoji() {
+ const { emojiTag } = this;
+ const hasStatusMessage = this.message;
+ if (hasStatusMessage && emojiTag) {
+ return;
+ }
+
+ if (hasStatusMessage) {
+ this.noEmoji = false;
+ this.emojiTag = this.defaultEmojiTag;
+ } else if (emojiTag === this.defaultEmojiTag) {
+ this.noEmoji = true;
+ this.clearEmoji();
+ }
+ },
+ setEmoji(emoji, emojiTag) {
+ this.emoji = emoji;
+ this.noEmoji = false;
+ this.clearEmoji();
+ this.emojiTag = emojiTag;
+ },
+ clearEmoji() {
+ if (this.emojiTag) {
+ this.emojiTag = '';
+ }
+ },
+ clearStatusInputs() {
+ this.emoji = '';
+ this.message = '';
+ this.noEmoji = true;
+ this.clearEmoji();
+ this.hideEmojiMenu();
+ },
+ removeStatus() {
+ this.clearStatusInputs();
+ this.setStatus();
+ },
+ setStatus() {
+ const { emoji, message } = this;
+
+ Api.postUserStatus({
+ emoji,
+ message,
+ })
+ .then(this.onUpdateSuccess)
+ .catch(this.onUpdateFail);
+ },
+ onUpdateSuccess() {
+ this.closeModal();
+ window.location.reload();
+ },
+ onUpdateFail() {
+ createFlash(
+ s__("SetStatusModal|Sorry, we weren't able to set your status. Please try again later."),
+ );
+
+ this.closeModal();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-ui-modal
+ :title="s__('SetStatusModal|Set a status')"
+ :modal-id="modalId"
+ :ok-title="s__('SetStatusModal|Set status')"
+ :cancel-title="s__('SetStatusModal|Remove status')"
+ ok-variant="success"
+ class="set-user-status-modal"
+ @shown="setupEmojiListAndAutocomplete"
+ @hide="hideEmojiMenu"
+ @ok="setStatus"
+ @cancel="removeStatus"
+ >
+ <div>
+ <input
+ v-model="emoji"
+ class="js-status-emoji-field"
+ type="hidden"
+ name="user[status][emoji]"
+ />
+ <div
+ ref="userStatusForm"
+ class="form-group position-relative m-0"
+ >
+ <div class="input-group">
+ <span class="input-group-btn">
+ <button
+ ref="toggleEmojiMenuButton"
+ v-gl-tooltip.bottom
+ :title="s__('SetStatusModal|Add status emoji')"
+ :aria-label="s__('SetStatusModal|Add status emoji')"
+ name="button"
+ type="button"
+ class="js-toggle-emoji-menu emoji-menu-toggle-button btn"
+ @click="showEmojiMenu"
+ >
+ <span v-html="emojiTag"></span>
+ <span
+ v-show="noEmoji"
+ class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
+ >
+ <icon
+ name="emoji_slightly_smiling_face"
+ css-classes="award-control-icon-neutral"
+ />
+ <icon
+ name="emoji_smiley"
+ css-classes="award-control-icon-positive"
+ />
+ <icon
+ name="emoji_smile"
+ css-classes="award-control-icon-super-positive"
+ />
+ </span>
+ </button>
+ </span>
+ <input
+ ref="statusMessageField"
+ v-model="message"
+ :placeholder="s__('SetStatusModal|What\'s your status?')"
+ type="text"
+ class="form-control form-control input-lg js-status-message-field"
+ name="user[status][message]"
+ @keyup="setDefaultEmoji"
+ @keyup.enter.prevent
+ @click="hideEmojiMenu"
+ />
+ <span
+ v-show="isDirty"
+ class="input-group-btn"
+ >
+ <button
+ v-gl-tooltip.bottom
+ :title="s__('SetStatusModal|Clear status')"
+ :aria-label="s__('SetStatusModal|Clear status')"
+ name="button"
+ type="button"
+ class="js-clear-user-status-button clear-user-status btn"
+ @click="clearStatusInputs()"
+ >
+ <icon name="close" />
+ </button>
+ </span>
+ </div>
+ </div>
+ </div>
+ </gl-ui-modal>
+</template>
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
index 8681a1776c6..0ff84dc4667 100644
--- a/app/assets/javascripts/shared/milestones/form.js
+++ b/app/assets/javascripts/shared/milestones/form.js
@@ -15,5 +15,6 @@ export default (initGFM = true) => {
epics: initGFM,
milestones: initGFM,
labels: initGFM,
+ snippets: initGFM,
});
};
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 56d57f6aac8..286a16f7bbf 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,7 +1,6 @@
<script>
import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
- import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
@@ -9,7 +8,6 @@
tooltip,
},
components: {
- loadingIcon,
userAvatarImage,
},
props: {
@@ -93,7 +91,7 @@
aria-hidden="true"
>
</i>
- <loading-icon
+ <gl-loading-icon
v-if="loading"
class="js-participants-collapsed-loading-icon"
/>
@@ -105,7 +103,7 @@
</span>
</div>
<div class="title hide-collapsed">
- <loading-icon
+ <gl-loading-icon
v-if="loading"
:inline="true"
class="js-participants-expanded-loading-icon"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 2e1d6e9643a..8660b0546cf 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -51,10 +51,10 @@ export default {
<template>
<div class="block">
<issuable-time-tracker
- :time_estimate="store.timeEstimate"
- :time_spent="store.totalTimeSpent"
- :human_time_estimate="store.humanTimeEstimate"
- :human_time_spent="store.humanTotalTimeSpent"
+ :time-estimate="store.timeEstimate"
+ :time-spent="store.totalTimeSpent"
+ :human-time-estimate="store.humanTimeEstimate"
+ :human-time-spent="store.humanTotalTimeSpent"
:root-path="store.rootPath"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index ca3b9338c29..ef76dc13ce9 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -19,20 +19,20 @@ export default {
TimeTrackingHelpState,
},
props: {
- time_estimate: {
+ timeEstimate: {
type: Number,
required: true,
},
- time_spent: {
+ timeSpent: {
type: Number,
required: true,
},
- human_time_estimate: {
+ humanTimeEstimate: {
type: String,
required: false,
default: '',
},
- human_time_spent: {
+ humanTimeSpent: {
type: String,
required: false,
default: '',
@@ -48,18 +48,6 @@ export default {
};
},
computed: {
- timeSpent() {
- return this.time_spent;
- },
- timeEstimate() {
- return this.time_estimate;
- },
- timeEstimateHumanReadable() {
- return this.human_time_estimate;
- },
- timeSpentHumanReadable() {
- return this.human_time_spent;
- },
hasTimeSpent() {
return !!this.timeSpent;
},
@@ -90,10 +78,12 @@ export default {
this.showHelp = show;
},
update(data) {
- this.time_estimate = data.time_estimate;
- this.time_spent = data.time_spent;
- this.human_time_estimate = data.human_time_estimate;
- this.human_time_spent = data.human_time_spent;
+ const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = data;
+
+ this.timeEstimate = timeEstimate;
+ this.timeSpent = timeSpent;
+ this.humanTimeEstimate = humanTimeEstimate;
+ this.humanTimeSpent = humanTimeSpent;
},
},
};
@@ -110,8 +100,8 @@ export default {
:show-help-state="showHelpState"
:show-spent-only-state="showSpentOnlyState"
:show-estimate-only-state="showEstimateOnlyState"
- :time-spent-human-readable="timeSpentHumanReadable"
- :time-estimate-human-readable="timeEstimateHumanReadable"
+ :time-spent-human-readable="humanTimeSpent"
+ :time-estimate-human-readable="humanTimeEstimate"
/>
<div class="title hide-collapsed">
{{ __('Time tracking') }}
@@ -141,11 +131,11 @@ export default {
<div class="time-tracking-content hide-collapsed">
<time-tracking-estimate-only-pane
v-if="showEstimateOnlyState"
- :time-estimate-human-readable="timeEstimateHumanReadable"
+ :time-estimate-human-readable="humanTimeEstimate"
/>
<time-tracking-spent-only-pane
v-if="showSpentOnlyState"
- :time-spent-human-readable="timeSpentHumanReadable"
+ :time-spent-human-readable="humanTimeSpent"
/>
<time-tracking-no-tracking-pane
v-if="showNoTimeTrackingState"
@@ -154,8 +144,8 @@ export default {
v-if="showComparisonState"
:time-estimate="timeEstimate"
:time-spent="timeSpent"
- :time-spent-human-readable="timeSpentHumanReadable"
- :time-estimate-human-readable="timeEstimateHumanReadable"
+ :time-spent-human-readable="humanTimeSpent"
+ :time-estimate-human-readable="humanTimeEstimate"
/>
<transition name="help-state-toggle">
<time-tracking-help-state
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index ffaed9c7193..a6b3a674952 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -3,7 +3,6 @@ import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
const MARK_TEXT = __('Mark todo as done');
const TODO_TEXT = __('Add todo');
@@ -14,7 +13,6 @@ export default {
},
components: {
Icon,
- LoadingIcon,
},
props: {
issuableId: {
@@ -90,7 +88,7 @@ export default {
>
{{ buttonLabel }}
</span>
- <loading-icon
+ <gl-loading-icon
v-show="isActionActive"
:inline="true"
/>
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index b15ad0e5586..87da65a1b1f 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -7,6 +7,8 @@ export default class SidebarMilestone {
if (!el) return;
+ const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = el.dataset;
+
// eslint-disable-next-line no-new
new Vue({
el,
@@ -15,10 +17,10 @@ export default class SidebarMilestone {
},
render: createElement => createElement('timeTracker', {
props: {
- time_estimate: parseInt(el.dataset.timeEstimate, 10),
- time_spent: parseInt(el.dataset.timeSpent, 10),
- human_time_estimate: el.dataset.humanTimeEstimate,
- human_time_spent: el.dataset.humanTimeSpent,
+ timeEstimate: parseInt(timeEstimate, 10),
+ timeSpent: parseInt(timeSpent, 10),
+ humanTimeEstimate,
+ humanTimeSpent,
rootPath: '/',
},
}),
diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/usage_ping_consent.js
new file mode 100644
index 00000000000..ae3fde190e3
--- /dev/null
+++ b/app/assets/javascripts/usage_ping_consent.js
@@ -0,0 +1,30 @@
+import $ from 'jquery';
+import axios from './lib/utils/axios_utils';
+import Flash, { hideFlash } from './flash';
+import { convertPermissionToBoolean } from './lib/utils/common_utils';
+
+export default () => {
+ $('body').on('click', '.js-usage-consent-action', (e) => {
+ e.preventDefault();
+ e.stopImmediatePropagation(); // overwrite rails listener
+
+ const { url, checkEnabled, pingEnabled } = e.target.dataset;
+ const data = {
+ application_setting: {
+ version_check_enabled: convertPermissionToBoolean(checkEnabled),
+ usage_ping_enabled: convertPermissionToBoolean(pingEnabled),
+ },
+ };
+
+ const hideConsentMessage = () => hideFlash(document.querySelector('.ping-consent-message'));
+
+ axios.put(url, data)
+ .then(() => {
+ hideConsentMessage();
+ })
+ .catch(() => {
+ hideConsentMessage();
+ Flash('Something went wrong. Try again later.');
+ });
+ });
+};
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index e19bbbacf4d..b9dfa22dd49 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -592,7 +592,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
var trimmed = query.term.trim();
emailUser = {
- name: "Invite \"" + query.term + "\" by email",
+ name: "Invite \"" + trimmed + "\" by email",
username: trimmed,
id: trimmed,
invite: true
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index d530ab2767b..70518ad73e8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -106,8 +106,8 @@ export default {
</tooltip-on-truncate>
</template>
<span
- v-tooltip
v-if="hasDeploymentTime"
+ v-tooltip
:title="deployment.deployed_at_formatted"
class="js-deploy-time"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 72bd28ae03f..acfdab3a015 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -4,6 +4,7 @@ import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
@@ -13,6 +14,9 @@ export default {
clipboardButton,
TooltipOnTruncate,
},
+ directives: {
+ tooltip,
+ },
props: {
mr: {
type: Object,
@@ -24,11 +28,17 @@ export default {
return this.mr.divergedCommitsCount > 0;
},
commitsBehindText() {
- return sprintf(s__('mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch'), {
- commitsBehindLinkStart: `<a href="${_.escape(this.mr.targetBranchPath)}">`,
- commitsBehind: n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount),
- commitsBehindLinkEnd: '</a>',
- }, false);
+ return sprintf(
+ s__(
+ 'mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch',
+ ),
+ {
+ commitsBehindLinkStart: `<a href="${_.escape(this.mr.targetBranchPath)}">`,
+ commitsBehind: n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount),
+ commitsBehindLinkEnd: '</a>',
+ },
+ false,
+ );
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
@@ -40,10 +50,26 @@ export default {
});
},
webIdePath() {
- return mergeUrlParams({
- target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ?
- this.mr.targetProjectFullPath : '',
- }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`));
+ if (this.mr.canPushToSourceBranch) {
+ return mergeUrlParams(
+ {
+ target_project:
+ this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath
+ ? this.mr.targetProjectFullPath
+ : '',
+ },
+ webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`),
+ );
+ }
+
+ return null;
+ },
+ ideButtonTitle() {
+ return !this.mr.canPushToSourceBranch
+ ? s__(
+ 'mrWidget|You are not allowed to edit this project directly. Please fork to make changes.',
+ )
+ : '';
},
},
};
@@ -91,12 +117,18 @@ export default {
<div
v-if="mr.isOpen"
- class="branch-actions"
+ class="branch-actions d-flex"
>
<a
v-if="!mr.sourceBranchRemoved"
+ v-tooltip
:href="webIdePath"
- class="btn btn-default inline js-web-ide d-none d-md-inline-block"
+ :title="ideButtonTitle"
+ :class="{ disabled: !mr.canPushToSourceBranch }"
+ class="btn btn-default js-web-ide d-none d-md-inline-block append-right-8"
+ data-placement="bottom"
+ tabindex="0"
+ role="button"
>
{{ s__("mrWidget|Open in Web IDE") }}
</a>
@@ -104,15 +136,15 @@ export default {
:disabled="mr.sourceBranchRemoved"
data-target="#modal_merge_info"
data-toggle="modal"
- class="btn btn-default inline js-check-out-branch"
+ class="btn btn-default js-check-out-branch append-right-default"
type="button"
>
{{ s__("mrWidget|Check out branch") }}
</button>
- <span class="dropdown prepend-left-10">
+ <span class="dropdown">
<button
type="button"
- class="btn inline dropdown-toggle"
+ class="btn dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
aria-haspopup="true"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 9aff95dcfec..035ae791a1d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -1,11 +1,9 @@
<script>
import ciIcon from '../../vue_shared/components/ci_icon.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
ciIcon,
- loadingIcon,
},
props: {
status: {
@@ -37,7 +35,7 @@
v-if="isLoading"
class="mr-widget-icon"
>
- <loading-icon />
+ <gl-loading-icon />
</div>
<ci-icon
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index 2133124347c..01294d5b40c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -1,5 +1,4 @@
<script>
- import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -7,7 +6,6 @@
name: 'MRWidgetAutoMergeFailed',
components: {
statusIcon,
- loadingIcon,
},
props: {
mr: {
@@ -44,7 +42,7 @@
class="btn btn-sm btn-default"
@click="refreshWidget"
>
- <loading-icon
+ <gl-loading-icon
v-if="isRefreshing"
:inline="true"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 1a444c04a1d..8184ef33022 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -1,7 +1,6 @@
<script>
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
- import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
@@ -15,7 +14,6 @@
},
components: {
MrWidgetAuthorTime,
- loadingIcon,
statusIcon,
ClipboardButton,
},
@@ -116,8 +114,8 @@
:date-readable="mr.metrics.readableMergedAt"
/>
<a
- v-tooltip
v-if="mr.canRevertInCurrentMR"
+ v-tooltip
:title="revertTitle"
class="btn btn-close btn-sm"
href="#modal-revert-commit"
@@ -127,8 +125,8 @@
{{ revertLabel }}
</a>
<a
- v-tooltip
v-else-if="mr.revertInForkPath"
+ v-tooltip
:href="mr.revertInForkPath"
:title="revertTitle"
class="btn btn-close btn-sm"
@@ -137,8 +135,8 @@
{{ revertLabel }}
</a>
<a
- v-tooltip
v-if="mr.canCherryPickInCurrentMR"
+ v-tooltip
:title="cherryPickTitle"
class="btn btn-default btn-sm"
href="#modal-cherry-pick-commit"
@@ -148,8 +146,8 @@
{{ cherryPickLabel }}
</a>
<a
- v-tooltip
v-else-if="mr.cherryPickInForkPath"
+ v-tooltip
:href="mr.cherryPickInForkPath"
:title="cherryPickTitle"
class="btn btn-default btn-sm"
@@ -195,7 +193,7 @@
</button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
- <loading-icon :inline="true" />
+ <gl-loading-icon :inline="true" />
<span>
{{ s__("mrWidget|The source branch is being removed") }}
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 2d8c3d6be87..f31c7a3edb8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -2,14 +2,12 @@
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
- import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Flash from '../../../flash';
export default {
name: 'MRWidgetRebase',
components: {
statusIcon,
- loadingIcon,
},
props: {
mr: {
@@ -115,7 +113,7 @@ js-toggle-container accept-action media space-children"
class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button"
@click="rebase"
>
- <loading-icon v-if="isMakingRequest" />
+ <gl-loading-icon v-if="isMakingRequest" />
Rebase
</button>
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
deleted file mode 100644
index bf8628d18a6..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
-The squash-before-merge button is EE only, but it's located right in the middle
-of the readyToMerge state component template.
-
-If we didn't declare this component in CE, we'd need to maintain a separate copy
-of the readyToMergeState template in EE, which is pretty big and likely to change.
-
-Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
-In EE, the configuration extends this object to add a functioning squash-before-merge
-button.
-*/
-
-export default {
- template: '',
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 086dbabe77e..e73b7e410d5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -37,7 +37,7 @@ export default {
<a
v-if="mr.newBlobPath"
:href="mr.newBlobPath"
- class="btn btn-inverted btn-save">
+ class="btn btn-inverted btn-success">
Create file
</a>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index a5ca7b719a1..c8ad2aa30a6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -6,7 +6,7 @@ import MergeRequest from '../../../merge_request';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
-import SquashBeforeMerge from './mr_widget_squash_before_merge.vue';
+import SquashBeforeMerge from './squash_before_merge.vue';
export default {
name: 'ReadyToMerge',
@@ -255,7 +255,7 @@ export default {
data-toggle="dropdown"
aria-label="Select merge moment">
<i
- class="fa fa-chevron-down"
+ class="fa fa-chevron-down qa-merge-moment-dropdown"
aria-hidden="true"
></i>
</button>
@@ -265,7 +265,7 @@ export default {
role="menu">
<li>
<a
- class="merge_when_pipeline_succeeds"
+ class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option"
href="#"
@click.prevent="handleMergeButtonClick(true)">
<span class="media">
@@ -279,7 +279,7 @@ export default {
</li>
<li>
<a
- class="accept-merge-request"
+ class="accept-merge-request qa-merge-immediately-option"
href="#"
@click.prevent="handleMergeButtonClick(false, true)">
<span class="media">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index 25c1044fe2b..25ad329e196 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -37,8 +37,8 @@ export default {
<div class="accept-control inline">
<label class="merge-param-checkbox">
<input
- :disabled="isMergeButtonDisabled"
v-model="squashBeforeMerge"
+ :disabled="isMergeButtonDisabled"
type="checkbox"
name="squash"
class="qa-squash-checkbox"
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 15097fa2a3f..a23496c6bf5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -40,7 +40,7 @@ export { default as MRWidgetService } from './services/mr_widget_service';
export { default as eventHub } from './event_hub';
export { default as getStateKey } from './stores/get_state_key';
export { default as stateMaps } from './stores/state_maps';
-export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge.vue';
+export { default as SquashBeforeMerge } from './components/states/squash_before_merge.vue';
export { default as notify } from '../lib/utils/notify';
export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 69a9132a2da..cc6e620f365 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,7 +1,4 @@
-import {
- Vue,
- mrWidgetOptions,
-} from './dependencies';
+import { Vue, mrWidgetOptions } from './dependencies';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index dc6be025f11..0e445a29de4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -107,10 +107,16 @@ export default {
created() {
this.initPolling();
this.bindEventHubListeners();
+ eventHub.$on('mr.discussion.updated', this.checkStatus);
},
mounted() {
this.handleMounted();
},
+ beforeDestroy() {
+ eventHub.$off('mr.discussion.updated', this.checkStatus);
+ this.pollingInterval.destroy();
+ this.deploymentsInterval.destroy();
+ },
methods: {
createService(store) {
const endpoints = {
diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue
index 3ced4eb691a..33af7a7f1df 100644
--- a/app/assets/javascripts/vue_shared/components/bar_chart.vue
+++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue
@@ -291,8 +291,8 @@ export default {
<template
v-for="(data, index) in graphData">
<rect
- v-tooltip
:key="index"
+ v-tooltip
:width="xScale.bandwidth()"
:x="xScale(data.name)"
:y="yScale(data.value)"
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 720ae11aaa6..8684005e0fb 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -3,7 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
-import { getCommitIconMap } from '../utils';
+import { getCommitIconMap } from '~/ide/utils';
export default {
components: {
@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
+ size: {
+ type: Number,
+ required: false,
+ default: 12,
+ },
},
computed: {
changedIcon() {
@@ -42,7 +47,7 @@ export default {
return `${getCommitIconMap(this.file).icon}${suffix}`;
},
changedIconClass() {
- return `ide-${this.changedIcon} float-left`;
+ return `${this.changedIcon} float-left d-block`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
@@ -78,13 +83,30 @@ export default {
:title="tooltipTitle"
data-container="body"
data-placement="right"
- class="ide-file-changed-icon"
+ class="file-changed-icon ml-auto"
>
<icon
v-if="showIcon"
:name="changedIcon"
- :size="12"
+ :size="size"
:css-classes="changedIconClass"
/>
</span>
</template>
+
+<style>
+.file-addition,
+.file-addition-solid {
+ color: #1aaa55;
+}
+
+.file-modified,
+.file-modified-solid {
+ color: #fc9403;
+}
+
+.file-deletion,
+.file-deletion-solid {
+ color: #db3b21;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index f1ef50d0e3d..a07d63a495d 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -1,9 +1,11 @@
<script>
+import { Link } from '@gitlab-org/gitlab-ui';
import Icon from '../../icon.vue';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
components: {
+ 'gl-link': Link,
Icon,
},
props: {
@@ -37,7 +39,7 @@ export default {
({{ fileSizeReadable }})
</template>
</p>
- <a
+ <gl-link
:href="path"
class="btn btn-default"
rel="nofollow"
@@ -49,7 +51,7 @@ export default {
css-classes="float-left append-right-8"
/>
{{ __('Download') }}
- </a>
+ </gl-link>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index a10deb93f0f..807e049caf6 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -2,14 +2,14 @@
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import $ from 'jquery';
-import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
const { CancelToken } = axios;
let axiosSource;
export default {
components: {
- SkeletonLoadingContainer,
+ SkeletonLoading,
},
props: {
content: {
@@ -81,7 +81,7 @@ export default {
<div
ref="markdown-preview"
class="md md-previewer">
- <skeleton-loading-container v-if="isLoading" />
+ <skeleton-loading v-if="isLoading" />
<div
v-else
v-html="previewContent">
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index d3cbe3c7e74..cfc5343217c 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -46,7 +46,7 @@ export default {
}
},
basePath() {
- // We might get the project path from rails with the relative url already setup
+ // We might get the project path from rails with the relative url already set up
return this.projectPath.indexOf('/') === 0 ? '' : `${gon.relative_url_root}/`;
},
fullOldPath() {
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index af5ebcdc40a..31087017968 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -1,11 +1,7 @@
<script>
import { __ } from '~/locale';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
export default {
- components: {
- LoadingIcon,
- },
props: {
isDisabled: {
type: Boolean,
@@ -34,7 +30,7 @@ export default {
data-toggle="dropdown"
aria-expanded="false"
>
- <loading-icon
+ <gl-loading-icon
v-show="isLoading"
:inline="true"
/>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 878c805ada5..408f7d7965f 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,6 +1,5 @@
<script>
import getIconForFile from './file_icon/file_icon_map';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
/* This is a re-usable vue component for rendering a svg sprite
@@ -17,7 +16,6 @@ import icon from '../../vue_shared/components/icon.vue';
*/
export default {
components: {
- loadingIcon,
icon,
},
props: {
@@ -84,7 +82,7 @@ export default {
:size="size"
css-classes="folder-icon"
/>
- <loading-icon
+ <gl-loading-icon
v-if="loading"
:inline="true"
/>
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
new file mode 100644
index 00000000000..36a345130c0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -0,0 +1,237 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
+
+export default {
+ name: 'FileRow',
+ components: {
+ FileIcon,
+ Icon,
+ ChangedFileIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ level: {
+ type: Number,
+ required: true,
+ },
+ extraComponent: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ hideExtraOnTree: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showChangedIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ mouseOver: false,
+ };
+ },
+ computed: {
+ isTree() {
+ return this.file.type === 'tree';
+ },
+ isBlob() {
+ return this.file.type === 'blob';
+ },
+ levelIndentation() {
+ return {
+ marginLeft: `${this.level * 16}px`,
+ };
+ },
+ fileClass() {
+ return {
+ 'file-open': this.isBlob && this.file.opened,
+ 'is-active': this.isBlob && this.file.active,
+ folder: this.isTree,
+ 'is-open': this.file.opened,
+ };
+ },
+ },
+ watch: {
+ 'file.active': function fileActiveWatch(active) {
+ if (this.file.type === 'blob' && active) {
+ this.scrollIntoView();
+ }
+ },
+ },
+ mounted() {
+ if (this.hasPathAtCurrentRoute()) {
+ this.scrollIntoView(true);
+ }
+ },
+ methods: {
+ toggleTreeOpen(path) {
+ this.$emit('toggleTreeOpen', path);
+ },
+ clickedFile(path) {
+ this.$emit('clickFile', path);
+ },
+ clickFile() {
+ // Manual Action if a tree is selected/opened
+ if (this.isTree && this.hasUrlAtCurrentRoute()) {
+ this.toggleTreeOpen(this.file.path);
+ }
+
+ if (this.$router) this.$router.push(`/project${this.file.url}`);
+
+ if (this.isBlob) this.clickedFile(this.file.path);
+ },
+ scrollIntoView(isInit = false) {
+ const block = isInit && this.isTree ? 'center' : 'nearest';
+
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ block,
+ });
+ },
+ hasPathAtCurrentRoute() {
+ if (!this.$router || !this.$router.currentRoute) {
+ return false;
+ }
+
+ // - strip route up to "/-/" and ending "/"
+ const routePath = this.$router.currentRoute.path
+ .replace(/^.*?[/]-[/]/g, '')
+ .replace(/[/]$/g, '');
+
+ // - strip ending "/"
+ const filePath = this.file.path.replace(/[/]$/g, '');
+
+ return filePath === routePath;
+ },
+ hasUrlAtCurrentRoute() {
+ if (!this.$router || !this.$router.currentRoute) return true;
+
+ return this.$router.currentRoute.path === `/project${this.file.url}`;
+ },
+ toggleHover(over) {
+ this.mouseOver = over;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ :class="fileClass"
+ class="file-row"
+ role="button"
+ @click="clickFile"
+ @mouseover="toggleHover(true)"
+ @mouseout="toggleHover(false)"
+ >
+ <div
+ class="file-row-name-container"
+ >
+ <span
+ :style="levelIndentation"
+ class="file-row-name str-truncated"
+ >
+ <file-icon
+ v-if="!showChangedIcon || file.type === 'tree'"
+ :file-name="file.name"
+ :loading="file.loading"
+ :folder="isTree"
+ :opened="file.opened"
+ :size="16"
+ />
+ <changed-file-icon
+ v-else
+ :file="file"
+ :size="16"
+ class="append-right-5"
+ />
+ {{ file.name }}
+ </span>
+ <component
+ :is="extraComponent"
+ v-if="extraComponent && !(hideExtraOnTree && file.type === 'tree')"
+ :file="file"
+ :mouse-over="mouseOver"
+ />
+ </div>
+ </div>
+ <template v-if="file.opened">
+ <file-row
+ v-for="childFile in file.tree"
+ :key="childFile.key"
+ :file="childFile"
+ :level="level + 1"
+ :hide-extra-on-tree="hideExtraOnTree"
+ :extra-component="extraComponent"
+ :show-changed-icon="showChangedIcon"
+ @toggleTreeOpen="toggleTreeOpen"
+ @clickFile="clickedFile"
+ />
+ </template>
+ </div>
+</template>
+
+<style>
+.file-row {
+ display: flex;
+ align-items: center;
+ height: 32px;
+ padding: 4px 8px;
+ margin-left: -8px;
+ margin-right: -8px;
+ border-radius: 3px;
+ text-align: left;
+ cursor: pointer;
+}
+
+.file-row:hover,
+.file-row:focus {
+ background: #f2f2f2;
+}
+
+.file-row:active {
+ background: #dfdfdf;
+}
+
+.file-row.is-active {
+ background: #f2f2f2;
+}
+
+.file-row-name-container {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ overflow: visible;
+}
+
+.file-row-name {
+ display: inline-block;
+ flex: 1;
+ max-width: inherit;
+ height: 18px;
+ line-height: 16px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.file-row-name svg {
+ margin-right: 2px;
+ vertical-align: middle;
+}
+
+.file-row-name .loading-container {
+ display: inline-block;
+ margin-right: 4px;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 49fbce75110..b371b6adf7e 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,6 +1,5 @@
<script>
import CiIconBadge from './ci_badge_link.vue';
-import LoadingIcon from './loading_icon.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip';
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
@@ -15,7 +14,6 @@ import UserAvatarImage from './user_avatar/user_avatar_image.vue';
export default {
components: {
CiIconBadge,
- LoadingIcon,
TimeagoTooltip,
UserAvatarImage,
},
@@ -128,18 +126,18 @@ export default {
>
<a
v-if="action.type === 'link'"
+ :key="i"
:href="action.path"
:class="action.cssClass"
- :key="i"
>
{{ action.label }}
</a>
<a
v-else-if="action.type === 'ujs-link'"
+ :key="i"
:href="action.path"
:class="action.cssClass"
- :key="i"
data-method="post"
rel="nofollow"
>
@@ -148,9 +146,9 @@ export default {
<button
v-else-if="action.type === 'button'"
+ :key="i"
:disabled="action.isLoading"
:class="action.cssClass"
- :key="i"
type="button"
@click="onClickAction(action)"
>
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index 2ff0c056b9c..4cbd3e6429d 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -17,12 +17,7 @@
*/
- import loadingIcon from './loading_icon.vue';
-
export default {
- components: {
- loadingIcon,
- },
props: {
loading: {
type: Boolean,
@@ -60,7 +55,7 @@
@click="onClick"
>
<transition name="fade">
- <loading-icon
+ <gl-loading-icon
v-if="loading"
:inline="true"
:class="{
diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue
deleted file mode 100644
index db22c5f02cd..00000000000
--- a/app/assets/javascripts/vue_shared/components/loading_icon.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<script>
- export default {
- props: {
- label: {
- type: String,
- required: false,
- default: 'Loading',
- },
-
- size: {
- type: String,
- required: false,
- default: '1',
- },
-
- inline: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- computed: {
- rootElementType() {
- return this.inline ? 'span' : 'div';
- },
- cssClass() {
- return `fa-${this.size}x`;
- },
- },
- };
-</script>
-<template>
- <component
- :is="rootElementType"
- class="loading-container text-center">
- <i
- :class="cssClass"
- :aria-label="label"
- class="fa fa-spin fa-spinner"
- aria-hidden="true"
- >
- </i>
- </component>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index d62537021ca..10e8ddad9cd 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -76,6 +76,7 @@
epics: this.enableAutocomplete,
milestones: this.enableAutocomplete,
labels: this.enableAutocomplete,
+ snippets: this.enableAutocomplete,
});
},
beforeDestroy() {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 8c22f3f6536..ccd53158820 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -18,6 +18,16 @@
required: true,
},
},
+ computed: {
+ mdTable() {
+ return [
+ '| header | header |',
+ '| ------ | ------ |',
+ '| cell | cell |',
+ '| cell | cell |',
+ ].join('\n');
+ },
+ },
mounted() {
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
@@ -106,6 +116,12 @@
icon="code"
/>
<toolbar-button
+ tag="[{text}](url)"
+ tag-select="url"
+ button-title="Add a link"
+ icon="link"
+ />
+ <toolbar-button
:prepend="true"
tag="* "
button-title="Add a bullet list"
@@ -123,6 +139,12 @@
button-title="Add a task list"
icon="task-done"
/>
+ <toolbar-button
+ :tag="mdTable"
+ :prepend="true"
+ :button-title="__('Add a table')"
+ icon="table"
+ />
<button
v-tooltip
aria-label="Go full screen"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index d63318f3da6..c45dafa9807 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,5 +1,10 @@
<script>
+ import { Link } from '@gitlab-org/gitlab-ui';
+
export default {
+ components: {
+ 'gl-link': Link,
+ },
props: {
markdownDocsPath: {
type: String,
@@ -28,30 +33,30 @@
<div class="comment-toolbar clearfix">
<div class="toolbar-text">
<template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
- <a
+ <gl-link
:href="markdownDocsPath"
target="_blank"
tabindex="-1"
>
Markdown is supported
- </a>
+ </gl-link>
</template>
<template v-if="hasQuickActionsDocsPath && markdownDocsPath">
- <a
+ <gl-link
:href="markdownDocsPath"
target="_blank"
tabindex="-1"
>
Markdown
- </a>
+ </gl-link>
and
- <a
+ <gl-link
:href="quickActionsDocsPath"
target="_blank"
tabindex="-1"
>
quick actions
- </a>
+ </gl-link>
are supported
</template>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 9f1e009efdd..bda33636369 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -27,6 +27,11 @@
required: false,
default: '',
},
+ tagSelect: {
+ type: String,
+ required: false,
+ default: '',
+ },
prepend: {
type: Boolean,
required: false,
@@ -40,6 +45,7 @@
<button
v-tooltip
:data-md-tag="tag"
+ :data-md-select="tagSelect"
:data-md-block="tagBlock"
:data-md-prepend="prepend"
:title="buttonTitle"
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index 2eb6c20b2c0..1d9c9220469 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -1,3 +1,14 @@
+<script>
+import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+
+export default {
+ name: 'SkeletonNote',
+ components: {
+ SkeletonLoading,
+ },
+};
+</script>
+
<template>
<li class="timeline-entry note">
<div class="timeline-entry-inner">
@@ -6,20 +17,9 @@
<div class="timeline-content">
<div class="note-header"></div>
<div class="note-body">
- <skeleton-loading-container />
+ <skeleton-loading />
</div>
</div>
</div>
</li>
</template>
-
-<script>
-import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-
-export default {
- name: 'SkeletonNote',
- components: {
- skeletonLoadingContainer,
- },
-};
-</script>
diff --git a/app/assets/javascripts/vue_shared/components/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue
new file mode 100644
index 00000000000..1f2a679c145
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue
@@ -0,0 +1,34 @@
+<script>
+import { s__ } from '../../locale';
+
+export default {
+ props: {
+ change: {
+ type: Function,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+ firstText: s__('Pagination|« First'),
+ prevText: s__('Pagination|Prev'),
+ nextText: s__('Pagination|Next'),
+ lastText: s__('Pagination|Last »'),
+};
+</script>
+
+<template>
+ <gl-pagination
+ v-bind="$attrs"
+ :change="change"
+ :page="pageInfo.page"
+ :per-page="pageInfo.perPage"
+ :total-items="pageInfo.total"
+ :first-text="$options.firstText"
+ :prev-text="$options.prevText"
+ :next-text="$options.nextText"
+ :last-text="$options.lastText"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 74998a4787d..9d757b27edc 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -1,6 +1,5 @@
<script>
import datePicker from '../pikaday.vue';
- import loadingIcon from '../loading_icon.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
import { dateInWords } from '../../../lib/utils/datetime_utility';
@@ -10,7 +9,6 @@
components: {
datePicker,
toggleSidebar,
- loadingIcon,
collapsedCalendarIcon,
},
props: {
@@ -112,7 +110,7 @@
/>
<div class="title">
{{ label }}
- <loading-icon
+ <gl-loading-icon
v-if="isLoading"
:inline="true"
/>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index a3fc358130f..3df286de129 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -3,7 +3,6 @@ import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import LoadingIcon from '../../loading_icon.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
@@ -16,7 +15,6 @@ import DropdownCreateLabel from './dropdown_create_label.vue';
export default {
components: {
- LoadingIcon,
DropdownTitle,
DropdownValue,
DropdownValueCollapsed,
@@ -164,7 +162,7 @@ dropdown-menu-labels dropdown-menu-selectable"
<dropdown-search-input/>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
- <loading-icon />
+ <gl-loading-icon />
</div>
<dropdown-footer
v-if="showCreate"
diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
deleted file mode 100644
index 4a5ffbe5d5a..00000000000
--- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<script>
- export default {
- props: {
- small: {
- type: Boolean,
- required: false,
- default: false,
- },
- lines: {
- type: Number,
- required: false,
- default: 3,
- },
- },
- computed: {
- lineClasses() {
- return new Array(this.lines).fill().map((_, i) => `skeleton-line-${i + 1}`);
- },
- },
- };
-</script>
-
-<template>
- <div
- :class="{
- 'animation-container-small': small,
- }"
- class="animation-container"
- >
- <div
- v-for="(css, index) in lineClasses"
- :key="index"
- :class="css"
- >
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
index 78fde463507..cd3ee544344 100644
--- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
@@ -99,8 +99,8 @@ export default {
{{ __("Not available") }}
</span>
<span
- v-tooltip
v-if="successPercent"
+ v-tooltip
:title="successTooltip"
:style="successBarStyle"
class="status-green"
@@ -109,8 +109,8 @@ export default {
{{ successPercent }}%
</span>
<span
- v-tooltip
v-if="neutralPercent"
+ v-tooltip
:title="neutralTooltip"
:style="neutralBarStyle"
class="status-neutral"
@@ -119,8 +119,8 @@ export default {
{{ neutralPercent }}%
</span>
<span
- v-tooltip
v-if="failurePercent"
+ v-tooltip
:title="failureTooltip"
:style="failureBarStyle"
class="status-red"
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index a897300b62b..5b9c51786d6 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -1,7 +1,6 @@
<script>
import { s__ } from '../../locale';
import icon from './icon.vue';
- import loadingIcon from './loading_icon.vue';
const ICON_ON = 'status_success_borderless';
const ICON_OFF = 'status_failed_borderless';
@@ -11,7 +10,6 @@
export default {
components: {
icon,
- loadingIcon,
},
model: {
@@ -78,7 +76,7 @@
class="project-feature-toggle"
@click="toggleFeature"
>
- <loadingIcon class="loading-icon" />
+ <gl-loading-icon class="loading-icon" />
<span class="toggle-icon">
<icon
:name="toggleIcon"
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
index 125826da6c3..d5b58574123 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
@@ -51,8 +51,8 @@ export default {
<template>
<span
- v-tooltip
v-if="showTooltip"
+ v-tooltip
:title="title"
:data-placement="placement"
class="js-show-tooltip"
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 01c36fec41a..ee3157bcb1b 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -18,12 +18,14 @@
*/
+import { Link } from '@gitlab-org/gitlab-ui';
import userAvatarImage from './user_avatar_image.vue';
import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
components: {
+ 'gl-link': Link,
userAvatarImage,
},
directives: {
@@ -83,7 +85,7 @@ export default {
</script>
<template>
- <a
+ <gl-link
:href="linkHref"
class="user-avatar-link">
<user-avatar-image
@@ -94,10 +96,10 @@ export default {
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
/><span
- v-tooltip
v-if="shouldShowUsername"
+ v-tooltip
:title="tooltipText"
:tooltip-placement="tooltipPlacement"
>{{ username }}</span>
- </a>
+ </gl-link>
</template>
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index 73b9131e5ba..b9693892f45 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -28,7 +28,7 @@ Vue.http.interceptors.push((request, next) => {
response.headers.forEach((value, key) => {
headers[key] = value;
});
-
+ // eslint-disable-next-line no-param-reassign
response.headers = headers;
});
});
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index f2950308019..ffe65ce780e 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -5,7 +5,6 @@
*= require jquery.atwho
*= require select2
*= require_self
- *= require dropzone/basic
*= require cropper.css
*/
@@ -18,6 +17,7 @@
*/
@import "../../../node_modules/pikaday/scss/pikaday";
+@import "../../../node_modules/dropzone/dist/basic.css";
/*
* GitLab UI framework
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index c91f5e279ea..af73954bd2e 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -93,7 +93,6 @@ hr {
}
.form-group.row .col-form-label {
- padding-top: 0;
// Bootstrap 4 aligns labels to the left
// for horizontal forms
@include media-breakpoint-up(md) {
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index b1a20c06910..4ffb3e9ab42 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -27,7 +27,6 @@
@import 'framework/header';
@import 'framework/highlight';
@import 'framework/issue_box';
-@import 'framework/jquery';
@import 'framework/lists';
@import 'framework/logo';
@import 'framework/markdown_area';
@@ -64,3 +63,4 @@
@import 'framework/ci_variable_list';
@import 'framework/feature_highlight';
@import 'framework/terms';
+@import 'framework/read_more';
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 9dd0384a228..fcf282a7d7c 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -69,7 +69,7 @@
.identicon {
text-align: center;
vertical-align: top;
- color: $identicon-fg-color;
+ color: $gl-gray-700;
background-color: $gray-darker;
// Sizes
@@ -104,6 +104,7 @@
a {
width: 100%;
+ height: 100%;
display: flex;
}
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index a265e4206f1..702276780e9 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -229,8 +229,8 @@
svg {
margin-bottom: 1px;
- height: 18px;
- width: 18px;
+ height: $default-icon-size;
+ width: $default-icon-size;
border-radius: 50%;
path {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 72b4ed0ac33..c4296c7a88a 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -147,17 +147,12 @@
}
&.btn-success,
- &.btn-new,
- &.btn-create,
- &.btn-save {
+ &.btn-register {
@include btn-green;
}
&.btn-inverted {
- &.btn-success,
- &.btn-new,
- &.btn-create,
- &.btn-save {
+ &.btn-success {
@include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700);
}
@@ -165,6 +160,10 @@
@include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}
+ &.btn-warning {
+ @include btn-outline($white-light, $orange-500, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
+ }
+
&.btn-primary,
&.btn-info {
@include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
@@ -172,8 +171,7 @@
}
&.btn-info,
- &.btn-primary,
- &.btn-register {
+ &.btn-primary {
@include btn-blue;
}
@@ -248,7 +246,7 @@
.btn-terminal {
svg {
height: 14px;
- width: 18px;
+ width: $default-icon-size;
}
}
@@ -362,10 +360,14 @@
i {
color: $gl-text-color-secondary;
}
+
+ svg {
+ fill: $gl-text-color-secondary;
+ }
}
.clone-dropdown-btn a {
- color: $dropdown-link-color;
+ color: $gl-gray-700;
&:hover {
text-decoration: none;
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 0b9dff64b0b..9638fee6078 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -1,8 +1,7 @@
-.calender-block {
+.calendar-block {
padding-left: 0;
padding-right: 0;
border-top: 0;
- direction: rtl;
@media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
overflow-x: auto;
@@ -42,10 +41,13 @@
}
.calendar-hint {
- margin-top: -23px;
- float: right;
font-size: 12px;
- direction: ltr;
+
+ &.bottom-right {
+ direction: ltr;
+ margin-top: -23px;
+ float: right;
+ }
}
.pika-single.gitlab-theme {
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 72e27f9ad16..3c9505a21d6 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -43,7 +43,7 @@
color: $brand-info;
}
-.hint { font-style: italic; color: $hint-color; }
+.hint { font-style: italic; color: $gl-gray-400; }
.light { color: $gl-text-color; }
.slead {
@@ -70,13 +70,6 @@ pre {
padding: 0;
}
- &.card.card-body-pre {
- border: 1px solid $gray-darker;
- background: $gray-light;
- border-radius: 0;
- color: $well-pre-color;
- }
-
&.wrap {
word-break: break-word;
white-space: pre-wrap;
@@ -121,49 +114,24 @@ hr {
text-decoration: none;
}
-.back-link {
- font-size: 14px;
-}
-
table {
a code {
position: relative;
top: -2px;
margin-right: 3px;
}
-
- td.permission-x {
- background: $table-permission-x-bg !important;
- text-align: center;
- }
}
.loading {
margin: 20px auto;
height: 40px;
- color: $loading-color;
+ color: $gl-gray-700;
font-size: 32px;
text-align: center;
}
-span.update-author {
- display: block;
- color: $update-author-color;
- font-weight: $gl-font-weight-normal;
- font-style: italic;
-
- strong {
- font-weight: $gl-font-weight-bold;
- font-style: normal;
- }
-}
-
-.field_with_errors {
- display: inline;
-}
-
p.time {
- color: $time-color;
+ color: $gl-gray-400;
font-size: 90%;
margin: 30px 3px 3px 2px;
}
@@ -197,40 +165,11 @@ li.note {
background-color: inherit;
}
-.project_member_show {
- td:first-child {
- color: $project-member-show-color;
- }
-}
-
-.rss-icon {
- img {
- width: 24px;
- vertical-align: top;
- }
-
- strong {
- line-height: 24px;
- }
-}
-
.show-suppressed-diff,
.show-all-commits {
cursor: pointer;
}
-.git_error_tips {
- @extend .col-lg-6;
- text-align: left;
- margin-top: 40px;
-
- pre {
- background: $white-light;
- border: 0;
- font-size: 12px;
- }
-}
-
.error-message {
padding: 10px;
background: $red-400;
@@ -258,7 +197,7 @@ li.note {
.gitlab-promo {
a {
- color: $gl-promo-color;
+ color: $gl-gray-350;
margin-right: 30px;
}
}
@@ -271,19 +210,6 @@ li.note {
}
}
-.control-group {
- .controls {
- span {
- &.descr {
- position: relative;
- top: 2px;
- left: 5px;
- color: $control-group-descr-color;
- }
- }
- }
-}
-
img.emoji {
height: 20px;
vertical-align: top;
@@ -302,12 +228,6 @@ img.emoji {
margin-bottom: 10px;
}
-.side-filters {
- fieldset {
- margin-bottom: 15px;
- }
-}
-
.footer-links {
margin-bottom: 20px;
@@ -329,25 +249,6 @@ img.emoji {
text-align: center;
}
-.header-with-avatar {
- h3 {
- margin: 0;
- font-weight: $gl-font-weight-bold;
- }
-
- .username {
- font-size: 18px;
- color: $username-color;
- margin-top: 8px;
- }
-
- .description {
- font-size: $gl-font-size;
- color: $description-color;
- margin-top: 8px;
- }
-}
-
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
@@ -386,16 +287,6 @@ img.emoji {
}
}
-.content-separator {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- border-top: 1px solid $border-color;
-}
-
-.hide-bottom-border {
- border-bottom: 0 !important;
-}
-
.gl-accessibility {
&:focus {
display: flex;
@@ -433,6 +324,16 @@ img.emoji {
word-wrap: break-word;
}
+.checkbox-icon-inline-wrapper {
+ .checkbox {
+ display: inline;
+
+ label {
+ display: inline;
+ }
+ }
+}
+
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index e2bbcc67a67..2e7f25d975e 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -9,8 +9,7 @@
padding-left: $contextual-sidebar-width;
}
- .issues-bulk-update.right-sidebar.right-sidebar-expanded
- .issuable-sidebar-header {
+ .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
padding: 10px 0 15px;
}
}
@@ -75,7 +74,7 @@
.nav-sidebar {
transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed;
- z-index: 400;
+ z-index: 600;
width: $contextual-sidebar-width;
top: $header-height;
bottom: 0;
@@ -86,8 +85,7 @@
&:not(.sidebar-collapsed-desktop) {
@media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
- box-shadow: inset -1px 0 0 $border-color,
- 2px 1px 3px $dropdown-shadow-color;
+ box-shadow: inset -1px 0 0 $border-color, 2px 1px 3px $dropdown-shadow-color;
}
}
@@ -113,7 +111,7 @@
}
.avatar-container {
- margin-right: 0;
+ margin: 0 auto;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 8a224dc517e..8603714f709 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -607,25 +607,25 @@
width: 100%;
min-height: 30px;
padding: 0 7px;
- color: $dropdown-input-color;
+ color: $gl-gray-700;
line-height: 30px;
border: 1px solid $dropdown-divider-color;
border-radius: 2px;
outline: 0;
&:focus {
- color: $dropdown-link-color;
+ color: $gl-gray-700;
border-color: $blue-300;
box-shadow: 0 0 4px $dropdown-input-focus-shadow;
~ .fa {
- color: $dropdown-link-color;
+ color: $gl-gray-700;
}
}
&:hover {
~ .fa {
- color: $dropdown-link-color;
+ color: $gl-gray-700;
}
}
}
@@ -890,7 +890,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
position: absolute;
top: 13px;
right: 25px;
- color: $md-area-border;
+ color: $gray-100;
}
}
@@ -929,7 +929,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
&:hover {
.frequent-items-item-avatar-container .avatar {
- border-color: $md-area-border;
+ border-color: $gray-100;
}
}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 6c50ea719d3..be85e03430e 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -6,3 +6,13 @@ gl-emoji {
font-size: 1.4em;
line-height: 1em;
}
+
+.user-status-emoji {
+ margin-right: $gl-padding-4;
+
+ gl-emoji {
+ font-size: 1em;
+ line-height: 16px;
+ vertical-align: baseline;
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 1d3512bbb4c..53f198b47c6 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -184,7 +184,7 @@
&.line-numbers {
float: none;
- border-left: 1px solid $blame-line-numbers-border;
+ border-left: 1px solid $gl-gray-100;
i {
float: none;
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index abfe350677e..d5693a5d1a1 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -92,8 +92,8 @@
display: -webkit-flex;
display: flex;
flex-shrink: 0;
- margin-top: 5px;
- margin-bottom: 5px;
+ margin-top: 4px;
+ margin-bottom: 4px;
.selectable {
display: -webkit-flex;
@@ -216,8 +216,8 @@
vertical-align: inherit;
img {
- height: 18px;
- width: 18px;
+ height: $default-icon-size;
+ width: $default-icon-size;
}
}
@@ -389,9 +389,8 @@
.btn {
text-overflow: ellipsis;
- .fa {
- width: 15px;
- line-height: $line-height-base;
+ svg {
+ margin-right: $gl-padding-8;
}
.dropdown-label-box {
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index d2ba76f5160..50d4298d418 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -11,6 +11,10 @@
padding: 0 2px;
background-color: $blue-100;
border-radius: $border-radius-default;
+
+ &.current-user {
+ background-color: $orange-100;
+ }
}
.gfm-color_chip {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 11a30d83f03..c430009bfe0 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -529,9 +529,10 @@
}
.header-user {
- .dropdown-menu {
+ &.show .dropdown-menu {
width: auto;
min-width: unset;
+ max-height: 323px;
margin-top: 4px;
color: $gl-text-color;
left: auto;
@@ -542,6 +543,18 @@
.user-name {
display: block;
}
+
+ .user-status-emoji {
+ margin-right: 0;
+ display: block;
+ vertical-align: text-top;
+ max-width: 148px;
+ font-size: 12px;
+
+ gl-emoji {
+ font-size: $gl-font-size;
+ }
+ }
}
svg {
@@ -573,3 +586,24 @@
}
}
}
+
+.set-user-status-modal {
+ .modal-body {
+ min-height: unset;
+ }
+
+ .input-lg {
+ max-width: unset;
+ }
+
+ .no-emoji-placeholder,
+ .clear-user-status {
+ svg {
+ fill: $gl-text-color-secondary;
+ }
+ }
+
+ .emoji-menu-toggle-button {
+ @include emoji-menu-toggle-button;
+ }
+}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index f002edced8a..abd26e38d18 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -64,6 +64,7 @@
}
}
+.ci-status-icon-scheduled,
.ci-status-icon-manual {
svg {
fill: $gl-text-color;
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
deleted file mode 100644
index d1360a0c0eb..00000000000
--- a/app/assets/stylesheets/framework/jquery.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-.ui-widget {
- font-family: $regular-font;
- font-size: $font-size-base;
-
- .ui-state-default {
- border: 1px solid $white-light;
- background: $white-light;
- color: $jq-ui-default-color;
- }
-
- .ui-state-highlight {
- border: 0;
- background: transparent;
- }
-}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index d4bae4cb137..9218df9b40f 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -69,10 +69,14 @@ body {
float: right;
}
- /* Center alert text and alert action links on smaller screens */
- @include media-breakpoint-down(sm) {
- .alert {
- text-align: center;
+ .flex-alert {
+ @include media-breakpoint-up(lg) {
+ display: flex;
+
+ .alert-message {
+ flex: 1;
+ padding-right: 40px;
+ }
}
.alert-link-group {
@@ -80,6 +84,13 @@ body {
}
}
+ @include media-breakpoint-down(sm) {
+ .alert-link-group {
+ float: none;
+ margin-top: $gl-padding-8;
+ }
+ }
+
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
.alert-warning {
transition: background-color 0.15s, border-color 0.15s;
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index fdc0454d837..d9d4a210f5f 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -111,6 +111,7 @@ ul.content-list {
border-color: $white-normal;
font-size: $gl-font-size;
color: $gl-text-color;
+ word-break: break-word;
&.no-description {
.title {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index d8391b59a8c..554e2b6720a 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -122,7 +122,7 @@
.markdown-area {
border-radius: 0;
background: $white-light;
- border: 1px solid $md-area-border;
+ border: 1px solid $gray-100;
min-height: 140px;
max-height: 500px;
padding: 5px;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 7edb89ce6f3..be41dbfc61f 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -20,20 +20,24 @@
display: inline-block;
overflow-x: auto;
border: 0;
- border-color: $md-area-border;
+ border-color: $gl-gray-100;
@supports (width: fit-content) {
display: block;
width: fit-content;
}
+ tbody {
+ background-color: $white-light;
+ }
+
tr {
th {
- border-bottom: solid 2px $md-area-border;
+ border-bottom: solid 2px $gl-gray-100;
}
td {
- border-color: $md-area-border;
+ border-color: $gl-gray-100;
}
}
}
@@ -266,3 +270,59 @@
border-radius: 50%;
}
}
+
+@mixin emoji-menu-toggle-button {
+ line-height: 1;
+ padding: 0;
+ min-width: 16px;
+ color: $gray-darkest;
+ fill: $gray-darkest;
+
+ .fa {
+ position: relative;
+ font-size: 16px;
+ }
+
+ svg {
+ @include btn-svg;
+ margin: 0;
+ }
+
+ .award-control-icon-positive,
+ .award-control-icon-super-positive {
+ position: absolute;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ }
+
+ &:hover,
+ &.is-active {
+ .danger-highlight {
+ color: $red-500;
+ }
+
+ .link-highlight {
+ color: $blue-600;
+ fill: $blue-600;
+ }
+
+ .award-control-icon-neutral {
+ opacity: 0;
+ }
+
+ .award-control-icon-positive {
+ opacity: 1;
+ }
+ }
+
+ &.is-active {
+ .award-control-icon-positive {
+ opacity: 0;
+ }
+
+ .award-control-icon-super-positive {
+ opacity: 1;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 6244fb86fea..6d20c46b99d 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -4,11 +4,6 @@
margin-top: 20px;
}
- .container-fluid {
- padding-left: 5px;
- padding-right: 5px;
- }
-
.nav-links > li > a {
padding: 10px;
font-size: 12px;
@@ -49,12 +44,8 @@
.project-repo-buttons {
display: block;
- .count-buttons .btn {
- margin: 0 10px;
- }
-
- .count-buttons .count-with-arrow {
- display: none;
+ .count-buttons .count-badge {
+ margin-top: $gl-padding-8;
}
}
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 7d53a631cdf..7e30747963a 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -19,6 +19,17 @@
}
}
+ // leave enough space for the close icon
+ .modal-title {
+ &.mw-100,
+ &.w-100 {
+ // after upgrading to Bootstrap 4.2 we can use $modal-header-padding-x here
+ // https://github.com/twbs/bootstrap/pull/26976
+ margin-right: -2rem;
+ padding-right: 2rem;
+ }
+ }
+
.page-title {
margin-top: 0;
}
@@ -59,7 +70,7 @@
}
@include media-breakpoint-up(sm) {
- .btn:first-of-type {
+ .btn:nth-child(1) {
margin-left: auto;
}
}
diff --git a/app/assets/stylesheets/framework/read_more.scss b/app/assets/stylesheets/framework/read_more.scss
new file mode 100644
index 00000000000..b84b6e0b256
--- /dev/null
+++ b/app/assets/stylesheets/framework/read_more.scss
@@ -0,0 +1,13 @@
+.read-more-container {
+ @include media-breakpoint-down(md) {
+ &:not(.is-expanded) {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ > * {
+ display: inline;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index 764bebd82c6..29a9c076cdf 100644
--- a/app/assets/stylesheets/framework/responsive_tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -39,7 +39,7 @@
.table-section {
white-space: nowrap;
- $section-widths: 10 15 20 25 30 40 50 100;
+ $section-widths: 5 10 15 20 25 30 40 50 60 100;
@each $width in $section-widths {
&.section-#{$width} {
flex: 0 0 #{$width + '%'};
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 3ae2c7078d6..381c0290d32 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -237,7 +237,7 @@
}
.group-path {
- color: $group-path-color;
+ color: $gl-gray-400;
}
}
@@ -257,7 +257,7 @@
.namespace-result {
.namespace-kind {
- color: $namespace-kind-color;
+ color: $gl-gray-350;
font-weight: $gl-font-weight-normal;
}
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 7152ef9bcfd..36ab38f1c9d 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -45,7 +45,7 @@
}
}
-.snippet-scope-menu .btn-new {
+.snippet-scope-menu .btn-success {
margin-top: 15px;
}
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
index 20394cc1e52..8258da07e4d 100644
--- a/app/assets/stylesheets/framework/toggle.scss
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -31,7 +31,7 @@
height: 24px;
cursor: pointer;
user-select: none;
- background: $feature-toggle-color-disabled;
+ background: $gl-gray-400;
border-radius: 12px;
padding: 3px;
transition: all .4s ease;
@@ -56,12 +56,12 @@
&,
.toggle-icon-svg {
- width: 18px;
- height: 18px;
+ width: $default-icon-size;
+ height: $default-icon-size;
}
.toggle-icon-svg {
- fill: $feature-toggle-color-disabled;
+ fill: $gl-gray-400;
}
.toggle-status-checked {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 9929f1bdebf..6d891e21556 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -61,12 +61,12 @@
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
- color: $kdb-color;
+ color: $gl-gray-700;
vertical-align: middle;
background-color: $kdb-bg;
border-width: 1px;
border-style: solid;
- border-color: $kdb-border $kdb-border $kdb-border-bottom;
+ border-color: $gl-gray-200 $gl-gray-200 $kdb-border-bottom;
border-image: none;
border-radius: 3px;
box-shadow: 0 -1px 0 $kdb-shadow inset;
@@ -286,7 +286,7 @@ body {
}
.page-title {
- margin-top: $gl-padding;
+ margin: #{2 * $grid-size} 0;
line-height: 1.3;
font-size: 1.25em;
font-weight: $gl-font-weight-bold;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d76f5cbd9ff..b7a95f604b8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -31,6 +31,14 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c4c4c4;
+$gl-gray-100: #dddddd;
+$gl-gray-200: #cccccc;
+$gl-gray-350: #aaaaaa;
+$gl-gray-400: #999999;
+$gl-gray-500: #777777;
+$gl-gray-600: #666666;
+$gl-gray-700: #555555;
+
$green-50: #f1fdf6;
$green-100: #dcf5e7;
$green-200: #b3e6c8;
@@ -47,7 +55,7 @@ $blue-50: #f6fafe;
$blue-100: #e4f0fb;
$blue-200: #b8d6f4;
$blue-300: #73afea;
-$blue-400: #2e87e0;
+$blue-400: #418cd8;
$blue-500: #1f78d1;
$blue-600: #1b69b6;
$blue-700: #17599c;
@@ -59,7 +67,7 @@ $orange-50: #fffaf4;
$orange-100: #fff1de;
$orange-200: #fed69f;
$orange-300: #fdbc60;
-$orange-400: #fca121;
+$orange-400: #fca429;
$orange-500: #fc9403;
$orange-600: #de7e00;
$orange-700: #c26700;
@@ -70,7 +78,7 @@ $orange-950: #592800;
$red-50: #fef6f5;
$red-100: #fbe5e1;
$red-200: #f2b4a9;
-$red-300: #e67664;
+$red-300: #ea8271;
$red-400: #e05842;
$red-500: #db3b21;
$red-600: #c0341d;
@@ -207,11 +215,6 @@ $list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
/*
- * Markdown
- */
-$md-area-border: #ddd;
-
-/*
* Code
*/
$code-font-size: 90%;
@@ -241,7 +244,6 @@ $input-horizontal-padding: 12px;
/*
* Misc
*/
-$progress-color: #c0392b;
$header-height: 40px;
$ide-statusbar-height: 25px;
$fixed-layout-width: 1280px;
@@ -250,20 +252,13 @@ $container-text-max-width: 540px;
$gl-avatar-size: 40px;
$border-radius-default: 4px;
$border-radius-small: 2px;
-$settings-icon-size: 18px;
+$default-icon-size: 18px;
$layout-link-gray: #7e7c7c;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-margin-5: 5px;
$sidebar-block-hover-color: #ebebeb;
-$group-path-color: #999;
-$namespace-kind-color: #aaa;
-$panel-heading-link-color: #777;
-$graph-author-email-color: #777;
$count-arrow-border: #dce0e5;
-$save-project-loader-color: #555;
-$divergence-graph-bar-bg: #ccc;
-$divergence-graph-separator-bg: #ccc;
$general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
@@ -271,24 +266,13 @@ $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
+$project-title-row-height: 24px;
/*
* Common component specific colors
*/
-$hint-color: #999;
-$well-pre-color: #555;
-$loading-color: #555;
-$update-author-color: #999;
$user-mention-bg: rgba($blue-500, 0.044);
$user-mention-bg-hover: rgba($blue-500, 0.15);
-$time-color: #999;
-$project-member-show-color: #aaa;
-$gl-promo-color: #aaa;
-$control-group-descr-color: #666;
-$table-permission-x-bg: #d9edf7;
-$username-color: #666;
-$description-color: #666;
-$profiler-border: #eee;
/* tanuki logo colors */
$tanuki-red: #e24329;
@@ -319,9 +303,7 @@ $line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
$dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1);
-$file-mode-changed: #777;
$diff-image-info-color: gray;
-$diff-swipe-border: #999;
$diff-view-modes-color: gray;
$diff-view-modes-border: #c1c1c1;
$diff-jagged-border-gradient-color: darken($white-normal, 8%);
@@ -332,7 +314,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace;
$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
- 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
/*
* Dropdowns
@@ -341,12 +324,10 @@ $dropdown-width: 300px;
$dropdown-min-height: 40px;
$dropdown-max-height: 312px;
$dropdown-vertical-offset: 4px;
-$dropdown-link-color: #555;
$dropdown-empty-row-bg: rgba(#000, 0.04);
$dropdown-shadow-color: rgba(#000, 0.1);
$dropdown-divider-color: rgba(#000, 0.1);
$dropdown-title-btn-color: #bfbfbf;
-$dropdown-input-color: #555;
$dropdown-input-fa-color: #c7c7c7;
$dropdown-input-focus-shadow: rgba($blue-300, 0.4);
$dropdown-loading-bg: rgba(#fff, 0.6);
@@ -419,15 +400,9 @@ $location-icon-color: #e7e9ed;
$note-disabled-comment-color: #b2b2b2;
$note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3;
-$note-line2-border: #ddd;
$note-icon-gutter-width: 55px;
/*
-* Zen
-*/
-$zen-control-color: #555;
-
-/*
* Identicon
*/
$identicon-red: #ffebee;
@@ -436,7 +411,6 @@ $identicon-indigo: #e8eaf6;
$identicon-blue: #e3f2fd;
$identicon-teal: #e0f2f1;
$identicon-orange: #fbe9e7;
-$identicon-fg-color: #555555;
/*
* Calendar
@@ -505,16 +479,8 @@ $common-gray-light: #bbb;
$common-gray-dark: #444;
/*
-* Events
-*/
-$events-pre-color: #777;
-$events-note-icon-color: #777;
-$events-body-border: #ddd;
-
-/*
* Files
*/
-$blame-line-numbers-border: #ddd;
$logs-li-color: #888;
$logs-p-color: #333;
@@ -533,8 +499,6 @@ $input-short-md-width: 280px;
* Help
*/
$document-index-color: #888;
-$help-shortcut-color: #999;
-$help-shortcut-mapping-color: #555;
$help-shortcut-header-color: #333;
/*
@@ -545,12 +509,6 @@ $issues-today-border: #e1e8d5;
$compare-display-color: #888;
/*
-* jQuery UI
-*/
-$jq-ui-border: #ddd;
-$jq-ui-default-color: #777;
-
-/*
* Label
*/
$label-font-size: 12px;
@@ -574,34 +532,19 @@ $fade-mask-transition-curve: ease-in-out;
$login-brand-holder-color: #888;
/*
-* Nav
-*/
-$nav-link-gray: #959494;
-$nav-toggle-gray: #666;
-
-/*
-* Notify
-*/
-$notify-details: #777;
-$notify-footer: #777;
-
-/*
* Projects
*/
$project-option-descr-color: #54565b;
-$project-breadcrumb-color: #999;
$project-network-controls-color: #888;
$feature-toggle-color: #fff;
$feature-toggle-text-color: #fff;
-$feature-toggle-color-disabled: #999;
$feature-toggle-color-enabled: #4a8bee;
/*
Stat Graph
*/
$stat-graph-common-bg: #f3f3f3;
-$stat-graph-axis-fill: #aaa;
$stat-graph-selection-fill: #333;
$stat-graph-selection-stroke: #333;
@@ -612,17 +555,9 @@ $select2-drop-shadow1: rgba(76, 86, 103, 0.247059);
$select2-drop-shadow2: rgba(31, 37, 50, 0.317647);
/*
-* Todo
-*/
-$todo-body-pre-color: #777;
-$todo-body-border: #ddd;
-
-/*
* Typography
*/
$kdb-bg: #fcfcfc;
-$kdb-color: #555;
-$kdb-border: #ccc;
$kdb-border-bottom: #bbb;
$kdb-shadow: #bbb;
$body-text-shadow: rgba(255, 255, 255, 0.01);
@@ -631,7 +566,6 @@ $body-text-shadow: rgba(255, 255, 255, 0.01);
* UI Dev Kit
*/
$ui-dev-kit-example-color: #bbb;
-$ui-dev-kit-example-border: #ddd;
/*
Pipeline Graph
@@ -665,12 +599,10 @@ $dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1);
/*
Performance Bar
*/
-$perf-bar-text: #999;
$perf-bar-production: #222;
$perf-bar-staging: #291430;
$perf-bar-development: #4c1210;
$perf-bar-bucket-bg: #111;
-$perf-bar-bucket-color: #ccc;
$perf-bar-bucket-box-shadow-from: rgba($white-light, 0.2);
$perf-bar-bucket-box-shadow-to: rgba($black, 0.25);
@@ -703,5 +635,4 @@ Modals
*/
$modal-body-height: 134px;
-
$priority-label-empty-state-width: 114px;
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index 7d90452e1f4..759b4f333ca 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -18,3 +18,4 @@ $success: $green-500;
$info: $blue-500;
$warning: $orange-500;
$danger: $red-500;
+$zindex-modal-backdrop: 1040;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index f2d296fb875..a4fbd9c073f 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -35,7 +35,7 @@
.zen-control {
padding: 0;
- color: $zen-control-color;
+ color: $gl-gray-700;
background: none;
border: 0;
}
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index a81e5eb5ebf..f24c80bd81c 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -7,12 +7,12 @@ img {
p.details {
font-style: italic;
- color: $notify-details;
+ color: $gl-gray-500;
}
.footer > p {
font-size: small;
- color: $notify-footer;
+ color: $gl-gray-500;
}
pre.commit-message {
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 5ff4e487d04..07d82e984ba 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -7,6 +7,8 @@ $ide-context-header-padding: 10px;
$ide-project-avatar-end: $ide-context-header-padding + 48px;
$ide-tree-padding: $gl-padding;
$ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
+$ide-commit-row-height: 32px;
+$ide-commit-header-height: 48px;
.project-refs-form,
.project-refs-target-form {
@@ -51,83 +53,9 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
flex: 1;
min-height: 0; // firefox fix
- .file {
- height: 32px;
- cursor: pointer;
-
- &.file-active {
- background: $theme-gray-100;
- }
-
- .ide-file-name {
- flex: 1;
- white-space: nowrap;
- text-overflow: ellipsis;
- max-width: inherit;
- line-height: 16px;
- display: inline-block;
- height: 18px;
-
- svg {
- vertical-align: middle;
- margin-right: 2px;
- }
-
- .loading-container {
- margin-right: 4px;
- display: inline-block;
- }
- }
-
- .ide-file-icon-holder {
- display: flex;
- align-items: center;
- color: $theme-gray-700;
- }
-
- .ide-file-changed-icon {
- margin-left: auto;
-
- > svg {
- display: block;
- }
- }
-
- .ide-new-btn {
- display: none;
-
- .btn {
- padding: 2px 5px;
- }
- }
-
- &:hover,
- &:focus {
- .ide-new-btn {
- display: block;
- }
- }
-
- .folder-icon {
- fill: $gl-text-color-secondary;
- }
- }
-
a {
color: $gl-text-color;
}
-
- th {
- position: sticky;
- top: 0;
- }
-}
-
-.file-name {
- display: flex;
- overflow: visible;
- align-items: center;
- width: 100%;
}
.multi-file-loading-container {
@@ -567,24 +495,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
}
.multi-file-commit-panel-header {
- display: flex;
- align-items: center;
- margin-bottom: 0;
+ height: $ide-commit-header-height;
border-bottom: 1px solid $white-dark;
padding: 12px 0;
}
-.multi-file-commit-panel-header-title {
- display: flex;
- flex: 1;
- align-items: center;
-
- svg {
- margin-right: $gl-btn-padding;
- color: $theme-gray-700;
- }
-}
-
.multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark;
margin-left: auto;
@@ -594,8 +509,6 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
flex: 1;
overflow: auto;
padding: $grid-size 0;
- margin-left: -$grid-size;
- margin-right: -$grid-size;
min-height: 60px;
&.form-text.text-muted {
@@ -604,21 +517,6 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
}
}
-.ide-file-addition,
-.ide-file-addition-solid {
- color: $green-500;
-}
-
-.ide-file-modified,
-.ide-file-modified-solid {
- color: $orange-500;
-}
-
-.ide-file-deletion,
-.ide-file-deletion-solid {
- color: $red-500;
-}
-
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
@@ -638,8 +536,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
}
}
-.multi-file-commit-list-path,
-.ide-file-list .file {
+.multi-file-commit-list-path {
display: flex;
align-items: center;
margin-left: -$grid-size;
@@ -647,29 +544,31 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
padding: $grid-size / 2 $grid-size;
border-radius: $border-radius-default;
text-align: left;
+ cursor: pointer;
+ height: $ide-commit-row-height;
+ padding-right: 0;
&:hover,
&:focus {
background: $theme-gray-100;
+
+ outline: 0;
+
+ .multi-file-discard-btn {
+ > .btn {
+ display: flex;
+ }
+ }
}
&:active {
background: $theme-gray-200;
}
-}
-
-.multi-file-commit-list-path {
- cursor: pointer;
&.is-active {
background-color: $white-normal;
}
- &:hover,
- &:focus {
- outline: 0;
- }
-
svg {
min-width: 16px;
vertical-align: middle;
@@ -679,6 +578,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
.multi-file-commit-list-file-path {
@include str-truncated(calc(100% - 30px));
+ user-select: none;
&:active {
text-decoration: none;
@@ -686,9 +586,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
}
.multi-file-discard-btn {
- top: 4px;
- right: 8px;
- bottom: 4px;
+ > .btn {
+ display: none;
+ width: $ide-commit-row-height;
+ height: $ide-commit-row-height;
+ }
svg {
top: 0;
@@ -807,10 +709,9 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
}
.ide-staged-action-btn {
- width: 22px;
- margin-left: -1px;
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
+ width: $ide-commit-row-height;
+ height: $ide-commit-row-height;
+ color: inherit;
> svg {
top: 0;
@@ -1401,9 +1302,17 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
}
}
-.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle {
- color: $white-normal;
- background-color: $blue-500;
+.ide-new-btn {
+ display: none;
+
+ .btn {
+ padding: 2px 5px;
+ }
+
+ .dropdown.show .ide-entry-dropdown-toggle {
+ color: $white-normal;
+ background-color: $blue-500;
+ }
}
.ide-preview-header {
@@ -1442,3 +1351,46 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
top: 50%;
transform: translateY(-50%);
}
+
+.ide-file-templates {
+ padding: $grid-size $gl-padding;
+ background-color: $gray-light;
+ border-bottom: 1px solid $white-dark;
+
+ .dropdown {
+ min-width: 180px;
+ }
+
+ .dropdown-content {
+ max-height: 222px;
+ }
+}
+
+.ide-commit-editor-header {
+ height: 65px;
+ padding: 8px 16px;
+ background-color: $theme-gray-50;
+ box-shadow: inset 0 -1px $white-dark;
+}
+
+.ide-commit-list-changed-icon {
+ width: $ide-commit-row-height;
+ height: $ide-commit-row-height;
+}
+
+.ide-file-icon-holder {
+ display: flex;
+ align-items: center;
+ color: $theme-gray-700;
+}
+
+.file-row:hover,
+.file-row:focus {
+ .ide-new-btn {
+ display: block;
+ }
+
+ .folder-icon {
+ fill: $gl-text-color-secondary;
+ }
+}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/page_bundles/xterm.scss
index 7d40c61da26..7f040ac9b96 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/page_bundles/xterm.scss
@@ -1,3 +1,5 @@
+@import 'framework/variables';
+
.build-page {
// color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
// see also: https://gist.github.com/jasonm23/2868981
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 6c555aee20a..f0acb78f731 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -4,3 +4,7 @@
padding-bottom: 46px;
}
}
+
+.usage-data {
+ max-height: 400px;
+}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 69d7de886b4..b3c5c693824 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -136,15 +136,23 @@
right: 0;
bottom: 0;
left: 0;
+
+ button {
+ display: none;
+ }
}
.board-title {
padding: 0;
border-bottom: 0;
+ justify-content: center;
> span {
+ width: 100%;
+ margin-top: -12px;
display: block;
- transform: rotate(90deg) translate(35px, 10px);
+ transform: rotate(90deg) translate(35px, 0);
+ overflow: initial;
}
}
@@ -265,7 +273,7 @@
margin-bottom: 0;
padding: 5px;
list-style: none;
- overflow-y: scroll;
+ overflow-y: auto;
overflow-x: hidden;
}
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss
index 49fe50977f5..38fec3f0aa8 100644
--- a/app/assets/stylesheets/pages/branches.scss
+++ b/app/assets/stylesheets/pages/branches.scss
@@ -23,7 +23,7 @@
.bar {
position: absolute;
height: 4px;
- background-color: $divergence-graph-bar-bg;
+ background-color: $gl-gray-200;
}
.bar-behind {
@@ -61,7 +61,7 @@
height: 18px;
margin: 5px 0 0;
float: left;
- background-color: $divergence-graph-separator-bg;
+ background-color: $gl-gray-200;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 14ba8b1df83..ed877f625b5 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -328,23 +328,6 @@
}
}
- .build-dropdown {
- margin: $gl-padding 0;
- padding: 0;
-
- .dropdown-menu-toggle {
- margin-top: #{$gl-padding / 2};
- }
-
- svg {
- position: relative;
- top: 3px;
- margin-right: 3px;
- width: 14px;
- height: 14px;
- }
- }
-
.builds-container {
background-color: $white-light;
border-top: 1px solid $border-color;
@@ -381,15 +364,11 @@
position: absolute;
left: 15px;
top: 20px;
- display: none;
+ display: block;
}
&.active {
font-weight: $gl-font-weight-bold;
-
- .icon-arrow-right {
- display: block;
- }
}
&.retried {
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 0f22fe21143..71a3fd544f2 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -4,9 +4,60 @@
}
}
-.cluster-applications-table {
- // Wait for the Vue to kick-in and render the applications block
- min-height: 628px;
+.cluster-application-row {
+ background: $gray-lighter;
+
+ &.cluster-application-installed {
+ background: none;
+ }
+
+ .settings-message {
+ padding: $gl-vert-padding $gl-padding-8;
+ }
+}
+
+@media (min-width: map-get($grid-breakpoints, md)) {
+ .cluster-application-list {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ }
+
+ .cluster-application-row {
+ border-bottom: 1px solid $border-color;
+ padding: $gl-padding;
+ }
+}
+
+.cluster-application-logo {
+ border: 3px solid $white-light;
+ box-shadow: 0 0 0 1px $gray-normal;
+
+ &.avatar:hover {
+ border-color: $white-light;
+ }
+}
+
+.cluster-application-warning {
+ font-weight: bold;
+ text-align: center;
+ padding: $gl-padding;
+ border-bottom: 1px solid $white-normal;
+
+ .svg-container {
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: $gl-padding-8;
+ width: 40px;
+ height: 40px;
+ }
+}
+
+.cluster-application-description {
+ flex: 1;
+}
+
+.cluster-application-disabled {
+ opacity: 0.5;
}
.clusters-dropdown-menu {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 10764e0f3df..628a4ca38da 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -223,6 +223,7 @@
}
}
+.clipboard-group,
.commit-sha-group {
display: inline-flex;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 7d7143631f2..17b02c6e31e 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -31,7 +31,7 @@
.file-mode-changed {
padding: 10px;
- color: $file-mode-changed;
+ color: $gl-gray-500;
}
.suppressed-container {
@@ -72,6 +72,7 @@
.line_holder td {
line-height: $code-line-height;
font-size: $code-font-size;
+ vertical-align: top;
&.noteable_line {
position: relative;
@@ -244,7 +245,7 @@
.swipe-wrap {
overflow: hidden;
- border-left: 1px solid $diff-swipe-border;
+ border-left: 1px solid $gl-gray-400;
position: absolute;
display: block;
top: 13px;
@@ -570,8 +571,6 @@
}
.files {
- margin-top: 1px;
-
.diff-file:last-child {
margin-bottom: 0;
}
@@ -749,6 +748,10 @@
left: $gl-padding;
}
+ .dropdown-input .dropdown-input-search {
+ pointer-events: all;
+ }
+
.diff-changed-file {
display: flex;
padding-top: 8px;
@@ -982,3 +985,64 @@
.discussion-body .image .frame {
position: relative;
}
+
+.diff-tree-list {
+ width: 320px;
+}
+
+.diff-files-holder {
+ flex: 1;
+ min-width: 0;
+}
+
+.compare-versions-container {
+ min-width: 0;
+}
+
+.tree-list-holder {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 100px;
+ max-height: calc(100vh - 100px);
+ padding-right: $gl-padding;
+
+ .file-row {
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ .with-performance-bar & {
+ top: 135px;
+ }
+}
+
+.tree-list-scroll {
+ max-height: 100%;
+ padding-top: $grid-size;
+ padding-bottom: $grid-size;
+ border-top: 1px solid $border-color;
+ border-bottom: 1px solid $border-color;
+ overflow-y: scroll;
+ overflow-x: auto;
+}
+
+.tree-list-search .form-control {
+ padding-left: 30px;
+}
+
+.tree-list-icon {
+ top: 50%;
+ left: 10px;
+ transform: translateY(-50%);
+
+ &,
+ svg {
+ fill: $gl-text-color-tertiary;
+ }
+}
+
+.tree-list-clear-icon {
+ right: 10px;
+ left: auto;
+ line-height: 0;
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 196f6ae6d8c..79984c1a546 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -153,7 +153,7 @@
.x-axis path,
.y-axis path {
- stroke: $stat-graph-axis-fill;
+ stroke: $gl-gray-350;
}
.label-x-axis-line,
@@ -163,7 +163,7 @@
.y-axis {
line {
- stroke: $stat-graph-axis-fill;
+ stroke: $gl-gray-350;
stroke-width: 1;
}
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index da0c9b44498..a91d44805ee 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -87,7 +87,7 @@
border: 0;
background: $gray-light;
border-radius: 0;
- color: $events-pre-color;
+ color: $gl-gray-500;
overflow: hidden;
}
@@ -104,7 +104,7 @@
}
.event-note-icon {
- color: $events-pre-color;
+ color: $gl-gray-500;
float: left;
font-size: $gl-font-size;
line-height: 16px;
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index 22fce893fd7..4fb1a956fab 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -20,7 +20,7 @@
.graphs {
.graph-author-email {
float: right;
- color: $graph-author-email-color;
+ color: $gl-gray-500;
}
.graph-additions {
@@ -58,7 +58,7 @@
.y-axis-label {
line {
- stroke: $stat-graph-axis-fill;
+ stroke: $gl-gray-350;
}
text {
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 60b4d39bb1a..fe792a53b44 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -3,7 +3,6 @@
}
.dashboard .side .card .card-header .input-group {
-
.form-control {
height: 42px;
}
@@ -30,14 +29,15 @@
}
}
+.group-nav-container .group-search,
.group-nav-container .nav-controls {
display: flex;
align-items: flex-start;
- padding: $gl-padding-top 0;
- border-bottom: 1px solid $border-color;
+ padding: $gl-padding-top 0 0;
.group-filter-form {
- flex: 1;
+ flex: 1 1 auto;
+ margin-right: $gl-padding-8;
}
.dropdown-menu-right {
@@ -106,7 +106,7 @@
&,
.dropdown,
.dropdown .dropdown-toggle,
- .btn-new {
+ .btn-success {
display: block;
}
@@ -118,7 +118,7 @@
.group-filter-form,
.dropdown .dropdown-toggle,
- .btn-new {
+ .btn-success {
width: 100%;
}
@@ -136,6 +136,10 @@
flex: 1;
}
+ .dropdown-toggle {
+ width: auto;
+ }
+
.dropdown-menu {
width: 100%;
max-width: inherit;
@@ -145,38 +149,14 @@
}
}
-.groups-empty-state {
- padding: 50px 100px;
- overflow: hidden;
-
- @include media-breakpoint-down(sm) {
- padding: 50px 0;
- }
-
- svg {
- float: right;
-
- @include media-breakpoint-down(sm) {
- float: none;
- display: block;
- width: 250px;
- position: relative;
- left: 50%;
- margin-left: -125px;
- }
- }
-
- .text-content {
- float: left;
- width: 460px;
- margin-top: 120px;
+.group-nav-container .group-search {
+ padding: $gl-padding 0;
+ border-bottom: 1px solid $border-color;
+}
- @include media-breakpoint-down(sm) {
- float: none;
- margin-top: 60px;
- width: auto;
- text-align: center;
- }
+.groups-listing {
+ .group-list-tree .group-row:first-child {
+ border-top: 0;
}
}
@@ -278,12 +258,12 @@
}
&::after {
- content: "";
+ content: '';
position: absolute;
height: 100%;
width: 100%;
background-color: transparent;
- border: 2px outset $kdb-border;
+ border: 2px outset $gl-gray-200;
border-radius: 50%;
animation: spin-avatar 3s infinite linear;
}
@@ -346,7 +326,7 @@
position: relative;
&::before {
- content: "";
+ content: '';
display: block;
width: 10px;
height: 0;
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 0350fe5752e..2c23f31c240 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -1,6 +1,6 @@
.shortcut-mappings {
font-size: 12px;
- color: $help-shortcut-mapping-color;
+ color: $gl-gray-700;
tbody:first-child tr:first-child {
padding-top: 0;
@@ -22,7 +22,7 @@
.shortcut {
padding-right: 10px;
- color: $help-shortcut-color;
+ color: $gl-gray-400;
text-align: right;
white-space: nowrap;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 9ac47a771a5..62a9f97caa9 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -701,6 +701,11 @@
align-self: center;
overflow: hidden;
text-overflow: ellipsis;
+
+ .user-status-emoji {
+ margin-left: $gl-padding-4;
+ margin-right: 0;
+ }
}
.js-issuable-selector-wrap {
@@ -721,13 +726,13 @@
display: flex;
}
- .issue-info-container {
+ .issuable-info-container {
-webkit-flex: 1;
flex: 1;
display: flex;
padding-right: $gl-padding;
- .issue-main-info {
+ .issuable-main-info {
flex: 1 auto;
margin-right: 10px;
}
@@ -763,7 +768,7 @@
margin-bottom: 10px;
min-width: 15px;
- .selected_issue {
+ .selected-issuable {
vertical-align: text-top;
}
}
@@ -795,7 +800,7 @@
}
.issuable-list li,
-.issue-info-container .controls {
+.issuable-info-container .controls {
.avatar-counter {
display: inline-block;
vertical-align: middle;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index c9e5fb9c579..fa0ab1a3bae 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -100,6 +100,22 @@
p {
margin: 0;
}
+
+ .omniauth-btn {
+ margin-bottom: $gl-padding;
+ width: 48%;
+ padding: $gl-padding-8;
+
+ @include media-breakpoint-down(md) {
+ width: 100%;
+ }
+
+ img {
+ width: $default-icon-size;
+ height: $default-icon-size;
+ margin-right: $gl-padding;
+ }
+ }
}
.new-session-tabs {
@@ -169,10 +185,6 @@
}
}
- label {
- font-weight: $gl-font-weight-normal;
- }
-
.submit-container {
margin-top: 16px;
}
@@ -200,15 +212,6 @@
}
}
-.oauth-image-link {
- margin-right: 10px;
-
- img {
- width: 32px;
- height: 32px;
- }
-}
-
.devise-layout-html {
margin: 0;
padding: 0;
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 5fdb2b4a90a..99609a96976 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -4,7 +4,7 @@
}
.users-project-form {
- .btn-create {
+ .btn-success {
margin-right: 10px;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 7b8cad254c7..45382d4ea43 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -460,7 +460,7 @@
display: -webkit-flex;
display: flex;
- .issue-info-container {
+ .issuable-info-container {
-webkit-flex: 1;
flex: 1;
}
@@ -723,6 +723,17 @@
align-items: center;
padding: 16px;
z-index: 199;
+ white-space: nowrap;
+
+ .dropdown-menu-toggle {
+ width: auto;
+ max-width: 170px;
+
+ svg {
+ top: 10px;
+ right: 8px;
+ }
+ }
}
.content-block {
@@ -910,7 +921,7 @@
opacity: .65;
&:hover {
- color: $file-mode-changed;
+ color: $gl-gray-500;
text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index ac7b701c2e2..4268e194ed7 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -2,7 +2,7 @@
* Note Form
*/
.comment-btn {
- @extend .btn-create;
+ @extend .btn-success;
}
.diff-file .diff-content {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index dbe9f0c03fb..bfba1bf1b2b 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -94,8 +94,8 @@ ul.notes {
opacity: 0.5;
.dummy-avatar {
- background-color: $kdb-border;
- border: 1px solid darken($kdb-border, 25%);
+ background-color: $gl-gray-200;
+ border: 1px solid darken($gl-gray-200, 25%);
}
.note-headline-light,
@@ -334,20 +334,6 @@ ul.notes {
border: 1px solid $white-normal;
border-left: 0;
- &.notes_line {
- vertical-align: middle;
- text-align: center;
- padding: 10px 0;
- background: $gray-light;
- color: $text-color;
- }
-
- &.notes_line2 {
- text-align: center;
- padding: 10px 0;
- border-left: 1px solid $note-line2-border !important;
- }
-
&.notes_content {
background-color: $gray-light;
border-width: 1px 0;
@@ -357,6 +343,10 @@ ul.notes {
&.parallel {
border-width: 1px;
+
+ &.new {
+ border-right-width: 0;
+ }
}
.discussion-notes {
@@ -533,59 +523,7 @@ ul.notes {
}
.note-action-button {
- line-height: 1;
- padding: 0;
- min-width: 16px;
- color: $gray-darkest;
- fill: $gray-darkest;
-
- .fa {
- position: relative;
- font-size: 16px;
- }
-
- svg {
- @include btn-svg;
- margin: 0;
- }
-
- .award-control-icon-positive,
- .award-control-icon-super-positive {
- position: absolute;
- top: 0;
- left: 0;
- opacity: 0;
- }
-
- &:hover,
- &.is-active {
- .danger-highlight {
- color: $red-500;
- }
-
- .link-highlight {
- color: $blue-600;
- fill: $blue-600;
- }
-
- .award-control-icon-neutral {
- opacity: 0;
- }
-
- .award-control-icon-positive {
- opacity: 1;
- }
- }
-
- &.is-active {
- .award-control-icon-positive {
- opacity: 0;
- }
-
- .award-control-icon-super-positive {
- opacity: 1;
- }
- }
+ @include emoji-menu-toggle-button;
}
.discussion-toggle-button {
@@ -804,7 +742,7 @@ ul.notes {
padding-top: 0;
.discussion-wrapper {
- border-color: transparent;
+ border: 0;
}
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 8bb8b83dc5e..14395cc59b0 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -760,6 +760,7 @@
}
&.ci-status-icon-canceled,
+ &.ci-status-icon-scheduled,
&.ci-status-icon-disabled,
&.ci-status-icon-not-found,
&.ci-status-icon-manual {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 17f34319050..f084adaf5d3 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -81,14 +81,14 @@
// Middle dot divider between each element in a list of items.
.middle-dot-divider {
&::after {
- content: "\00B7"; // Middle Dot
+ content: '\00B7'; // Middle Dot
padding: 0 6px;
font-weight: $gl-font-weight-bold;
}
&:last-child {
&::after {
- content: "";
+ content: '';
padding: 0;
}
}
@@ -191,7 +191,6 @@
@include media-breakpoint-down(xs) {
width: auto;
}
-
}
.profile-crop-image-container {
@@ -215,7 +214,6 @@
}
}
-
.user-profile {
.cover-controls a {
margin-left: 5px;
@@ -279,6 +277,10 @@ table.u2f-registrations {
}
}
+.codes {
+ padding-top: 14px;
+}
+
.oauth-application-show {
.scope-name {
font-weight: $gl-font-weight-bold;
@@ -414,7 +416,7 @@ table.u2f-registrations {
}
&.unverified {
- @include status-color($gray-dark, color("gray"), $common-gray-dark);
+ @include status-color($gray-dark, color('gray'), $common-gray-dark);
}
}
}
@@ -427,7 +429,7 @@ table.u2f-registrations {
}
.emoji-menu-toggle-button {
- @extend .note-action-button;
+ @include emoji-menu-toggle-button;
.no-emoji-placeholder {
position: relative;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index a95e78931b1..da3d8aa53ad 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -115,7 +115,7 @@
.project-feature-controls {
display: flex;
align-items: center;
- margin: 8px 0;
+ margin: $gl-padding-8 0;
max-width: 432px;
.toggle-wrapper {
@@ -144,12 +144,8 @@
.group-home-panel {
padding-top: 24px;
padding-bottom: 24px;
+ border-bottom: 1px solid $border-color;
- @include media-breakpoint-up(sm) {
- border-bottom: 1px solid $border-color;
- }
-
- .project-avatar,
.group-avatar {
float: none;
margin: 0 auto;
@@ -175,7 +171,6 @@
}
}
- .project-home-desc,
.group-home-desc {
margin-left: auto;
margin-right: auto;
@@ -199,6 +194,62 @@
}
}
+.project-home-panel {
+ padding-top: $gl-padding-8;
+ padding-bottom: $gl-padding-24;
+
+ .project-title-row {
+ margin-right: $gl-padding-8;
+ }
+
+ .project-avatar {
+ width: $project-title-row-height;
+ height: $project-title-row-height;
+ flex-shrink: 0;
+ flex-basis: $project-title-row-height;
+ margin: 0 $gl-padding-8 0 0;
+ }
+
+ .project-title {
+ font-size: 20px;
+ line-height: $project-title-row-height;
+ font-weight: bold;
+ }
+
+ .project-metadata {
+ font-weight: normal;
+ font-size: 14px;
+ line-height: $gl-btn-line-height;
+ color: $gl-text-color-secondary;
+
+ .icon {
+ margin-right: $gl-padding-4;
+ font-size: 16px;
+ }
+
+ .project-visibility,
+ .project-license,
+ .project-tag-list {
+ margin-right: $gl-padding-8;
+ }
+
+ .project-license {
+ .btn {
+ line-height: 0;
+ border-width: 0;
+ }
+ }
+
+ .project-tag-list,
+ .project-license {
+ .icon {
+ position: relative;
+ top: 2px;
+ }
+ }
+ }
+}
+
.nav > .project-repo-buttons {
margin-top: 0;
}
@@ -206,8 +257,6 @@
.project-repo-buttons,
.group-buttons {
.btn {
- padding: 3px 10px;
-
&:last-child {
margin-left: 0;
}
@@ -222,11 +271,15 @@
.fa-caret-down {
margin-left: 3px;
+
+ &.dropdown-btn-icon {
+ margin-left: 0;
+ }
}
}
.project-action-button {
- margin: 15px 5px 0;
+ margin: $gl-padding $gl-padding-8 0 0;
vertical-align: top;
}
@@ -243,82 +296,45 @@
.count-buttons {
display: inline-block;
vertical-align: top;
- margin-top: 15px;
- }
+ margin-top: $gl-padding;
- .project-clone-holder {
- display: inline-block;
- margin: 15px 5px 0 0;
+ .count-badge {
+ height: $input-height;
- input {
- height: 28px;
+ .icon {
+ top: -1px;
+ }
}
- }
- .count-with-arrow {
- display: inline-block;
- position: relative;
- margin-left: 4px;
+ .count-badge-count,
+ .count-badge-button {
+ border: 1px solid $border-color;
+ line-height: 1;
+ }
- .arrow {
- &::before {
- content: '';
- display: inline-block;
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- top: 50%;
- left: 0;
- margin-top: -6px;
- border-width: 7px 5px 7px 0;
- border-right-color: $count-arrow-border;
- pointer-events: none;
- }
+ .count,
+ .count-badge-button {
+ color: $gl-text-color;
+ }
- &::after {
- content: '';
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- top: 50%;
- left: 1px;
- margin-top: -9px;
- border-width: 10px 7px 10px 0;
- border-right-color: $white-light;
- pointer-events: none;
- }
+ .count-badge-count {
+ padding: 0 12px;
+ border-right: 0;
+ border-radius: $border-radius-base 0 0 $border-radius-base;
+ background: $gray-light;
}
- .count {
- @include btn-white;
- display: inline-block;
- background: $white-light;
- border-radius: 2px;
- border-width: 1px;
- border-style: solid;
- font-size: 13px;
- font-weight: $gl-font-weight-bold;
- line-height: 13px;
- letter-spacing: 0.4px;
- padding: 6px 14px;
- text-align: center;
- vertical-align: middle;
- touch-action: manipulation;
- background-image: none;
- white-space: nowrap;
- margin: 0 10px 0 4px;
+ .count-badge-button {
+ border-radius: 0 $border-radius-base $border-radius-base 0;
+ }
+ }
- a {
- color: inherit;
- }
+ .project-clone-holder {
+ display: inline-block;
+ margin: $gl-padding $gl-padding-8 0 0;
- &:hover {
- background: $white-light;
- }
+ input {
+ height: $input-height;
}
}
@@ -333,6 +349,14 @@
min-width: 320px;
}
}
+
+ .mobile-git-clone {
+ margin-top: $gl-padding-8;
+
+ .dropdown-menu-inner-content {
+ @extend .monospace;
+ }
+ }
}
.split-one {
@@ -347,7 +371,7 @@
.save-project-loader {
margin-top: 50px;
margin-bottom: 50px;
- color: $save-project-loader-color;
+ color: $gl-gray-700;
}
.transfer-project .select2-container {
@@ -423,7 +447,7 @@
> li + li::before {
padding: 0 3px;
- color: $project-breadcrumb-color;
+ color: $gl-gray-400;
}
a {
@@ -511,7 +535,6 @@
.controls {
margin-left: auto;
}
-
}
.choose-template {
@@ -574,7 +597,7 @@
flex-wrap: wrap;
.btn {
- padding: 8px;
+ padding: $gl-padding-8;
margin-right: 10px;
}
@@ -651,7 +674,7 @@
left: -10px;
top: 50%;
z-index: 10;
- padding: 8px 0;
+ padding: $gl-padding-8 0;
text-align: center;
background-color: $white-light;
color: $gl-text-color-tertiary;
@@ -665,7 +688,7 @@
left: 50%;
top: 0;
transform: translateX(-50%);
- padding: 0 8px;
+ padding: 0 $gl-padding-8;
}
}
@@ -699,17 +722,51 @@
.project-stats {
font-size: 0;
text-align: center;
- max-width: 100%;
border-bottom: 1px solid $border-color;
- .nav {
- margin-top: $gl-padding-8;
- margin-bottom: $gl-padding-8;
+ .scrolling-tabs-container {
+ .scrolling-tabs {
+ margin-top: $gl-padding-8;
+ margin-bottom: $gl-padding-8;
+ flex-wrap: wrap;
+ border-bottom: 0;
+ }
+
+ .fade-left,
+ .fade-right {
+ top: 0;
+ height: 100%;
+ .fa {
+ top: 50%;
+ margin-top: -$gl-padding-8;
+ }
+ }
+
+ .nav {
+ flex-basis: 100%;
+
+ + .nav {
+ margin: $gl-padding-8 0;
+ }
+ }
+
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+
+ .nav {
+ flex-wrap: nowrap;
+ }
+
+ .nav:first-child {
+ margin-right: $gl-padding-8;
+ }
+ }
+ }
+
+ .nav {
> li {
display: inline-block;
- margin-top: $gl-padding-4;
- margin-bottom: $gl-padding-4;
&:not(:last-child) {
margin-right: $gl-padding;
@@ -732,13 +789,17 @@
font-size: $gl-font-size;
line-height: $gl-btn-line-height;
color: $gl-text-color-secondary;
+ white-space: nowrap;
}
.stat-link {
+ border-bottom: 0;
+
&:hover,
&:focus {
color: $gl-text-color;
text-decoration: underline;
+ border-bottom: 0;
}
}
@@ -769,6 +830,14 @@
}
}
+.repository-language-bar-tooltip-language {
+ font-weight: $gl-font-weight-bold;
+}
+
+.repository-language-bar-tooltip-share {
+ color: $theme-gray-400;
+}
+
pre.light-well {
border-color: $well-light-border;
}
@@ -868,7 +937,7 @@ pre.light-well {
}
.git-clone-holder {
- width: 380px;
+ width: 320px;
.btn-clipboard {
border: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 77119aea9e2..04151b1cd59 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -218,7 +218,7 @@ input[type='checkbox']:hover {
}
.btn-search,
- .btn-new {
+ .btn-success {
width: 100%;
margin-top: 5px;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index e351dd7c0bb..dbf8692d69b 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -106,7 +106,7 @@
.settings-list-icon {
color: $gl-text-color-secondary;
- font-size: $settings-icon-size;
+ font-size: $default-icon-size;
line-height: 42px;
}
@@ -249,7 +249,7 @@
}
.loading-metrics .metrics-load-spinner {
- color: $loading-color;
+ color: $gl-gray-700;
}
.metrics-list {
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 620297e589d..7d59dd3b5d1 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -27,6 +27,7 @@
&.ci-canceled,
&.ci-disabled,
+ &.ci-scheduled,
&.ci-manual {
color: $gl-text-color;
border-color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 5d3b7b21ce4..3fc37e20c36 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -143,7 +143,7 @@
border: 0;
background: $gray-light;
border-radius: 0;
- color: $todo-body-pre-color;
+ color: $gl-gray-500;
margin: 0 20px;
overflow: hidden;
}
@@ -205,7 +205,7 @@
.todo-body {
margin: 0;
- border-left: 2px solid $todo-body-border;
+ border-left: 2px solid $gl-gray-100;
padding-left: 10px;
}
}
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 48ac5b21db8..84c617c7ec0 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -6,7 +6,7 @@
.example {
padding: 15px;
- border: 1px dashed $ui-dev-kit-example-border;
+ border: 1px dashed $gl-gray-100;
margin-bottom: 15px;
&::before {
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 57d43beaf21..59fdbf31fe9 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -1,4 +1,5 @@
@import 'framework/variables';
+@import 'framework/variables_overrides';
@import 'peek/views/rblineprof';
#js-peek {
@@ -6,15 +7,15 @@
left: 0;
top: 0;
width: 100%;
- z-index: 1039;
+ z-index: #{$zindex-modal-backdrop + 1};
height: $performance-bar-height;
background: $black;
line-height: $performance-bar-height;
- color: $perf-bar-text;
+ color: $gl-gray-400;
select {
- color: $perf-bar-text;
+ color: $gl-gray-400;
width: 200px;
}
@@ -53,7 +54,7 @@
padding: 4px 6px;
font-family: Consolas, 'Liberation Mono', Courier, monospace;
line-height: 1;
- color: $perf-bar-bucket-color;
+ color: $gl-gray-200;
border-radius: 3px;
box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from,
inset 0 1px 2px $perf-bar-bucket-box-shadow-to;
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index ed13ead63f9..68e14f0c2e5 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AbuseReportsController < ApplicationController
before_action :set_user, only: [:new]
@@ -30,6 +32,7 @@ class AbuseReportsController < ApplicationController
))
end
+ # rubocop: disable CodeReuse/ActiveRecord
def set_user
@user = User.find_by(id: params[:user_id])
@@ -39,4 +42,5 @@ class AbuseReportsController < ApplicationController
redirect_to @user, alert: "Cannot create the abuse report. This user has been blocked."
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index dc9a6df5f75..d5537023b26 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -1,8 +1,12 @@
+# frozen_string_literal: true
+
class Admin::AbuseReportsController < Admin::ApplicationController
+ # rubocop: disable CodeReuse/ActiveRecord
def index
@abuse_reports = AbuseReport.order(id: :desc).page(params[:page])
@abuse_reports.includes(:reporter, :user)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def destroy
abuse_report = AbuseReport.find(params[:id])
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index 9aaec905734..fdd3b4126ff 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::AppearancesController < Admin::ApplicationController
before_action :set_appearance, except: :create
diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb
index a4648b33cfa..ef182b981f1 100644
--- a/app/controllers/admin/application_controller.rb
+++ b/app/controllers/admin/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Provides a base class for Admin controllers to subclass
#
# Automatically sets the layout and ensures an administrator is logged in
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 9723e400574..8040a14ef56 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -1,19 +1,58 @@
+# frozen_string_literal: true
+
class Admin::ApplicationSettingsController < Admin::ApplicationController
+ include InternalRedirect
before_action :set_application_setting
def show
end
+ def integrations
+ end
+
+ def repository
+ end
+
+ def templates
+ end
+
+ def ci_cd
+ end
+
+ def reporting
+ end
+
+ def metrics_and_profiling
+ end
+
+ def network
+ end
+
+ def geo
+ end
+
+ def preferences
+ end
+
def update
successful = ApplicationSettings::UpdateService
.new(@application_setting, current_user, application_setting_params)
.execute
- if successful
- redirect_to admin_application_settings_path,
- notice: 'Application settings saved successfully'
- else
- render :show
+ if recheck_user_consent?
+ session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent?
+ end
+
+ redirect_path = referer_path(request) || admin_application_settings_path
+
+ respond_to do |format|
+ if successful
+ format.json { head :ok }
+ format.html { redirect_to redirect_path, notice: 'Application settings saved successfully' }
+ else
+ format.json { head :bad_request }
+ format.html { render :show }
+ end
end
end
@@ -28,8 +67,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
end
- def reset_runners_token
+ def reset_registration_token
@application_setting.reset_runners_registration_token!
+
flash[:notice] = 'New runners registration token has been generated!'
redirect_to admin_runners_path
end
@@ -76,14 +116,20 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
)
end
+ def recheck_user_consent?
+ return false unless session[:ask_for_usage_stats_consent]
+ return false unless params[:application_setting]
+
+ params[:application_setting].key?(:usage_ping_enabled) || params[:application_setting].key?(:version_check_enabled)
+ end
+
def visible_application_setting_attributes
ApplicationSettingsHelper.visible_attributes + [
:domain_blacklist_file,
disabled_oauth_sign_in_sources: [],
import_sources: [],
repository_storages: [],
- restricted_visibility_levels: [],
- sidekiq_throttling_queues: []
+ restricted_visibility_levels: []
]
end
end
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 5be23c76a95..00d2cc01192 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -1,12 +1,16 @@
+# frozen_string_literal: true
+
class Admin::ApplicationsController < Admin::ApplicationController
include OauthApplications
before_action :set_application, only: [:show, :edit, :update, :destroy]
before_action :load_scopes, only: [:new, :create, :edit, :update]
+ # rubocop: disable CodeReuse/ActiveRecord
def index
@applications = Doorkeeper::Application.where("owner_id IS NULL")
end
+ # rubocop: enable CodeReuse/ActiveRecord
def show
end
@@ -45,9 +49,11 @@ class Admin::ApplicationsController < Admin::ApplicationController
private
+ # rubocop: disable CodeReuse/ActiveRecord
def set_application
@application = Doorkeeper::Application.where("owner_id IS NULL").find(params[:id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Only allow a trusted parameter "white list" through.
def application_params
diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb
index 5f90ad7137d..7701f2e645b 100644
--- a/app/controllers/admin/background_jobs_controller.rb
+++ b/app/controllers/admin/background_jobs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::BackgroundJobsController < Admin::ApplicationController
def show
ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index a9109a1d4d0..a91d9a534cd 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -1,12 +1,16 @@
+# frozen_string_literal: true
+
class Admin::BroadcastMessagesController < Admin::ApplicationController
include BroadcastMessagesHelper
before_action :finder, only: [:edit, :update, :destroy]
+ # rubocop: disable CodeReuse/ActiveRecord
def index
@broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page])
@broadcast_message = BroadcastMessage.new
end
+ # rubocop: enable CodeReuse/ActiveRecord
def edit
end
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 737942f3eb2..b5fb5511638 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,13 +1,17 @@
+# frozen_string_literal: true
+
class Admin::DashboardController < Admin::ApplicationController
include CountHelper
COUNTED_ITEMS = [Project, User, Group, ForkedProjectLink, Issue, MergeRequest,
Note, Snippet, Key, Milestone].freeze
+ # rubocop: disable CodeReuse/ActiveRecord
def index
@counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
index 5c2025c1988..49ce275ad14 100644
--- a/app/controllers/admin/deploy_keys_controller.rb
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::DeployKeysController < Admin::ApplicationController
before_action :deploy_keys, only: [:index]
before_action :deploy_key, only: [:destroy, :edit, :update]
diff --git a/app/controllers/admin/gitaly_servers_controller.rb b/app/controllers/admin/gitaly_servers_controller.rb
index 11c4dfe3d8d..0a5566bfe70 100644
--- a/app/controllers/admin/gitaly_servers_controller.rb
+++ b/app/controllers/admin/gitaly_servers_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::GitalyServersController < Admin::ApplicationController
def index
@gitaly_servers = Gitaly::Server.all
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index d7a5b745d3f..46e85e1424f 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::GroupsController < Admin::ApplicationController
include MembersPresentation
@@ -10,6 +12,7 @@ class Admin::GroupsController < Admin::ApplicationController
@groups = @groups.page(params[:page])
end
+ # rubocop: disable CodeReuse/ActiveRecord
def show
@group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id])
@members = present_members(
@@ -18,6 +21,7 @@ class Admin::GroupsController < Admin::ApplicationController
AccessRequestsFinder.new(@group).execute(current_user))
@projects = @group.projects.with_statistics.page(params[:projects_page])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def new
@group = Group.new
diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb
index 61247b280b3..44864f9c7d0 100644
--- a/app/controllers/admin/health_check_controller.rb
+++ b/app/controllers/admin/health_check_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::HealthCheckController < Admin::ApplicationController
def show
@errors = HealthCheck::Utils.process_checks(['standard'])
diff --git a/app/controllers/admin/hook_logs_controller.rb b/app/controllers/admin/hook_logs_controller.rb
index 3017f96c26f..8301b3aa880 100644
--- a/app/controllers/admin/hook_logs_controller.rb
+++ b/app/controllers/admin/hook_logs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::HookLogsController < Admin::ApplicationController
include HooksExecution
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index a98c355c7ba..d0abdec50ae 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::HooksController < Admin::ApplicationController
include HooksExecution
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index ceb45865804..b51c2f678ca 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::IdentitiesController < Admin::ApplicationController
before_action :user
before_action :identity, except: [:index, :new, :create]
@@ -44,9 +46,11 @@ class Admin::IdentitiesController < Admin::ApplicationController
protected
+ # rubocop: disable CodeReuse/ActiveRecord
def user
@user ||= User.find_by!(username: params[:user_id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def identity
@identity ||= user.identities.find(params[:id])
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index a7b562b1d8e..f5825ecb19a 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::ImpersonationTokensController < Admin::ApplicationController
before_action :user
@@ -30,9 +32,11 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
private
+ # rubocop: disable CodeReuse/ActiveRecord
def user
@user ||= User.find_by!(username: params[:user_id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def finder(options = {})
PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
@@ -42,6 +46,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: [])
end
+ # rubocop: disable CodeReuse/ActiveRecord
def set_index_vars
@scopes = Gitlab::Auth.available_scopes(current_user)
@@ -49,4 +54,5 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
@inactive_impersonation_tokens = finder(state: 'inactive').execute
@active_impersonation_tokens = finder(state: 'active').execute.order(:expires_at)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb
index d2f947d2c66..08d7e3b4fa2 100644
--- a/app/controllers/admin/impersonations_controller.rb
+++ b/app/controllers/admin/impersonations_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::ImpersonationsController < Admin::ApplicationController
skip_before_action :authenticate_admin!
before_action :authenticate_impersonator!
diff --git a/app/controllers/admin/jobs_controller.rb b/app/controllers/admin/jobs_controller.rb
index e355d5fdea7..0c1afdc3d3b 100644
--- a/app/controllers/admin/jobs_controller.rb
+++ b/app/controllers/admin/jobs_controller.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class Admin::JobsController < Admin::ApplicationController
+ # rubocop: disable CodeReuse/ActiveRecord
def index
@scope = params[:scope]
@all_builds = Ci::Build
@@ -16,6 +19,7 @@ class Admin::JobsController < Admin::ApplicationController
end
@builds = @builds.page(params[:page]).per(30)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def cancel_all
Ci::Build.running_or_pending.each(&:cancel)
diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb
index 0b76193a90e..4e9262ccc96 100644
--- a/app/controllers/admin/keys_controller.rb
+++ b/app/controllers/admin/keys_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::KeysController < Admin::ApplicationController
before_action :user, only: [:show, :destroy]
@@ -24,9 +26,11 @@ class Admin::KeysController < Admin::ApplicationController
protected
+ # rubocop: disable CodeReuse/ActiveRecord
def user
@user ||= User.find_by!(username: params[:user_id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def key_params
params.require(:user_id, :id)
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index 7eb8f758807..aa5eae7a474 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::LabelsController < Admin::ApplicationController
before_action :set_label, only: [:show, :edit, :update, :destroy]
diff --git a/app/controllers/admin/logs_controller.rb b/app/controllers/admin/logs_controller.rb
index 12a27cede75..06b0e6a15a3 100644
--- a/app/controllers/admin/logs_controller.rb
+++ b/app/controllers/admin/logs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::LogsController < Admin::ApplicationController
before_action :loggers
@@ -12,7 +14,8 @@ class Admin::LogsController < Admin::ApplicationController
Gitlab::GitLogger,
Gitlab::EnvironmentLogger,
Gitlab::SidekiqLogger,
- Gitlab::RepositoryCheckLogger
+ Gitlab::RepositoryCheckLogger,
+ Gitlab::ProjectServiceLogger
]
end
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 3afe66c3566..550f29a58d2 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::ProjectsController < Admin::ApplicationController
include MembersPresentation
@@ -19,6 +21,7 @@ class Admin::ProjectsController < Admin::ApplicationController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def show
if @group
@group_members = present_members(
@@ -30,7 +33,9 @@ class Admin::ProjectsController < Admin::ApplicationController
@requesters = present_members(
AccessRequestsFinder.new(@project).execute(current_user))
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def transfer
namespace = Namespace.find_by(id: params[:new_namespace_id])
::Projects::TransferService.new(@project, current_user, params.dup).execute(namespace)
@@ -38,6 +43,7 @@ class Admin::ProjectsController < Admin::ApplicationController
@project.reload
redirect_to admin_project_path(@project)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def repository_check
RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id)
diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb
index a478176e138..64d74ae4231 100644
--- a/app/controllers/admin/requests_profiles_controller.rb
+++ b/app/controllers/admin/requests_profiles_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::RequestsProfilesController < Admin::ApplicationController
def index
@profile_token = Gitlab::RequestProfiler.profile_token
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index 51d5799cd89..774ce04d079 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::RunnerProjectsController < Admin::ApplicationController
before_action :project, only: [:create]
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 6c76c55a9d4..0b6ff491c66 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -1,12 +1,13 @@
+# frozen_string_literal: true
+
class Admin::RunnersController < Admin::ApplicationController
before_action :runner, except: :index
def index
- sort = params[:sort] == 'contacted_asc' ? { contacted_at: :asc } : { id: :desc }
- @runners = Ci::Runner.order(sort)
- @runners = @runners.search(params[:search]) if params[:search].present?
- @runners = @runners.page(params[:page]).per(30)
- @active_runners_cnt = Ci::Runner.online.count
+ finder = Admin::RunnersFinder.new(params: params)
+ @runners = finder.execute
+ @active_runners_count = Ci::Runner.online.count
+ @sort = finder.sort_key
end
def show
@@ -57,6 +58,7 @@ class Admin::RunnersController < Admin::ApplicationController
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def assign_builds_and_projects
@builds = runner.builds.order('id DESC').first(30)
@projects =
@@ -69,4 +71,5 @@ class Admin::RunnersController < Admin::ApplicationController
@projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any?
@projects = @projects.page(params[:page]).per(30)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 91a36af34f3..c455930c044 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -30,16 +30,20 @@ class Admin::ServicesController < Admin::ApplicationController
private
+ # rubocop: disable CodeReuse/ActiveRecord
def services_templates
Service.available_services_names.map do |service_name|
service_template = "#{service_name}_service".camelize.constantize
service_template.where(template: true).first_or_create
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def service
@service ||= Service.where(id: params[:id], template: true).first
end
+ # rubocop: enable CodeReuse/ActiveRecord
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42430')
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index d52d67a67a5..18d22c95b61 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
class Admin::SpamLogsController < Admin::ApplicationController
+ # rubocop: disable CodeReuse/ActiveRecord
def index
@spam_logs = SpamLog.order(id: :desc).page(params[:page])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def destroy
spam_log = SpamLog.find(params[:id])
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index 99039724521..244fc2b31bb 100644
--- a/app/controllers/admin/system_info_controller.rb
+++ b/app/controllers/admin/system_info_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::SystemInfoController < Admin::ApplicationController
EXCLUDED_MOUNT_OPTIONS = [
'nobrowse',
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index a51a8c3ed4a..b783c0e2a6f 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::UsersController < Admin::ApplicationController
before_action :user, except: [:index, :new, :create]
@@ -174,9 +176,11 @@ class Admin::UsersController < Admin::ApplicationController
user == current_user
end
+ # rubocop: disable CodeReuse/ActiveRecord
def user
@user ||= User.find_by!(username: params[:id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def redirect_back_or_admin_user(options = {})
redirect_back_or_default(default: default_route, options: options)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index e5b38898a67..d7dbc712743 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'gon'
require 'fogbugz'
@@ -10,6 +12,7 @@ class ApplicationController < ActionController::Base
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
+ include InvalidUTF8ErrorHandler
before_action :authenticate_sessionless_user!
before_action :authenticate_user!
@@ -22,6 +25,7 @@ class ApplicationController < ActionController::Base
before_action :add_gon_variables, unless: [:peek_request?, :json_request?]
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
+ before_action :set_usage_stats_consent_flag
around_action :set_locale
@@ -105,11 +109,21 @@ class ApplicationController < ActionController::Base
request.env['rack.session.options'][:expire_after] = Settings.gitlab['unauthenticated_session_expire_delay']
end
+ def render(*args)
+ super.tap do
+ # Set a header for custom error pages to prevent them from being intercepted by gitlab-workhorse
+ if response.content_type == 'text/html' && (400..599).cover?(response.status)
+ response.headers['X-GitLab-Custom-Error'] = '1'
+ end
+ end
+ end
+
protected
def append_info_to_payload(payload)
super
+ payload[:ua] = request.env["HTTP_USER_AGENT"]
payload[:remote_ip] = request.remote_ip
logged_user = auth_user
@@ -268,9 +282,10 @@ class ApplicationController < ActionController::Base
end
def event_filter
- # Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
- filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present?
- @event_filter ||= EventFilter.new(filters)
+ @event_filter ||=
+ EventFilter.new(params[:event_filter].presence || cookies[:event_filter]).tap do |new_event_filter|
+ cookies[:event_filter] = new_event_filter.filter
+ end
end
# JSON for infinite scroll via Pager object
@@ -433,4 +448,29 @@ class ApplicationController < ActionController::Base
!(peek_request? || devise_controller?)
end
+
+ def set_usage_stats_consent_flag
+ return unless current_user
+ return if sessionless_user?
+ return if session.has_key?(:ask_for_usage_stats_consent)
+
+ session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent?
+
+ if session[:ask_for_usage_stats_consent]
+ disable_usage_stats
+ end
+ end
+
+ def disable_usage_stats
+ application_setting_params = {
+ usage_ping_enabled: false,
+ version_check_enabled: false,
+ skip_usage_stats_user: true
+ }
+ settings = Gitlab::CurrentSettings.current_application_settings
+
+ ApplicationSettings::UpdateService
+ .new(settings, current_user, application_setting_params)
+ .execute
+ end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 9e30b982b06..3766b64a091 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users, :award_emojis]
diff --git a/app/controllers/boards/application_controller.rb b/app/controllers/boards/application_controller.rb
index b2675025fc0..eab908ba5ed 100644
--- a/app/controllers/boards/application_controller.rb
+++ b/app/controllers/boards/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Boards
class ApplicationController < ::ApplicationController
respond_to :json
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 7dd19f87ef5..4f3d737e3ce 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Boards
class IssuesController < Boards::ApplicationController
include BoardsResponses
@@ -11,6 +13,7 @@ module Boards
before_action :authorize_update_issue, only: [:update]
skip_before_action :authenticate_user!, only: [:index]
+ # rubocop: disable CodeReuse/ActiveRecord
def index
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
issues = list_service.execute
@@ -25,6 +28,7 @@ module Boards
render_issues(issues, list_service.metadata)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def create
service = Boards::Issues::CreateService.new(board_parent, project, current_user, issue_params)
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index e8b5934f2a9..ccd02144671 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Boards
class ListsController < Boards::ApplicationController
include BoardsResponses
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index 738a6a5173e..99ce24bd435 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Ci
class LintsController < ::ApplicationController
before_action :authenticate_user!
diff --git a/app/controllers/concerns/accepts_pending_invitations.rb b/app/controllers/concerns/accepts_pending_invitations.rb
index 6e8aef52b52..cb66c1a055d 100644
--- a/app/controllers/concerns/accepts_pending_invitations.rb
+++ b/app/controllers/concerns/accepts_pending_invitations.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AcceptsPendingInvitations
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index dfa1da7872c..5507328f8ae 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# == AuthenticatesWithTwoFactor
#
# Controller concern to handle two-factor authentication
@@ -88,6 +90,7 @@ module AuthenticatesWithTwoFactor
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
+ # rubocop: disable CodeReuse/ActiveRecord
def setup_u2f_authentication(user)
key_handles = user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
@@ -99,4 +102,5 @@ module AuthenticatesWithTwoFactor
sign_requests: sign_requests })
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
index da830ec2cb1..b7e4f9b81f1 100644
--- a/app/controllers/concerns/boards_responses.rb
+++ b/app/controllers/concerns/boards_responses.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BoardsResponses
include Gitlab::Utils::StrongMemoize
diff --git a/app/controllers/concerns/checks_collaboration.rb b/app/controllers/concerns/checks_collaboration.rb
index 81367663a06..1fa82f7dcd4 100644
--- a/app/controllers/concerns/checks_collaboration.rb
+++ b/app/controllers/concerns/checks_collaboration.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ChecksCollaboration
def can_collaborate_with_project?(project, ref: nil)
return true if can?(current_user, :push_code, project)
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
index 8b7355974df..f0e6adf4dec 100644
--- a/app/controllers/concerns/continue_params.rb
+++ b/app/controllers/concerns/continue_params.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ContinueParams
include InternalRedirect
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/controller_with_cross_project_access_check.rb b/app/controllers/concerns/controller_with_cross_project_access_check.rb
index a45c3384578..3f72f092683 100644
--- a/app/controllers/concerns/controller_with_cross_project_access_check.rb
+++ b/app/controllers/concerns/controller_with_cross_project_access_check.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ControllerWithCrossProjectAccessCheck
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index b26a76d2b62..b3777fd2b0f 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module CreatesCommit
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
@@ -65,7 +67,7 @@ module CreatesCommit
flash[:notice] = nil
else
target = different_project? ? "project" : "branch"
- flash[:notice] << " You can now submit a merge request to get this change into the original #{target}."
+ flash[:notice] = flash[:notice] + " You can now submit a merge request to get this change into the original #{target}."
end
end
end
@@ -99,6 +101,7 @@ module CreatesCommit
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
+ # rubocop: disable CodeReuse/ActiveRecord
def merge_request_exists?
strong_memoize(:merge_request) do
MergeRequestsFinder.new(current_user, project_id: @project.id)
@@ -110,6 +113,7 @@ module CreatesCommit
target_branch: @start_branch)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def different_project?
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
index 1ab107168c0..c1ef848e1e7 100644
--- a/app/controllers/concerns/cycle_analytics_params.rb
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module CycleAnalyticsParams
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb
index d5388c4cd20..6be7a2a18a2 100644
--- a/app/controllers/concerns/diff_for_path.rb
+++ b/app/controllers/concerns/diff_for_path.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DiffForPath
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/enforces_two_factor_authentication.rb b/app/controllers/concerns/enforces_two_factor_authentication.rb
index 997af4ab9e9..71bdef8ce03 100644
--- a/app/controllers/concerns/enforces_two_factor_authentication.rb
+++ b/app/controllers/concerns/enforces_two_factor_authentication.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# == EnforcesTwoFactorAuthentication
#
# Controller concern to enforce two-factor authentication requirements
@@ -24,6 +26,7 @@ module EnforcesTwoFactorAuthentication
current_user.try(:require_two_factor_authentication_from_group?)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def two_factor_authentication_reason(global: -> {}, group: -> {})
if two_factor_authentication_required?
if Gitlab::CurrentSettings.require_two_factor_authentication?
@@ -34,6 +37,7 @@ module EnforcesTwoFactorAuthentication
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def two_factor_grace_period
periods = [Gitlab::CurrentSettings.two_factor_grace_period]
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index 6ec6897e707..4f56346832c 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -1,5 +1,8 @@
+# frozen_string_literal: true
+
module GroupTree
# rubocop:disable Gitlab/ModuleWithInstanceVariables
+ # rubocop: disable CodeReuse/ActiveRecord
def render_group_tree(groups)
groups = groups.sort_by_attribute(@sort = params[:sort])
@@ -23,7 +26,9 @@ module GroupTree
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def filtered_groups_with_ancestors(groups)
filtered_groups = groups.search(params[:filter]).page(params[:page])
@@ -40,4 +45,5 @@ module GroupTree
filtered_groups
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/concerns/hooks_execution.rb b/app/controllers/concerns/hooks_execution.rb
index a22e46b4860..e8add1f4055 100644
--- a/app/controllers/concerns/hooks_execution.rb
+++ b/app/controllers/concerns/hooks_execution.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module HooksExecution
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb
index 10b9852e329..6785e6972d0 100644
--- a/app/controllers/concerns/internal_redirect.rb
+++ b/app/controllers/concerns/internal_redirect.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module InternalRedirect
extend ActiveSupport::Concern
@@ -36,4 +38,10 @@ module InternalRedirect
path_with_query = [uri.path, uri.query].compact.join('?')
[path_with_query, uri.fragment].compact.join("#")
end
+
+ def referer_path(request)
+ return unless request.referer.presence
+
+ URI(request.referer).path
+ end
end
diff --git a/app/controllers/concerns/invalid_utf8_error_handler.rb b/app/controllers/concerns/invalid_utf8_error_handler.rb
new file mode 100644
index 00000000000..44c6d6b0da0
--- /dev/null
+++ b/app/controllers/concerns/invalid_utf8_error_handler.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module InvalidUTF8ErrorHandler
+ extend ActiveSupport::Concern
+
+ included do
+ rescue_from ArgumentError, with: :handle_invalid_utf8
+ end
+
+ private
+
+ def handle_invalid_utf8(error)
+ if error.message == "invalid byte sequence in UTF-8"
+ render_412
+ else
+ raise(error)
+ end
+ end
+
+ def render_412
+ respond_to do |format|
+ format.html { render "errors/precondition_failed", layout: "errors", status: 412 }
+ format.js { render json: { error: 'Invalid UTF-8' }, status: :precondition_failed, content_type: 'application/json' }
+ format.any { head :precondition_failed }
+ end
+ end
+end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 37e03d70b6f..07e01e903ea 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module IssuableActions
extend ActiveSupport::Concern
@@ -89,12 +91,14 @@ module IssuableActions
render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
end
+ # rubocop: disable CodeReuse/ActiveRecord
def discussions
notes = issuable.discussion_notes
.inc_relations_for_view
.includes(:noteable)
.fresh
+ notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
@@ -102,6 +106,7 @@ module IssuableActions
render json: discussion_serializer.represent(discussions, context: self)
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 22b39f47bf0..5217b4be928 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -1,5 +1,8 @@
+# frozen_string_literal: true
+
module IssuableCollections
extend ActiveSupport::Concern
+ include CookiesHelper
include SortingHelper
include Gitlab::IssuableMetadata
include Gitlab::Utils::StrongMemoize
@@ -47,9 +50,11 @@ module IssuableCollections
false
end
+ # rubocop: disable CodeReuse/ActiveRecord
def issuables_collection
finder.execute.preload(preload_for_collection)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def redirect_out_of_range(total_pages)
return false if total_pages.nil? || total_pages.zero?
@@ -80,6 +85,7 @@ module IssuableCollections
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
+ # rubocop: disable CodeReuse/ActiveRecord
def filter_params
set_sort_order_from_cookie
set_default_state
@@ -100,6 +106,7 @@ module IssuableCollections
@filter_params.permit(finder_type.valid_params)
end
+ # rubocop: enable CodeReuse/ActiveRecord
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def set_default_state
@@ -107,11 +114,14 @@ module IssuableCollections
end
def set_sort_order_from_cookie
- cookies[remember_sorting_key] = params[:sort] if params[:sort].present?
+ sort_param = params[:sort] if params[:sort].present?
# fallback to legacy cookie value for backward compatibility
- cookies[remember_sorting_key] ||= cookies['issuable_sort']
- cookies[remember_sorting_key] = update_cookie_value(cookies[remember_sorting_key])
- params[:sort] = cookies[remember_sorting_key]
+ sort_param ||= cookies['issuable_sort']
+ sort_param ||= cookies[remember_sorting_key]
+
+ sort_value = update_cookie_value(sort_param)
+ set_secure_cookie(remember_sorting_key, sort_value)
+ params[:sort] = sort_value
end
def remember_sorting_key
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index 9d58656773d..a75590457d6 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module IssuesAction
extend ActiveSupport::Concern
include IssuableCollections
diff --git a/app/controllers/concerns/issues_calendar.rb b/app/controllers/concerns/issues_calendar.rb
index 671a204621d..1fdfde4c869 100644
--- a/app/controllers/concerns/issues_calendar.rb
+++ b/app/controllers/concerns/issues_calendar.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
module IssuesCalendar
extend ActiveSupport::Concern
# rubocop:disable Gitlab/ModuleWithInstanceVariables
+ # rubocop: disable CodeReuse/ActiveRecord
def render_issues_calendar(issuables)
@issues = issuables
.non_archived
@@ -20,5 +23,6 @@ module IssuesCalendar
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/labels_as_hash.rb b/app/controllers/concerns/labels_as_hash.rb
new file mode 100644
index 00000000000..1171aa9cf44
--- /dev/null
+++ b/app/controllers/concerns/labels_as_hash.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module LabelsAsHash
+ extend ActiveSupport::Concern
+
+ def labels_as_hash(target = nil, params = {})
+ available_labels = LabelsFinder.new(
+ current_user,
+ params
+ ).execute
+
+ label_hashes = available_labels.as_json(only: [:title, :color])
+
+ if target&.respond_to?(:labels)
+ already_set_labels = available_labels & target.labels
+ if already_set_labels.present?
+ titles = already_set_labels.map(&:title)
+ label_hashes.each do |hash|
+ if titles.include?(hash['title'])
+ hash[:set] = true
+ end
+ end
+ end
+ end
+
+ label_hashes
+ end
+end
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 4584ff782a3..9576eb14fdd 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This concern assumes:
# - a `#project` accessor
# - a `#user` accessor
diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb
index 215e0bdf3cb..c6c3598a976 100644
--- a/app/controllers/concerns/members_presentation.rb
+++ b/app/controllers/concerns/members_presentation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MembersPresentation
extend ActiveSupport::Concern
@@ -10,10 +12,12 @@ module MembersPresentation
).fabricate!
end
+ # rubocop: disable CodeReuse/ActiveRecord
def preload_associations(members)
ActiveRecord::Associations::Preloader.new.preload(members, :user)
ActiveRecord::Associations::Preloader.new.preload(members, :source)
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status)
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 409e6d4c4d2..ca713192c9e 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MembershipActions
include MembersPresentation
extend ActiveSupport::Concern
@@ -57,6 +59,7 @@ module MembershipActions
redirect_to members_page_url
end
+ # rubocop: disable CodeReuse/ActiveRecord
def leave
member = membershipable.members_and_requesters.find_by!(user_id: current_user.id)
Members::DestroyService.new(current_user).execute(member)
@@ -77,6 +80,7 @@ module MembershipActions
format.json { render json: { notice: notice } }
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def resend_invite
member = membershipable.members.find(params[:id])
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index b70db99b157..285f2c3a8a0 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MergeRequestsAction
extend ActiveSupport::Concern
include IssuableCollections
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index d92cf8b4894..eccbe35577b 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MilestoneActions
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 5127db3f5fb..3a45d6205ab 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module NotesActions
include RendersNotes
include Gitlab::Utils::StrongMemoize
@@ -18,6 +20,7 @@ module NotesActions
notes = notes_finder.execute
.inc_relations_for_view
+ notes = ResourceEvents::MergeIntoNotesService.new(noteable, current_user, last_fetched_at: current_fetched_at).execute(notes)
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
@@ -40,12 +43,26 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute
- if @note.is_a?(Note)
- prepare_notes_for_rendering([@note], noteable)
- end
-
respond_to do |format|
- format.json { render json: note_json(@note) }
+ format.json do
+ json = {
+ commands_changes: @note.commands_changes
+ }
+
+ if @note.persisted? && return_discussion?
+ json[:valid] = true
+
+ discussion = @note.discussion
+ prepare_notes_for_rendering(discussion.notes)
+ json[:discussion] = discussion_serializer.represent(discussion, context: self)
+ else
+ prepare_notes_for_rendering([@note])
+
+ json.merge!(note_json(@note))
+ end
+
+ render json: json
+ end
format.html { redirect_back_or_default }
end
end
@@ -54,10 +71,7 @@ module NotesActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
-
- if @note.is_a?(Note)
- prepare_notes_for_rendering([@note])
- end
+ prepare_notes_for_rendering([@note])
respond_to do |format|
format.json { render json: note_json(@note) }
@@ -88,14 +102,17 @@ module NotesActions
end
def note_json(note)
- attrs = {
- commands_changes: note.commands_changes
- }
+ attrs = {}
if note.persisted?
attrs[:valid] = true
- if use_note_serializer?
+ if return_discussion?
+ discussion = note.discussion
+ prepare_notes_for_rendering(discussion.notes)
+
+ attrs[:discussion] = discussion_serializer.represent(discussion, context: self)
+ elsif use_note_serializer?
attrs.merge!(note_serializer.represent(note))
else
attrs.merge!(
@@ -215,6 +232,10 @@ module NotesActions
ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
end
+ def discussion_serializer
+ DiscussionSerializer.new(project: project, noteable: noteable, current_user: current_user, note_entity: ProjectNoteEntity)
+ end
+
def note_project
strong_memoize(:note_project) do
next nil unless project
@@ -234,6 +255,10 @@ module NotesActions
end
end
+ def return_discussion?
+ Gitlab::Utils.to_boolean(params[:return_discussion])
+ end
+
def use_note_serializer?
return false if params['html']
diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb
index f0a68f23566..d97e22df472 100644
--- a/app/controllers/concerns/oauth_applications.rb
+++ b/app/controllers/concerns/oauth_applications.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module OauthApplications
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/params_backward_compatibility.rb b/app/controllers/concerns/params_backward_compatibility.rb
index b0e3d9c7b34..c972d6e3161 100644
--- a/app/controllers/concerns/params_backward_compatibility.rb
+++ b/app/controllers/concerns/params_backward_compatibility.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ParamsBackwardCompatibility
private
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index 99123fcb3b0..c61b9fabe9e 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module PreviewMarkdown
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb
index ba7adcfea86..b8026c7a01d 100644
--- a/app/controllers/concerns/renders_blob.rb
+++ b/app/controllers/concerns/renders_blob.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RendersBlob
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb
index b1c9b1e532f..f48e0586211 100644
--- a/app/controllers/concerns/renders_commits.rb
+++ b/app/controllers/concerns/renders_commits.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RendersCommits
def limited_commits(commits)
if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
diff --git a/app/controllers/concerns/renders_member_access.rb b/app/controllers/concerns/renders_member_access.rb
index d640378c24d..955ac1a1bc8 100644
--- a/app/controllers/concerns/renders_member_access.rb
+++ b/app/controllers/concerns/renders_member_access.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RendersMemberAccess
def prepare_groups_for_rendering(groups)
preload_max_member_access_for_collection(Group, groups)
@@ -13,6 +15,7 @@ module RendersMemberAccess
private
+ # rubocop: disable CodeReuse/ActiveRecord
def preload_max_member_access_for_collection(klass, collection)
return if !current_user || collection.blank?
@@ -20,4 +23,5 @@ module RendersMemberAccess
current_user.public_send(method_name, collection.ids) # rubocop:disable GitlabSecurity/PublicSend
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index cf04023080a..ce36da6b715 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RendersNotes
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def prepare_notes_for_rendering(notes, noteable = nil)
@@ -20,9 +22,11 @@ module RendersNotes
project.team.max_member_access_for_user_ids(user_ids)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def preload_noteable_for_regular_notes(notes)
ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def preload_first_time_contribution_for_authors(noteable, notes)
return unless noteable.is_a?(Issuable) && noteable.first_contribution?
@@ -30,7 +34,9 @@ module RendersNotes
notes.each {|n| n.specialize_for_first_contribution!(noteable)}
end
+ # rubocop: disable CodeReuse/ActiveRecord
def preload_author_status(notes)
ActiveRecord::Associations::Preloader.new.preload(notes, { author: :status })
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/concerns/repository_settings_redirect.rb b/app/controllers/concerns/repository_settings_redirect.rb
index f3db3cd563b..0f18735c29e 100644
--- a/app/controllers/concerns/repository_settings_redirect.rb
+++ b/app/controllers/concerns/repository_settings_redirect.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RepositorySettingsRedirect
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
index 88d1b34bb06..426f224d26b 100644
--- a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
+++ b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RequiresWhitelistedMonitoringClient
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb
index 0931bdf4c04..88939b002b2 100644
--- a/app/controllers/concerns/routable_actions.rb
+++ b/app/controllers/concerns/routable_actions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RoutableActions
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
index 237c93daee8..0bb7b7efed0 100644
--- a/app/controllers/concerns/send_file_upload.rb
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -1,7 +1,13 @@
+# frozen_string_literal: true
+
module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment')
if attachment
- redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" }
+ # Response-Content-Type will not override an existing Content-Type in
+ # Google Cloud Storage, so the metadata needs to be cleared on GCS for
+ # this to work. However, this override works with AWS.
+ redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}",
+ "response-content-type" => guess_content_type(attachment) }
# By default, Rails will send uploads with an extension of .js with a
# content-type of text/javascript, which will trigger Rails'
# cross-origin JavaScript protection.
@@ -18,4 +24,14 @@ module SendFileUpload
redirect_to file_upload.url(**redirect_params)
end
end
+
+ def guess_content_type(filename)
+ types = MIME::Types.type_for(filename)
+
+ if types.present?
+ types.first.content_type
+ else
+ "application/octet-stream"
+ end
+ end
end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index c1acb50b76c..8bd93a349ef 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ServiceParams
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index 120614739aa..8c22490700c 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SnippetsActions
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 922aa58a00f..c3a1b12af84 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SpammableActions
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/todos_actions.rb b/app/controllers/concerns/todos_actions.rb
index c0acdb3498d..78b65f7961b 100644
--- a/app/controllers/concerns/todos_actions.rb
+++ b/app/controllers/concerns/todos_actions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TodosActions
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index ae0b815f85e..97b343f8b1a 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ToggleAwardEmoji
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
index 776583579e8..e613bfaeef2 100644
--- a/app/controllers/concerns/toggle_subscription_action.rb
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ToggleSubscriptionAction
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index 434459a225a..7a1c7abfb8f 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module UploadsActions
extend ActiveSupport::Concern
@@ -53,6 +55,8 @@ module UploadsActions
maximum_size: Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i)
render json: authorized
+ rescue SocketError
+ render json: "Error uploading file", status: :internal_server_error
end
private
@@ -87,6 +91,7 @@ module UploadsActions
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def build_uploader_from_upload
return unless uploader = build_uploader
@@ -94,6 +99,7 @@ module UploadsActions
upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_paths)
upload&.build_uploader
end
+ # rubocop: enable CodeReuse/ActiveRecord
def build_uploader_from_params
return unless uploader = build_uploader
diff --git a/app/controllers/concerns/with_performance_bar.rb b/app/controllers/concerns/with_performance_bar.rb
index 6a8b1a4de7b..77c3d476ac6 100644
--- a/app/controllers/concerns/with_performance_bar.rb
+++ b/app/controllers/concerns/with_performance_bar.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module WithPerformanceBar
extend ActiveSupport::Concern
@@ -8,11 +10,7 @@ module WithPerformanceBar
def peek_enabled?
return false unless Gitlab::PerformanceBar.enabled?(current_user)
- if RequestStore.active?
- RequestStore.fetch(:peek_enabled) { cookie_or_default_value }
- else
- cookie_or_default_value
- end
+ Gitlab::SafeRequestStore.fetch(:peek_enabled) { cookie_or_default_value }
end
private
diff --git a/app/controllers/concerns/workhorse_request.rb b/app/controllers/concerns/workhorse_request.rb
index 43c0f1b173c..028f10e866a 100644
--- a/app/controllers/concerns/workhorse_request.rb
+++ b/app/controllers/concerns/workhorse_request.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module WorkhorseRequest
extend ActiveSupport::Concern
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 7bc46a6ccc0..2c4aab67448 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ConfirmationsController < Devise::ConfirmationsController
include AcceptsPendingInvitations
@@ -20,7 +22,7 @@ class ConfirmationsController < Devise::ConfirmationsController
after_sign_in(resource)
else
Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}")
- flash[:notice] += " Please sign in."
+ flash[:notice] = flash[:notice] + " Please sign in."
new_session_path(:user, anchor: 'login-pane')
end
end
diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb
index 9fb5c525425..cee0753a021 100644
--- a/app/controllers/dashboard/application_controller.rb
+++ b/app/controllers/dashboard/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Dashboard::ApplicationController < ApplicationController
include ControllerWithCrossProjectAccessCheck
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 79f563bef86..f82cde8e10a 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Dashboard::GroupsController < Dashboard::ApplicationController
include GroupTree
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
index 9dcb3a0eb6d..89d87c2d5c8 100644
--- a/app/controllers/dashboard/labels_controller.rb
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Dashboard::LabelsController < Dashboard::ApplicationController
def index
respond_to do |format|
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 0469e7e1e1f..6e17bc212e4 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Dashboard::MilestonesController < Dashboard::ApplicationController
include MilestoneActions
@@ -22,7 +24,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
private
def group_milestones
- groups = GroupsFinder.new(current_user, all_available: true).execute
+ groups = GroupsFinder.new(current_user, all_available: false).execute
DashboardGroupMilestone.build_collection(groups)
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index ccfcbbdc776..e9686ed8d06 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Dashboard::ProjectsController < Dashboard::ApplicationController
include ParamsBackwardCompatibility
include RendersMemberAccess
@@ -23,6 +25,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def starred
@projects = load_projects(params.merge(starred: true))
.includes(:forked_from_project, :tags)
@@ -38,6 +41,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
@@ -46,6 +50,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@sort = params[:sort]
end
+ # rubocop: disable CodeReuse/ActiveRecord
def load_projects(finder_params)
projects = ProjectsFinder
.new(params: finder_params, current_user: current_user)
@@ -55,6 +60,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
prepare_projects_for_rendering(projects)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def load_events
projects = load_projects(params.merge(non_public: true))
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index 0ba97e4fd59..161c22046f9 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Dashboard::SnippetsController < Dashboard::ApplicationController
skip_cross_project_access_check :index
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index bd7111e28bc..b82caf30a91 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Dashboard::TodosController < Dashboard::ApplicationController
include ActionView::Helpers::NumberHelper
@@ -73,6 +75,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def redirect_out_of_range(todos)
total_pages =
if todo_params.except(:sort, :page).empty?
@@ -91,4 +94,5 @@ class Dashboard::TodosController < Dashboard::ApplicationController
out_of_range
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index ff133001b84..c032fb2efb5 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DashboardController < Dashboard::ApplicationController
include IssuesAction
include MergeRequestsAction
@@ -38,7 +40,7 @@ class DashboardController < Dashboard::ApplicationController
end
@events = EventCollection
- .new(projects, offset: params[:offset].to_i, filter: @event_filter)
+ .new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
Events::RenderService.new(current_user).execute(@events)
diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb
index baf54520b9c..8eee3742d89 100644
--- a/app/controllers/explore/application_controller.rb
+++ b/app/controllers/explore/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Explore::ApplicationController < ApplicationController
skip_before_action :authenticate_user!
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index fa0a0f68fbc..67db797b80a 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Explore::GroupsController < Explore::ApplicationController
include GroupTree
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index c7273606a85..7ecbc32cf4e 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Explore::ProjectsController < Explore::ApplicationController
include ParamsBackwardCompatibility
include RendersMemberAccess
@@ -34,6 +36,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def starred
@projects = load_projects.reorder('star_count DESC')
@@ -46,9 +49,11 @@ class Explore::ProjectsController < Explore::ApplicationController
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
+ # rubocop: disable CodeReuse/ActiveRecord
def load_projects
projects = ProjectsFinder.new(current_user: current_user, params: params)
.execute
@@ -58,4 +63,5 @@ class Explore::ProjectsController < Explore::ApplicationController
prepare_projects_for_rendering(projects)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index d3f0e033068..76ed142c939 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Explore::SnippetsController < Explore::ApplicationController
def index
@snippets = SnippetsFinder.new(current_user).execute
diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb
index 5551057ff55..dd9f5af61b3 100644
--- a/app/controllers/google_api/authorizations_controller.rb
+++ b/app/controllers/google_api/authorizations_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module GoogleApi
class AuthorizationsController < ApplicationController
def callback
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 0a1cf169aca..a1ec144410b 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GraphqlController < ApplicationController
# Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user!
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 62213561898..5f92333c2c3 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Groups::ApplicationController < ApplicationController
include RoutableActions
include ControllerWithCrossProjectAccessCheck
diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb
index 35a61b359c8..8e4dc2bb6e9 100644
--- a/app/controllers/groups/avatars_controller.rb
+++ b/app/controllers/groups/avatars_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Groups::AvatarsController < Groups::ApplicationController
before_action :authorize_admin_group!
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index e892d1f8dbf..8d259b4052e 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Groups::BoardsController < Groups::ApplicationController
include BoardsResponses
diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb
index 0e8125d6113..d549f793ad7 100644
--- a/app/controllers/groups/children_controller.rb
+++ b/app/controllers/groups/children_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Groups
class ChildrenController < Groups::ApplicationController
before_action :group
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 7dc51f4c357..0bc082246a1 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
include MembersPresentation
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 863f50e8e66..26768c628ca 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
+
class Groups::LabelsController < Groups::ApplicationController
include ToggleSubscriptionAction
before_action :label, only: [:edit, :update, :destroy]
- before_action :available_labels, only: [:index]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
@@ -11,10 +12,11 @@ class Groups::LabelsController < Groups::ApplicationController
def index
respond_to do |format|
format.html do
- @labels = @group.labels.page(params[:page])
+ @labels = GroupLabelsFinder
+ .new(current_user, @group, params.merge(sort: sort)).execute
end
format.json do
- render json: LabelSerializer.new.represent_appearance(@available_labels)
+ render json: LabelSerializer.new.represent_appearance(available_labels)
end
end
end
@@ -113,7 +115,11 @@ class Groups::LabelsController < Groups::ApplicationController
group_id: @group.id,
only_group_labels: params[:only_group_labels],
include_ancestor_groups: params[:include_ancestor_groups],
- include_descendant_groups: params[:include_descendant_groups]
- ).execute
+ include_descendant_groups: params[:include_descendant_groups],
+ search: params[:search]).execute
+ end
+
+ def sort
+ @sort ||= params[:sort] || 'name_asc'
end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 6bdc0f79ef2..a7cee426cf1 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index 1036b4e6ed3..dd8fbf7a029 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Groups::RunnersController < Groups::ApplicationController
# Proper policies should be implemented per
# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
diff --git a/app/controllers/groups/settings/badges_controller.rb b/app/controllers/groups/settings/badges_controller.rb
deleted file mode 100644
index ccbd0a3bc02..00000000000
--- a/app/controllers/groups/settings/badges_controller.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Groups
- module Settings
- class BadgesController < Groups::ApplicationController
- include API::Helpers::RelatedResourcesHelpers
-
- before_action :authorize_admin_group!
-
- def index
- @badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id))
- end
- end
- end
-end
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 4bf6a2a3ad1..93f3eb2be6d 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Groups
module Settings
class CiCdController < Groups::ApplicationController
@@ -8,6 +10,13 @@ module Groups
define_secret_variables
end
+ def reset_registration_token
+ @group.reset_runners_token!
+
+ flash[:notice] = 'New runners registration token has been generated!'
+ redirect_to group_settings_ci_cd_path
+ end
+
private
def define_secret_variables
diff --git a/app/controllers/groups/shared_projects_controller.rb b/app/controllers/groups/shared_projects_controller.rb
index 7dec1f5f402..30b7bfc70ae 100644
--- a/app/controllers/groups/shared_projects_controller.rb
+++ b/app/controllers/groups/shared_projects_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Groups
class SharedProjectsController < Groups::ApplicationController
respond_to :json
diff --git a/app/controllers/groups/uploads_controller.rb b/app/controllers/groups/uploads_controller.rb
index 74760194a1f..7e5cdae0ce3 100644
--- a/app/controllers/groups/uploads_controller.rb
+++ b/app/controllers/groups/uploads_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Groups::UploadsController < Groups::ApplicationController
include UploadsActions
include WorkhorseRequest
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index 4d8a20de017..4f641de0357 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Groups
class VariablesController < Groups::ApplicationController
before_action :authorize_admin_build!
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 83169636ccf..062c8c4e9e1 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class GroupsController < Groups::ApplicationController
+ include API::Helpers::RelatedResourcesHelpers
include IssuesAction
include MergeRequestsAction
include ParamsBackwardCompatibility
@@ -16,7 +19,7 @@ class GroupsController < Groups::ApplicationController
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :event_filter, only: [:activity]
- before_action :user_actions, only: [:show, :subgroups]
+ before_action :user_actions, only: [:show]
skip_cross_project_access_check :index, :new, :create, :edit, :update,
:destroy, :projects
@@ -52,11 +55,7 @@ class GroupsController < Groups::ApplicationController
def show
respond_to do |format|
- format.html do
- @has_children = GroupDescendantsFinder.new(current_user: current_user,
- parent_group: @group,
- params: params).has_children?
- end
+ format.html
format.atom do
load_events
@@ -77,6 +76,7 @@ class GroupsController < Groups::ApplicationController
end
def edit
+ @badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id))
end
def projects
@@ -99,6 +99,7 @@ class GroupsController < Groups::ApplicationController
redirect_to root_path, status: 302, alert: "Group '#{@group.name}' was scheduled for deletion."
end
+ # rubocop: disable CodeReuse/ActiveRecord
def transfer
parent_group = Group.find_by(id: params[:new_parent_group_id])
service = ::Groups::TransferService.new(@group, current_user)
@@ -111,9 +112,11 @@ class GroupsController < Groups::ApplicationController
render :edit
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
protected
+ # rubocop: disable CodeReuse/ActiveRecord
def authorize_create_group!
allowed = if params[:parent_id].present?
parent = Group.find_by(id: params[:parent_id])
@@ -124,6 +127,7 @@ class GroupsController < Groups::ApplicationController
render_404 unless allowed
end
+ # rubocop: enable CodeReuse/ActiveRecord
def determine_layout
if [:new, :create].include?(action_name.to_sym)
@@ -158,6 +162,7 @@ class GroupsController < Groups::ApplicationController
]
end
+ # rubocop: disable CodeReuse/ActiveRecord
def load_events
params[:sort] ||= 'latest_activity_desc'
@@ -177,6 +182,7 @@ class GroupsController < Groups::ApplicationController
.new(current_user)
.execute(@events, atom_request: request.format.atom?)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def user_actions
if current_user
diff --git a/app/controllers/health_check_controller.rb b/app/controllers/health_check_controller.rb
index c3d18991fd4..a2abed7ba4e 100644
--- a/app/controllers/health_check_controller.rb
+++ b/app/controllers/health_check_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class HealthCheckController < HealthCheck::HealthCheckController
include RequiresWhitelistedMonitoringClient
end
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 3fedd5bfb29..ab4bc911e17 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class HealthController < ActionController::Base
protect_from_forgery with: :exception, except: :storage_check, prepend: true
include RequiresWhitelistedMonitoringClient
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index a394521698c..e5a1fc9d6ff 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class HelpController < ApplicationController
skip_before_action :authenticate_user!
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index 96bb2237d90..eeeebe430a7 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class IdeController < ApplicationController
layout 'fullscreen'
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 5766c6924cd..042b6b1264f 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -1,16 +1,22 @@
+# frozen_string_literal: true
+
class Import::BaseController < ApplicationController
private
+ # rubocop: disable CodeReuse/ActiveRecord
def find_already_added_projects(import_type)
current_user.created_projects.where(import_type: import_type).includes(:import_state)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def find_jobs(import_type)
current_user.created_projects
.includes(:import_state)
.where(import_type: import_type)
.to_json(only: [:id], methods: [:import_status])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def find_or_create_namespace(names, owner)
names = params[:target_namespace].presence || names
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index fa31933e778..1b30b4dda36 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Import::BitbucketController < Import::BaseController
before_action :verify_bitbucket_import_enabled
before_action :bitbucket_auth, except: :callback
@@ -16,6 +18,7 @@ class Import::BitbucketController < Import::BaseController
redirect_to status_import_bitbucket_url
end
+ # rubocop: disable CodeReuse/ActiveRecord
def status
bitbucket_client = Bitbucket::Client.new(credentials)
repos = bitbucket_client.repos
@@ -27,6 +30,7 @@ class Import::BitbucketController < Import::BaseController
@repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
end
+ # rubocop: enable CodeReuse/ActiveRecord
def jobs
render json: find_jobs('bitbucket')
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index 798daeca6c9..fdd1078cdf7 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -52,6 +52,7 @@ class Import::BitbucketServerController < Import::BaseController
redirect_to status_import_bitbucket_server_path
end
+ # rubocop: disable CodeReuse/ActiveRecord
def status
repos = bitbucket_client.repos
@@ -66,6 +67,7 @@ class Import::BitbucketServerController < Import::BaseController
clear_session_data
redirect_to new_import_bitbucket_server_path
end
+ # rubocop: enable CodeReuse/ActiveRecord
def jobs
render json: find_jobs('bitbucket_server')
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 2d665e05ac3..5a439e6de78 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Import::FogbugzController < Import::BaseController
before_action :verify_fogbugz_import_enabled
before_action :user_map, only: [:new_user_map, :create_user_map]
@@ -39,6 +41,7 @@ class Import::FogbugzController < Import::BaseController
redirect_to status_import_fogbugz_path
end
+ # rubocop: disable CodeReuse/ActiveRecord
def status
unless client.valid?
return redirect_to new_import_fogbugz_path
@@ -51,6 +54,7 @@ class Import::FogbugzController < Import::BaseController
@repos.reject! { |repo| already_added_projects_names.include? repo.name }
end
+ # rubocop: enable CodeReuse/ActiveRecord
def jobs
render json: find_jobs('fogbugz')
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index fbd851c64a7..382c684a408 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Import::GiteaController < Import::GithubController
def new
if session[access_token_key].present? && session[host_key].present?
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index c9870332c0f..1dfa814cdd5 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Import::GithubController < Import::BaseController
before_action :verify_import_enabled
before_action :provider_auth, only: [:status, :jobs, :create]
@@ -22,6 +24,7 @@ class Import::GithubController < Import::BaseController
redirect_to status_import_url
end
+ # rubocop: disable CodeReuse/ActiveRecord
def status
@repos = client.repos
@already_added_projects = find_already_added_projects(provider)
@@ -29,6 +32,7 @@ class Import::GithubController < Import::BaseController
@repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
end
+ # rubocop: enable CodeReuse/ActiveRecord
def jobs
render json: find_jobs(provider)
@@ -104,9 +108,11 @@ class Import::GithubController < Import::BaseController
:github
end
+ # rubocop: disable CodeReuse/ActiveRecord
def logged_in_with_provider?
current_user.identities.exists?(provider: provider)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def provider_auth
if session[access_token_key].blank?
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 53f70446d95..498de0b07b8 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Import::GitlabController < Import::BaseController
MAX_PROJECT_PAGES = 15
PER_PAGE_PROJECTS = 100
@@ -12,6 +14,7 @@ class Import::GitlabController < Import::BaseController
redirect_to status_import_gitlab_url
end
+ # rubocop: disable CodeReuse/ActiveRecord
def status
@repos = client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
@@ -20,6 +23,7 @@ class Import::GitlabController < Import::BaseController
@repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] }
end
+ # rubocop: enable CodeReuse/ActiveRecord
def jobs
render json: find_jobs('gitlab')
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index f22df992fe9..354fba5d204 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Import::GitlabProjectsController < Import::BaseController
before_action :whitelist_query_limiting, only: [:create]
before_action :verify_gitlab_project_import_enabled
@@ -11,7 +13,7 @@ class Import::GitlabProjectsController < Import::BaseController
def create
unless file_is_valid?
- return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
+ return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive (ending in .gz)." })
end
@project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute
@@ -29,7 +31,11 @@ class Import::GitlabProjectsController < Import::BaseController
private
def file_is_valid?
- project_params[:file] && project_params[:file].respond_to?(:read)
+ return false unless project_params[:file] && project_params[:file].respond_to?(:read)
+
+ filename = project_params[:file].original_filename
+
+ ImportExportUploader::EXTENSION_WHITELIST.include?(File.extname(filename).delete('.'))
end
def verify_gitlab_project_import_enabled
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index 3bce27e810a..331f06c3dd6 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Import::GoogleCodeController < Import::BaseController
before_action :verify_google_code_import_enabled
before_action :user_map, only: [:new_user_map, :create_user_map]
@@ -65,6 +67,7 @@ class Import::GoogleCodeController < Import::BaseController
redirect_to status_import_google_code_path
end
+ # rubocop: disable CodeReuse/ActiveRecord
def status
unless client.valid?
return redirect_to new_import_google_code_path
@@ -78,6 +81,7 @@ class Import::GoogleCodeController < Import::BaseController
@repos.reject! { |repo| already_added_projects_names.include? repo.name }
end
+ # rubocop: enable CodeReuse/ActiveRecord
def jobs
render json: find_jobs('google_code')
diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb
index e5a719fa0df..320cd45b925 100644
--- a/app/controllers/import/manifest_controller.rb
+++ b/app/controllers/import/manifest_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Import::ManifestController < Import::BaseController
before_action :whitelist_query_limiting, only: [:create]
before_action :verify_import_enabled
@@ -6,6 +8,7 @@ class Import::ManifestController < Import::BaseController
def new
end
+ # rubocop: disable CodeReuse/ActiveRecord
def status
@already_added_projects = find_already_added_projects
already_added_import_urls = @already_added_projects.pluck(:import_url)
@@ -14,6 +17,7 @@ class Import::ManifestController < Import::BaseController
already_added_import_urls.include?(repository[:url])
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def upload
group = Group.find(params[:group_id])
@@ -64,9 +68,11 @@ class Import::ManifestController < Import::BaseController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def group
@group ||= Group.find_by(id: session[:manifest_import_group_id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def repositories
@repositories ||= session[:manifest_import_repositories]
@@ -76,12 +82,14 @@ class Import::ManifestController < Import::BaseController
find_already_added_projects.to_json(only: [:id], methods: [:import_status])
end
+ # rubocop: disable CodeReuse/ActiveRecord
def find_already_added_projects
group.all_projects
.where(import_type: 'manifest')
.where(creator_id: current_user)
.includes(:import_state)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def verify_import_enabled
render_404 unless manifest_import_enabled?
diff --git a/app/controllers/instance_statistics/cohorts_controller.rb b/app/controllers/instance_statistics/cohorts_controller.rb
index 7eba0a5ecdd..4b4e39db2e1 100644
--- a/app/controllers/instance_statistics/cohorts_controller.rb
+++ b/app/controllers/instance_statistics/cohorts_controller.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController
+ before_action :authenticate_usage_ping_enabled_or_admin!
+
def index
if Gitlab::CurrentSettings.usage_ping_enabled
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
@@ -10,4 +12,8 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon
@cohorts = CohortsSerializer.new.represent(cohorts_results)
end
end
+
+ def authenticate_usage_ping_enabled_or_admin!
+ render_404 unless Gitlab::CurrentSettings.usage_ping_enabled || current_user.admin?
+ end
end
diff --git a/app/controllers/instance_statistics/conversational_development_index_controller.rb b/app/controllers/instance_statistics/conversational_development_index_controller.rb
index d6d2191849f..306c16d559c 100644
--- a/app/controllers/instance_statistics/conversational_development_index_controller.rb
+++ b/app/controllers/instance_statistics/conversational_development_index_controller.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
class InstanceStatistics::ConversationalDevelopmentIndexController < InstanceStatistics::ApplicationController
+ # rubocop: disable CodeReuse/ActiveRecord
def index
@metric = ConversationalDevelopmentIndex::Metric.order(:created_at).last&.present
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 025d8270b7c..315d1375e02 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class InvitesController < ApplicationController
before_action :member
skip_before_action :authenticate_user!, only: :decline
@@ -50,9 +52,9 @@ class InvitesController < ApplicationController
def authenticate_user!
return if current_user
- notice = "To accept this invitation, sign in"
- notice << " or create an account" if Gitlab::CurrentSettings.allow_signup?
- notice << "."
+ notice = ["To accept this invitation, sign in"]
+ notice << "or create an account" if Gitlab::CurrentSettings.allow_signup?
+ notice = notice.join(' ') + "."
store_location_for :user, request.fullpath
redirect_to new_user_session_path, notice: notice
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index d172aee5436..f9008a5b67e 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class JwtController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb
index 745abf3c0f5..72aa9d4f17f 100644
--- a/app/controllers/koding_controller.rb
+++ b/app/controllers/koding_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class KodingController < ApplicationController
before_action :check_integration!
layout 'koding'
diff --git a/app/controllers/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb
index fb24edb8602..5e872804448 100644
--- a/app/controllers/ldap/omniauth_callbacks_controller.rb
+++ b/app/controllers/ldap/omniauth_callbacks_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
extend ::Gitlab::Utils::Override
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 0400ffcfee5..7353be478e1 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class MetricsController < ActionController::Base
include RequiresWhitelistedMonitoringClient
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index 461f26561f1..84dce74ace8 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NotificationSettingsController < ApplicationController
before_action :authenticate_user!
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index a1fe02dc852..b50f140dc80 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::GonHelper
include Gitlab::Allowable
include PageLayoutHelper
include OauthApplications
- before_action :verify_user_oauth_applications_enabled
+ before_action :verify_user_oauth_applications_enabled, except: :index
before_action :authenticate_user!
before_action :add_gon_variables
before_action :load_scopes, only: [:index, :create, :edit]
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 05190103767..894a6a431e3 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
layout 'profile'
diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index 656107d2b26..a59ade559b3 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
include PageLayoutHelper
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 1547d4b5972..30be50d4595 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
@@ -135,14 +137,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def handle_signup_error
label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
- message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
+ message = ["Signing in using your #{label} account without a pre-existing GitLab account is not allowed."]
if Gitlab::CurrentSettings.allow_signup?
- message << " Create a GitLab account first, and then connect it to your #{label} account."
+ message << "Create a GitLab account first, and then connect it to your #{label} account."
end
- flash[:notice] = message
-
+ flash[:notice] = message.join(' ')
redirect_to new_user_session_path
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 331583c49e6..2912a22411e 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PasswordsController < Devise::PasswordsController
skip_before_action :require_no_authentication, only: [:edit, :update]
@@ -5,6 +7,7 @@ class PasswordsController < Devise::PasswordsController
before_action :check_password_authentication_available, only: [:create]
before_action :throttle_reset, only: [:create]
+ # rubocop: disable CodeReuse/ActiveRecord
def edit
super
reset_password_token = Devise.token_generator.digest(
@@ -24,6 +27,7 @@ class PasswordsController < Devise::PasswordsController
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def update
super do |resource|
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index 7d1aa8d1ce0..cb3180f4196 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::AccountsController < Profiles::ApplicationController
include AuthHelper
@@ -5,6 +7,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
@user = current_user
end
+ # rubocop: disable CodeReuse/ActiveRecord
def unlink
provider = params[:provider]
identity = current_user.identities.find_by(provider: provider)
@@ -19,4 +22,5 @@ class Profiles::AccountsController < Profiles::ApplicationController
redirect_to profile_account_path
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
index f1e77d68acd..efe7ede5efa 100644
--- a/app/controllers/profiles/active_sessions_controller.rb
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::ActiveSessionsController < Profiles::ApplicationController
def index
@sessions = ActiveSession.list(current_user)
diff --git a/app/controllers/profiles/application_controller.rb b/app/controllers/profiles/application_controller.rb
index c8be288b9a0..52b046ef64f 100644
--- a/app/controllers/profiles/application_controller.rb
+++ b/app/controllers/profiles/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::ApplicationController < ApplicationController
layout 'profile'
end
diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb
index 4f030ded80f..3378a09628c 100644
--- a/app/controllers/profiles/avatars_controller.rb
+++ b/app/controllers/profiles/avatars_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::AvatarsController < Profiles::ApplicationController
def destroy
@user = current_user
diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb
index a186c5f36a8..2e78b9e6dc7 100644
--- a/app/controllers/profiles/chat_names_controller.rb
+++ b/app/controllers/profiles/chat_names_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::ChatNamesController < Profiles::ApplicationController
before_action :chat_name_token, only: [:new]
before_action :chat_name_params, only: [:new, :create, :deny]
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index a39824ec9c8..503eda250b4 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::EmailsController < Profiles::ApplicationController
before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
index c32507756e8..8c34a66c374 100644
--- a/app/controllers/profiles/gpg_keys_controller.rb
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::GpgKeysController < Profiles::ApplicationController
before_action :set_gpg_key, only: [:destroy, :revoke]
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 6035258667e..01801c31327 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::KeysController < Profiles::ApplicationController
skip_before_action :authenticate_user!, only: [:get_keys]
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 8a38ba65d4c..b719b70c56e 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -1,10 +1,14 @@
+# frozen_string_literal: true
+
class Profiles::NotificationsController < Profiles::ApplicationController
+ # rubocop: disable CodeReuse/ActiveRecord
def show
@user = current_user
@group_notifications = current_user.notification_settings.for_groups.order(:id)
@project_notifications = current_user.notification_settings.for_projects.order(:id)
@global_notification_setting = current_user.global_notification_setting
end
+ # rubocop: enable CodeReuse/ActiveRecord
def update
result = Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index b8ccc6e3c99..a0391d677c4 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::PasswordsController < Profiles::ApplicationController
skip_before_action :check_password_expiration, only: [:new, :create]
skip_before_action :check_two_factor_requirement, only: [:new, :create]
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 346eab4ba19..4b6ec2697b7 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def index
set_index_vars
@@ -38,6 +40,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
end
+ # rubocop: disable CodeReuse/ActiveRecord
def set_index_vars
@scopes = Gitlab::Auth.available_scopes(current_user)
@@ -46,4 +49,5 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
@new_personal_access_token = PersonalAccessToken.redis_getdel(current_user.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index ed0f98179eb..37ac11dc6a1 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::PreferencesController < Profiles::ApplicationController
before_action :user
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 29ff18a1219..ba94196b2f9 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_two_factor_requirement
@@ -30,7 +32,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
unless two_factor_grace_period_expired?
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
- flash.now[:alert] << " You need to do this before #{l(grace_period_deadline)}."
+ flash.now[:alert] = flash.now[:alert] + " You need to do this before #{l(grace_period_deadline)}."
end
end
diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb
index e3d7737f44a..e6a154fb6aa 100644
--- a/app/controllers/profiles/u2f_registrations_controller.rb
+++ b/app/controllers/profiles/u2f_registrations_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Profiles::U2fRegistrationsController < Profiles::ApplicationController
def destroy
u2f_registration = current_user.u2f_registrations.find(params[:id])
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 6f50cbb4a36..15248d2d08f 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProfilesController < Profiles::ApplicationController
include ActionView::Helpers::SanitizeHelper
@@ -44,11 +46,13 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_personal_access_tokens_path
end
+ # rubocop: disable CodeReuse/ActiveRecord
def audit_log
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id)
.order("created_at DESC")
.page(params[:page])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def update_username
result = Users::UpdateService.new(current_user, user: @user, username: username_param).execute
@@ -94,6 +98,7 @@ class ProfilesController < Profiles::ApplicationController
:location,
:name,
:public_email,
+ :commit_email,
:skype,
:twitter,
:username,
@@ -101,6 +106,7 @@ class ProfilesController < Profiles::ApplicationController
:organization,
:preferred_language,
:private_profile,
+ :include_private_contributions,
status: [:emoji, :message]
)
end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index b4f814fd3a4..a2bdcaefa9b 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class Projects::ApplicationController < ApplicationController
+ include CookiesHelper
include RoutableActions
include ChecksCollaboration
@@ -74,7 +77,7 @@ class Projects::ApplicationController < ApplicationController
end
def apply_diff_view_cookie!
- cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
+ set_secure_cookie(:diff_view, params.delete(:view), permanent: true) if params[:view].present?
end
def require_pages_enabled!
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 6484a713f8e..d0f59aa8162 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::ArtifactsController < Projects::ApplicationController
include ExtractsPath
include RendersBlob
@@ -12,6 +14,8 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :entry, only: [:file]
def download
+ return render_404 unless artifacts_file
+
send_upload(artifacts_file, attachment: artifacts_file.filename)
end
@@ -82,19 +86,23 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def build_from_id
project.builds.find_by(id: params[:job_id]) if params[:job_id]
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def build_from_ref
return unless @ref_name
builds = project.latest_successful_builds_for(@ref_name)
builds.find_by(name: params[:job])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def artifacts_file
- @artifacts_file ||= build.artifacts_file
+ @artifacts_file ||= build.artifacts_file_for_type(params[:file_type] || :archive)
end
def entry
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index a8f73ed5cb0..d386fb63d9f 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :load_autocomplete_service, except: [:members]
@@ -25,6 +27,10 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
render json: @autocomplete_service.commands(target, params[:type])
end
+ def snippets
+ render json: @autocomplete_service.snippets
+ end
+
private
def load_autocomplete_service
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 878c82cd183..1c385c0e15a 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::AvatarsController < Projects::ApplicationController
include SendsBlob
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index 06ba73d8e8d..c24bf211760 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::BadgesController < Projects::ApplicationController
layout 'project_settings'
before_action :authorize_admin_project!, only: [:index]
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 6461eeac11c..9076bdb9f04 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Controller for viewing a file's blame
class Projects::BlameController < Projects::ApplicationController
include ExtractsPath
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index ebc61264b39..92d26a13da9 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Controller for viewing a file's blame
class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
@@ -127,7 +129,7 @@ class Projects::BlobController < Projects::ApplicationController
add_match_line
- render json: @lines
+ render json: DiffLineSerializer.new.represent(@lines)
end
def add_match_line
@@ -177,6 +179,7 @@ class Projects::BlobController < Projects::ApplicationController
render_404
end
+ # rubocop: disable CodeReuse/ActiveRecord
def after_edit_path
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:from_merge_request_iid])
if from_merge_request && @branch_name == @ref
@@ -186,6 +189,7 @@ class Projects::BlobController < Projects::ApplicationController
project_blob_path(@project, File.join(@branch_name, @path))
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def editor_variables
@branch_name = params[:branch_name]
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index e7354a9e1f7..77b818347c7 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::BoardsController < Projects::ApplicationController
include BoardsResponses
include IssuableCollections
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index d1dc9fe9600..b7750f4517b 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::BranchesController < Projects::ApplicationController
include ActionView::Helpers::SanitizeHelper
include SortingHelper
@@ -48,6 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController
@branches = @repository.recent_branches
end
+ # rubocop: disable CodeReuse/ActiveRecord
def create
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
@@ -88,6 +91,7 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def destroy
@branch_name = Addressable::URI.unescape(params[:id])
diff --git a/app/controllers/projects/build_artifacts_controller.rb b/app/controllers/projects/build_artifacts_controller.rb
index b45e5d7ff43..46449a4aae9 100644
--- a/app/controllers/projects/build_artifacts_controller.rb
+++ b/app/controllers/projects/build_artifacts_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::BuildArtifactsController < Projects::ApplicationController
include ExtractsPath
include RendersBlob
@@ -42,14 +44,18 @@ class Projects::BuildArtifactsController < Projects::ApplicationController
@job ||= job_from_id || job_from_ref
end
+ # rubocop: disable CodeReuse/ActiveRecord
def job_from_id
project.builds.find_by(id: params[:build_id]) if params[:build_id]
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def job_from_ref
return unless @ref_name
jobs = project.latest_successful_builds_for(@ref_name)
jobs.find_by(name: params[:job])
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 230b072dcea..6b3d70cb720 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::BuildsController < Projects::ApplicationController
before_action :authorize_read_build!
diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb
index a2185572a20..2090af0a111 100644
--- a/app/controllers/projects/ci/lints_controller.rb
+++ b/app/controllers/projects/ci/lints_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::Ci::LintsController < Projects::ApplicationController
before_action :authorize_create_pipeline!
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
index a5c82caa897..c356f8d2987 100644
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
class Projects::Clusters::ApplicationsController < Projects::ApplicationController
before_action :cluster
before_action :application_class, only: [:create]
before_action :authorize_read_cluster!
before_action :authorize_create_cluster!, only: [:create]
+ # rubocop: disable CodeReuse/ActiveRecord
def create
application = @application_class.find_or_initialize_by(cluster: @cluster)
@@ -23,6 +26,7 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
rescue StandardError
head :bad_request
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 358fe59618b..bcdbf48bb35 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::ClustersController < Projects::ApplicationController
before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
before_action :authorize_read_cluster!
@@ -141,7 +143,8 @@ class Projects::ClustersController < Projects::ApplicationController
:gcp_project_id,
:zone,
:num_nodes,
- :machine_type
+ :machine_type,
+ :legacy_abac
]).merge(
provider_type: :gcp,
platform_type: :kubernetes
@@ -157,7 +160,8 @@ class Projects::ClustersController < Projects::ApplicationController
:namespace,
:api_url,
:token,
- :ca_cert
+ :ca_cert,
+ :authorization_type
]).merge(
provider_type: :user,
platform_type: :kubernetes
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 53637780a07..00b63f55710 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Controller for a specific Commit
#
# Not to be confused with CommitsController, plural.
@@ -38,6 +40,7 @@ class Projects::CommitController < Projects::ApplicationController
render_diff_for_path(@commit.diffs(diff_options))
end
+ # rubocop: disable CodeReuse/ActiveRecord
def pipelines
@pipelines = @commit.pipelines.order(id: :desc)
@pipelines = @pipelines.where(ref: params[:ref]) if params[:ref]
@@ -58,6 +61,7 @@ class Projects::CommitController < Projects::ApplicationController
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def merge_requests
@merge_requests = @commit.merge_requests.map do |mr|
@@ -144,6 +148,7 @@ class Projects::CommitController < Projects::ApplicationController
@environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last
end
+ # rubocop: disable CodeReuse/ActiveRecord
def define_note_vars
@noteable = @commit
@note = @project.build_commit_note(commit)
@@ -176,6 +181,7 @@ class Projects::CommitController < Projects::ApplicationController
@notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes)
@notes = prepare_notes_for_rendering(@notes, @commit)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def assign_change_commit_vars
@start_branch = params[:start_branch]
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 5546bef850b..84a2a461da7 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "base64"
class Projects::CommitsController < Projects::ApplicationController
@@ -15,6 +17,7 @@ class Projects::CommitsController < Projects::ApplicationController
redirect_to project_commits_path(@project, @project.default_branch)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def show
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
@@ -32,6 +35,7 @@ class Projects::CommitsController < Projects::ApplicationController
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def signatures
respond_to do |format|
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index a1e12821caf..c2df7b34f90 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'addressable/uri'
class Projects::CompareController < Projects::ApplicationController
@@ -96,8 +98,10 @@ class Projects::CompareController < Projects::ApplicationController
@diff_notes_disabled = compare.present?
end
+ # rubocop: disable CodeReuse/ActiveRecord
def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: head_ref, target_branch: start_ref)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
index 26f3c114108..fb43356ff10 100644
--- a/app/controllers/projects/cycle_analytics/events_controller.rb
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Projects
module CycleAnalytics
class EventsController < Projects::ApplicationController
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index d1b8fd80c4e..8c071496ba9 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TextHelper
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 28fea322334..92ef10a9ef5 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::DeployKeysController < Projects::ApplicationController
include RepositorySettingsRedirect
respond_to :html
@@ -52,6 +54,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def disable
deploy_key_project = @project.deploy_keys_projects.find_by(deploy_key_id: params[:id])
return render_404 unless deploy_key_project
@@ -63,6 +66,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
format.json { head :ok }
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
protected
diff --git a/app/controllers/projects/deploy_tokens_controller.rb b/app/controllers/projects/deploy_tokens_controller.rb
index 83abda64fe0..830b1f4fe4a 100644
--- a/app/controllers/projects/deploy_tokens_controller.rb
+++ b/app/controllers/projects/deploy_tokens_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::DeployTokensController < Projects::ApplicationController
before_action :authorize_admin_project!
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index b68cdc39cb8..0a009477d61 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
class Projects::DeploymentsController < Projects::ApplicationController
before_action :authorize_read_environment!
before_action :authorize_read_deployment!
+ # rubocop: disable CodeReuse/ActiveRecord
def index
deployments = environment.deployments.reorder(created_at: :desc)
deployments = deployments.where('created_at > ?', params[:after].to_time) if params[:after]&.to_time
@@ -9,6 +12,7 @@ class Projects::DeploymentsController < Projects::ApplicationController
render json: { deployments: DeploymentSerializer.new(project: project)
.represent_concise(deployments) }
end
+ # rubocop: enable CodeReuse/ActiveRecord
def metrics
return render_404 unless deployment.has_metrics?
@@ -41,9 +45,11 @@ class Projects::DeploymentsController < Projects::ApplicationController
private
+ # rubocop: disable CodeReuse/ActiveRecord
def deployment
@deployment ||= environment.deployments.find_by(iid: params[:id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def environment
@environment ||= project.environments.find(params[:environment_id])
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 78b9d53a780..b62606067c0 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::DiscussionsController < Projects::ApplicationController
include NotesHelper
include RendersNotes
@@ -50,9 +52,11 @@ class Projects::DiscussionsController < Projects::ApplicationController
}
end
+ # rubocop: disable CodeReuse/ActiveRecord
def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def discussion
@discussion ||= @merge_request.find_discussion(params[:id]) || render_404
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 68353e6a210..de10783df1a 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
@@ -31,6 +33,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def folder
folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available)
@@ -51,10 +54,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def show
@deployments = environment.deployments.order(id: :desc).page(params[:page])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def new
@environment = project.environments.new
diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb
index cf53ad0a670..c026e9ff332 100644
--- a/app/controllers/projects/find_file_controller.rb
+++ b/app/controllers/projects/find_file_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Controller for viewing a repository's file structure
class Projects::FindFileController < Projects::ApplicationController
include ExtractsPath
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index f43bba18d81..7a1700a206a 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::ForksController < Projects::ApplicationController
include ContinueParams
@@ -7,6 +9,7 @@ class Projects::ForksController < Projects::ApplicationController
before_action :authorize_download_code!
before_action :authenticate_user!, only: [:new, :create]
+ # rubocop: disable CodeReuse/ActiveRecord
def index
base_query = project.forks.includes(:creator)
@@ -27,12 +30,14 @@ class Projects::ForksController < Projects::ApplicationController
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def new
@namespaces = current_user.manageable_namespaces
@namespaces.delete(@project.namespace)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def create
namespace = Namespace.find(params[:namespace_key])
@@ -55,6 +60,7 @@ class Projects::ForksController < Projects::ApplicationController
render :error
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42335')
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index a52814e6e52..d439db97252 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file should be identical in GitLab Community Edition and Enterprise Edition
class Projects::GitHttpClientController < Projects::ApplicationController
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 1dcf837f78e..be708835e30 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::GitHttpController < Projects::GitHttpClientController
include WorkhorseRequest
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 475d4c86294..925b6ed9bfd 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::GraphsController < Projects::ApplicationController
include ExtractsPath
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index bc5f38f3c2b..7c713c19762 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::GroupLinksController < Projects::ApplicationController
layout 'project_settings'
before_action :authorize_admin_project!
diff --git a/app/controllers/projects/hook_logs_controller.rb b/app/controllers/projects/hook_logs_controller.rb
index 745e89fc843..a7afc3d77a5 100644
--- a/app/controllers/projects/hook_logs_controller.rb
+++ b/app/controllers/projects/hook_logs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::HookLogsController < Projects::ApplicationController
include HooksExecution
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 2da2aad9b33..bc84418b79f 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::HooksController < Projects::ApplicationController
include HooksExecution
@@ -66,6 +68,7 @@ class Projects::HooksController < Projects::ApplicationController
:enable_ssl_verification,
:token,
:url,
+ :push_events_branch_filter,
*ProjectHook.triggers.values
)
end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 49aa32119ef..e55065c5817 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::ImportsController < Projects::ApplicationController
include ContinueParams
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index c3ac8e107fb..b06a6f3bb0d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::IssuesController < Projects::ApplicationController
include RendersNotes
include ToggleSubscriptionAction
@@ -125,7 +127,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def related_branches
- @related_branches = @issue.related_branches(current_user)
+ @related_branches = Issues::RelatedBranchesService.new(project, current_user).execute(issue)
respond_to do |format|
format.json do
@@ -161,6 +163,7 @@ class Projects::IssuesController < Projects::ApplicationController
protected
+ # rubocop: disable CodeReuse/ActiveRecord
def issue
return @issue if defined?(@issue)
@@ -172,6 +175,7 @@ class Projects::IssuesController < Projects::ApplicationController
@issue
end
+ # rubocop: enable CodeReuse/ActiveRecord
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
alias_method :awardable, :issue
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index e69faae754a..9c9bbe04947 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
@@ -11,6 +13,7 @@ class Projects::JobsController < Projects::ApplicationController
layout 'project'
+ # rubocop: disable CodeReuse/ActiveRecord
def index
@scope = params[:scope]
@all_builds = project.builds.relevant
@@ -33,6 +36,7 @@ class Projects::JobsController < Projects::ApplicationController
])
@builds = @builds.page(params[:page]).per(30).without_count
end
+ # rubocop: enable CodeReuse/ActiveRecord
def cancel_all
return access_denied! unless can?(current_user, :update_build, project)
@@ -44,6 +48,7 @@ class Projects::JobsController < Projects::ApplicationController
redirect_to project_jobs_path(project)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def show
@pipeline = @build.pipeline
@builds = @pipeline.builds
@@ -61,6 +66,7 @@ class Projects::JobsController < Projects::ApplicationController
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def trace
build.trace.read do |stream|
@@ -104,6 +110,13 @@ class Projects::JobsController < Projects::ApplicationController
redirect_to build_path(@build)
end
+ def unschedule
+ return respond_422 unless @build.scheduled?
+
+ @build.unschedule!
+ redirect_to build_path(@build)
+ end
+
def status
render json: BuildSerializer
.new(project: @project, current_user: @current_user)
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 8a2bce6e7b5..640038818f2 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::LabelsController < Projects::ApplicationController
include ToggleSubscriptionAction
@@ -90,6 +92,7 @@ class Projects::LabelsController < Projects::ApplicationController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def set_priorities
Label.transaction do
available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id)
@@ -105,6 +108,7 @@ class Projects::LabelsController < Projects::ApplicationController
format.json { render json: { message: 'success' } }
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def promote
promote_service = Labels::PromoteService.new(@project, @current_user)
@@ -136,12 +140,7 @@ class Projects::LabelsController < Projects::ApplicationController
end
def flash_notice_for(label, group)
- notice = ''.html_safe
- notice << label.title
- notice << ' promoted to '
- notice << view_context.link_to('<u>group label</u>'.html_safe, group_labels_path(group))
- notice << '.'
- notice
+ ''.html_safe + "#{label.title} promoted to " + view_context.link_to('<u>group label</u>'.html_safe, group_labels_path(group)) + '.'
end
protected
@@ -163,7 +162,13 @@ class Projects::LabelsController < Projects::ApplicationController
LabelsFinder.new(current_user,
project_id: @project.id,
include_ancestor_groups: params[:include_ancestor_groups],
- search: params[:search]).execute
+ search: params[:search],
+ subscribed: params[:subscribed],
+ sort: sort).execute
+ end
+
+ def sort
+ @sort ||= params[:sort] || 'name_asc'
end
def authorize_admin_labels!
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index a01351ba292..be40077d389 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::LfsApiController < Projects::GitHttpClientController
include LfsRequest
@@ -41,11 +43,13 @@ class Projects::LfsApiController < Projects::GitHttpClientController
params[:operation] == 'upload'
end
+ # rubocop: disable CodeReuse/ActiveRecord
def existing_oids
@existing_oids ||= begin
project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def download_objects!
objects.each do |object|
diff --git a/app/controllers/projects/lfs_locks_api_controller.rb b/app/controllers/projects/lfs_locks_api_controller.rb
index 3fff0fd69ae..fc67cd72faa 100644
--- a/app/controllers/projects/lfs_locks_api_controller.rb
+++ b/app/controllers/projects/lfs_locks_api_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::LfsLocksApiController < Projects::GitHttpClientController
include LfsRequest
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index dd7e673ec75..babeee48ef3 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::LfsStorageController < Projects::GitHttpClientController
include LfsRequest
include WorkhorseRequest
@@ -56,6 +58,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
params[:size].to_i
end
+ # rubocop: disable CodeReuse/ActiveRecord
def store_file!(oid, size)
object = LfsObject.find_by(oid: oid, size: size)
unless object&.file&.exists?
@@ -66,6 +69,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
link_to_project!(object)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def create_file!(oid, size)
uploaded_file = UploadedFile.from_params(
@@ -75,9 +79,11 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
LfsObject.create!(oid: oid, size: size, file: uploaded_file)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id)
object.lfs_objects_projects.create!(project: storage_project)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb
index 0f6add3e287..085b1bc1498 100644
--- a/app/controllers/projects/mattermosts_controller.rb
+++ b/app/controllers/projects/mattermosts_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::MattermostsController < Projects::ApplicationController
include TriggersHelper
include ActionView::Helpers::AssetUrlHelper
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index fead81dd472..368ee89ff5c 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::MergeRequests::ApplicationController < Projects::ApplicationController
before_action :check_merge_requests_available!
before_action :merge_request
@@ -5,9 +7,11 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
private
+ # rubocop: disable CodeReuse/ActiveRecord
def merge_request
@issuable = @merge_request ||= @project.merge_requests.includes(author: :status).find_by!(iid: params[:id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def merge_request_params
params.require(:merge_request).permit(merge_request_params_attributes)
diff --git a/app/controllers/projects/merge_requests/conflicts_controller.rb b/app/controllers/projects/merge_requests/conflicts_controller.rb
index 366524b0783..ac1969adc6e 100644
--- a/app/controllers/projects/merge_requests/conflicts_controller.rb
+++ b/app/controllers/projects/merge_requests/conflicts_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::ApplicationController
include IssuableActions
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 03d0290ac1d..5639402a1e9 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::MergeRequests::CreationsController < Projects::MergeRequests::ApplicationController
include DiffForPath
include DiffHelper
@@ -104,11 +106,16 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@commits = set_commits_for_rendering(@merge_request.commits)
@commit = @merge_request.diff_head_commit
+ # FIXME: We have to assign a presenter to another instance variable
+ # due to class_name checks being made with issuable classes
+ @mr_presenter = @merge_request.present(current_user: current_user)
+
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
set_pipeline_variables
end
+ # rubocop: disable CodeReuse/ActiveRecord
def selected_target_project
if @project.id.to_s == params[:target_project_id] || !@project.forked?
@project
@@ -119,6 +126,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@project.forked_from_project
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42384')
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 48e02581d54..5307cd0720a 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController
include DiffForPath
include DiffHelper
@@ -21,7 +23,15 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def render_diffs
@environment = @merge_request.environments_for(current_user).last
- render json: DiffsSerializer.new(current_user: current_user).represent(@diffs, additional_attributes)
+ @diffs.write_cache
+
+ request = {
+ current_user: current_user,
+ project: @merge_request.project,
+ render: ->(partial, locals) { view_to_html_string(partial, locals) }
+ }
+
+ render json: DiffsSerializer.new(request).represent(@diffs, additional_attributes)
end
def define_diff_vars
@@ -32,13 +42,16 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@diffs = @compare.diffs(diff_options)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def commit
return nil unless commit_id = params[:commit_id].presence
return nil unless @merge_request.all_commits.exists?(sha: commit_id)
@commit ||= @project.commit(commit_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def find_merge_request_diff_compare
@merge_request_diff =
if diff_id = params[:diff_id].presence
@@ -66,6 +79,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@merge_request_diff
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def additional_attributes
{
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index d31b58972ca..6a5da9b8292 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationController
include ToggleSubscriptionAction
include IssuableActions
@@ -205,7 +207,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
environments =
begin
@merge_request.environments_for(current_user).map do |environment|
- project = environment.project
+ project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_sha)
stop_url =
@@ -215,7 +217,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
metrics_url =
if can?(current_user, :read_environment, environment) && environment.has_metrics?
- metrics_project_environment_deployment_path(environment.project, environment, deployment)
+ metrics_project_environment_deployment_path(project, environment, deployment)
end
metrics_monitoring_url =
@@ -330,6 +332,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@source_project = @merge_request.source_project
@target_project = @merge_request.target_project
@target_branches = @merge_request.target_project.repository.branch_names
+ @noteable = @merge_request
+
+ # FIXME: We have to assign a presenter to another instance variable
+ # due to class_name checks being made with issuable classes
+ @mr_presenter = @merge_request.present(current_user: current_user)
end
def finder_type
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index b9b3dcd5a85..20998c97730 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::MilestonesController < Projects::ApplicationController
include Gitlab::Utils::StrongMemoize
include MilestoneActions
@@ -91,12 +93,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def flash_notice_for(milestone, group)
- notice = ''.html_safe
- notice << milestone.title
- notice << ' promoted to '
- notice << view_context.link_to('<u>group milestone</u>'.html_safe, group_milestone_path(group, milestone.iid))
- notice << '.'
- notice
+ ''.html_safe + "#{milestone.title} promoted to " + view_context.link_to('<u>group milestone</u>'.html_safe, group_milestone_path(group, milestone.iid)) + '.'
end
def destroy
@@ -118,9 +115,11 @@ class Projects::MilestonesController < Projects::ApplicationController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def milestone
@milestone ||= @project.milestones.find_by!(iid: params[:id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def authorize_admin_milestone!
return render_404 unless can?(current_user, :admin_milestone, @project)
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
index 3739608e4c0..78d5faf2326 100644
--- a/app/controllers/projects/mirrors_controller.rb
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::MirrorsController < Projects::ApplicationController
include RepositorySettingsRedirect
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index 35fec229db7..ad2466a8588 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::NetworkController < Projects::ApplicationController
include ExtractsPath
include ApplicationHelper
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 21e2145b73b..4bac763d000 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::NotesController < Projects::ApplicationController
include RendersNotes
include NotesActions
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index ff49911d892..c1ad6707c97 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::PagesController < Projects::ApplicationController
layout 'project_settings'
@@ -5,9 +7,11 @@ class Projects::PagesController < Projects::ApplicationController
before_action :authorize_read_pages!, only: [:show]
before_action :authorize_update_pages!, except: [:show]
+ # rubocop: disable CodeReuse/ActiveRecord
def show
@domains = @project.pages_domains.order(:domain)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def destroy
project.remove_pages
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
index 4856be61e88..439ec9b1731 100644
--- a/app/controllers/projects/pages_domains_controller.rb
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::PagesDomainsController < Projects::ApplicationController
layout 'project_settings'
@@ -70,7 +72,9 @@ class Projects::PagesDomainsController < Projects::ApplicationController
params.require(:pages_domain).permit(:key, :certificate)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def domain
@domain ||= @project.pages_domains.find_by!(domain: params[:id].to_s)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index aeda7b3edf5..acf56f0eb6a 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :schedule, except: [:index, :new, :create]
@@ -8,12 +10,14 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create, :play]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
+ # rubocop: disable CodeReuse/ActiveRecord
def index
@scope = params[:scope]
@all_schedules = PipelineSchedulesFinder.new(@project).execute
@schedules = PipelineSchedulesFinder.new(@project).execute(scope: params[:scope])
.includes(:last_pipeline)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def new
@schedule = project.pipeline_schedules.new
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index b5db646bf57..53b29d4146e 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::PipelinesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts]
@@ -96,7 +98,7 @@ class Projects::PipelinesController < Projects::ApplicationController
render json: StageSerializer
.new(project: @project, current_user: @current_user)
- .represent(@stage, details: true)
+ .represent(@stage, details: true, retried: params[:retried])
end
# TODO: This endpoint is used by mini-pipeline-graph
@@ -159,6 +161,7 @@ class Projects::PipelinesController < Projects::ApplicationController
params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value])
end
+ # rubocop: disable CodeReuse/ActiveRecord
def pipeline
@pipeline ||= project
.pipelines
@@ -166,6 +169,7 @@ class Projects::PipelinesController < Projects::ApplicationController
.find_by!(id: params[:id])
.present(current_user: current_user)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def whitelist_query_limiting
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 73c613b26f3..192e6d38f36 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::PipelinesSettingsController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index cfa5e72af64..8938cfbad54 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
include MembersPresentation
@@ -6,6 +8,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
+ # rubocop: disable CodeReuse/ActiveRecord
def index
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@@ -25,6 +28,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
@project_member = @project.project_members.new
end
+ # rubocop: enable CodeReuse/ActiveRecord
def import
@projects = current_user.authorized_projects.order_id_desc
diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb
index c6b6243b553..3a9f9aab4a5 100644
--- a/app/controllers/projects/prometheus/metrics_controller.rb
+++ b/app/controllers/projects/prometheus/metrics_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Projects
module Prometheus
class MetricsController < Projects::ApplicationController
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index 64954ac9a42..a860be83e95 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
protected
diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb
index cc62ce2f11b..3a3a29ddd0d 100644
--- a/app/controllers/projects/protected_refs_controller.rb
+++ b/app/controllers/projects/protected_refs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::ProtectedRefsController < Projects::ApplicationController
include RepositorySettingsRedirect
diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb
index 198c938ff35..01cedba95ac 100644
--- a/app/controllers/projects/protected_tags_controller.rb
+++ b/app/controllers/projects/protected_tags_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::ProtectedTagsController < Projects::ProtectedRefsController
protected
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 91cf35bc70b..1dd5d1ff2e8 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Controller for viewing a file's raw
class Projects::RawController < Projects::ApplicationController
include ExtractsPath
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 48a09e1ddb8..b97fbe19bbf 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::RefsController < Projects::ApplicationController
include ExtractsPath
include TreeHelper
@@ -36,54 +38,47 @@ class Projects::RefsController < Projects::ApplicationController
end
def logs_tree
- @offset = if params[:offset].present?
- params[:offset].to_i
- else
- 0
- end
-
- @limit = 25
-
- @path = params[:path]
-
- contents = []
- contents.push(*tree.trees)
- contents.push(*tree.blobs)
- contents.push(*tree.submodules)
+ summary = ::Gitlab::TreeSummary.new(
+ @commit,
+ @project,
+ path: @path,
+ offset: params[:offset],
+ limit: 25
+ )
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433
- @logs = Gitlab::GitalyClient.allow_n_plus_1_calls do
- contents[@offset, @limit].to_a.map do |content|
- file = @path ? File.join(@path, content.name) : content.name
- last_commit = @repo.last_commit_for_path(@commit.id, file)
- commit_path = project_commit_path(@project, last_commit) if last_commit
- {
- file_name: content.name,
- commit: last_commit,
- type: content.type,
- commit_path: commit_path
- }
- end
- end
-
- offset = (@offset + @limit)
- if contents.size > offset
- @more_log_url = logs_file_project_ref_path(@project, @ref, @path || '', offset: offset)
- end
+ @logs, commits = summary.summarize
+ @more_log_url = more_url(summary.next_offset) if summary.more?
respond_to do |format|
format.html { render_404 }
format.json do
- response.headers["More-Logs-Url"] = @more_log_url
-
+ response.headers["More-Logs-Url"] = @more_log_url if summary.more?
render json: @logs
end
- format.js
+
+ # The commit titles must be rendered and redacted before being shown.
+ # Doing it here allows us to apply performance optimizations that avoid
+ # N+1 problems
+ format.js do
+ prerender_commit_full_titles!(commits)
+ end
end
end
private
+ def more_url(offset)
+ logs_file_project_ref_path(@project, @ref, @path, offset: offset)
+ end
+
+ def prerender_commit_full_titles!(commits)
+ # Preload commit authors as they are used in rendering
+ commits.each(&:lazy_author)
+
+ renderer = Banzai::ObjectRenderer.new(user: current_user, default_project: @project)
+ renderer.render(commits, :full_title)
+ end
+
def validate_ref_id
return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex
end
diff --git a/app/controllers/projects/registry/application_controller.rb b/app/controllers/projects/registry/application_controller.rb
index a56f9c58726..2f891d78c91 100644
--- a/app/controllers/projects/registry/application_controller.rb
+++ b/app/controllers/projects/registry/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Projects
module Registry
class ApplicationController < Projects::ApplicationController
diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb
index 32c0fc6d14a..6d60117c37d 100644
--- a/app/controllers/projects/registry/repositories_controller.rb
+++ b/app/controllers/projects/registry/repositories_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Projects
module Registry
class RepositoriesController < ::Projects::Registry::ApplicationController
@@ -18,14 +20,10 @@ module Projects
end
def destroy
- if image.destroy
- respond_to do |format|
- format.json { head :no_content }
- end
- else
- respond_to do |format|
- format.json { head :bad_request }
- end
+ DeleteContainerRepositoryWorker.perform_async(current_user.id, image.id)
+
+ respond_to do |format|
+ format.json { head :no_content }
end
end
@@ -41,10 +39,10 @@ module Projects
# Needed to maintain a backwards compatibility.
#
def ensure_root_container_repository!
- ContainerRegistry::Path.new(@project.full_path).tap do |path|
+ ::ContainerRegistry::Path.new(@project.full_path).tap do |path|
break if path.has_repository?
- ContainerRepository.build_from_path(path).tap do |repository|
+ ::ContainerRepository.build_from_path(path).tap do |repository|
repository.save! if repository.has_tags?
end
end
diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb
index e602aa3f393..567d750caae 100644
--- a/app/controllers/projects/registry/tags_controller.rb
+++ b/app/controllers/projects/registry/tags_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Projects
module Registry
class TagsController < ::Projects::Registry::ApplicationController
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 19e09b3af6f..55827075896 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::ReleasesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
@@ -28,9 +30,11 @@ class Projects::ReleasesController < Projects::ApplicationController
@tag ||= @repository.find_tag(params[:tag_id])
end
+ # rubocop: disable CodeReuse/ActiveRecord
def release
@release ||= @project.releases.find_or_initialize_by(tag: @tag.name)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def release_params
params.require(:release).permit(:description)
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index ecb2ece7532..4eeaeb860ee 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::RepositoriesController < Projects::ApplicationController
include ExtractsPath
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index c098c82081e..cbeb32fd610 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::RunnerProjectsController < Projects::ApplicationController
before_action :authorize_admin_build!
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index d118cec977c..91f40b90aa8 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::RunnersController < Projects::ApplicationController
before_action :authorize_admin_build!
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index d55046047ae..f1c9d0d0f77 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::ServicesController < Projects::ApplicationController
include ServiceParams
diff --git a/app/controllers/projects/settings/badges_controller.rb b/app/controllers/projects/settings/badges_controller.rb
deleted file mode 100644
index 7887bee49c5..00000000000
--- a/app/controllers/projects/settings/badges_controller.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Projects
- module Settings
- class BadgesController < Projects::ApplicationController
- include API::Helpers::RelatedResourcesHelpers
-
- before_action :authorize_admin_project!
-
- def index
- @badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id))
- end
- end
- end
-end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 322ec096ffb..3a1344651df 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Projects
module Settings
class CiCdController < Projects::ApplicationController
@@ -34,6 +36,13 @@ module Projects
end
end
+ def reset_registration_token
+ @project.reset_runners_token!
+
+ flash[:notice] = 'New runners registration token has been generated!'
+ redirect_to namespace_project_settings_ci_cd_path
+ end
+
private
def update_params
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
index d9fecfecc40..388fcb32c35 100644
--- a/app/controllers/projects/settings/integrations_controller.rb
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Projects
module Settings
class IntegrationsController < Projects::ApplicationController
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 4697af4f26a..1d76c90d4eb 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Projects
module Settings
class RepositoryController < Projects::ApplicationController
@@ -31,6 +33,7 @@ module Projects
render 'show'
end
+ # rubocop: disable CodeReuse/ActiveRecord
def define_protected_refs
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
@protected_tags = @project.protected_tags.order(:name).page(params[:page])
@@ -42,6 +45,7 @@ module Projects
load_gon_index
end
+ # rubocop: enable CodeReuse/ActiveRecord
def remote_mirror
@remote_mirror = project.remote_mirrors.first_or_initialize
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 7c03d8ce827..a44acb12bdf 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::SnippetsController < Projects::ApplicationController
include RendersNotes
include ToggleAwardEmoji
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index b17753222a0..c8442ff3592 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::TagsController < Projects::ApplicationController
include SortingHelper
@@ -7,6 +9,7 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_push_code!, only: [:new, :create]
before_action :authorize_admin_project!, only: [:destroy]
+ # rubocop: disable CodeReuse/ActiveRecord
def index
params[:sort] = params[:sort].presence || sort_value_recently_updated
@@ -17,8 +20,15 @@ class Projects::TagsController < Projects::ApplicationController
tag_names = @tags.map(&:name)
@tags_pipelines = @project.pipelines.latest_successful_for_refs(tag_names)
@releases = project.releases.where(tag: tag_names)
+
+ respond_to do |format|
+ format.html
+ format.atom { render layout: 'xml.atom' }
+ end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def show
@tag = @repository.find_tag(params[:id])
@@ -27,6 +37,7 @@ class Projects::TagsController < Projects::ApplicationController
@release = @project.releases.find_or_initialize_by(tag: @tag.name)
@commit = @repository.commit(@tag.dereferenced_target)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def create
result = Tags::CreateService.new(@project, current_user)
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
index 52d6fb82093..7ceea4e5b96 100644
--- a/app/controllers/projects/templates_controller.rb
+++ b/app/controllers/projects/templates_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::TemplatesController < Projects::ApplicationController
before_action :authenticate_user!, :get_template_class
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index 93fb9da6510..0b11ee9edc0 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::TodosController < Projects::ApplicationController
include Gitlab::Utils::StrongMemoize
include TodosActions
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index ee9b5458282..3fe300dcfc0 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Controller for viewing a repository's file structure
class Projects::TreeController < Projects::ApplicationController
include ExtractsPath
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index cb12b707087..f5fdfb8accc 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::TriggersController < Projects::ApplicationController
before_action :authorize_admin_build!
before_action :authorize_manage_trigger!, except: [:index, :create]
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index 7a85046164c..4ffcc2ac805 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::UploadsController < Projects::ApplicationController
include UploadsActions
include WorkhorseRequest
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index bf09ea7e4d8..bb658bfcc19 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::VariablesController < Projects::ApplicationController
before_action :authorize_admin_build!
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index da7aeb26a75..8c6d87a421f 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Projects::WikisController < Projects::ApplicationController
include PreviewMarkdown
include Gitlab::Utils::StrongMemoize
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index e9ae8c13142..ee438e160f2 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class ProjectsController < Projects::ApplicationController
+ include API::Helpers::RelatedResourcesHelpers
include IssuableCollections
include ExtractsPath
include PreviewMarkdown
@@ -13,6 +16,7 @@ class ProjectsController < Projects::ApplicationController
before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
+ before_action :present_project, only: [:edit]
# Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
@@ -24,14 +28,17 @@ class ProjectsController < Projects::ApplicationController
redirect_to(current_user ? root_path : explore_root_path)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def new
namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id]
return access_denied! if namespace && !can?(current_user, :create_projects, namespace)
@project = Project.new(namespace_id: namespace&.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def edit
+ @badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id))
render 'edit'
end
@@ -73,6 +80,7 @@ class ProjectsController < Projects::ApplicationController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def transfer
return access_denied! unless can?(current_user, :change_namespace, @project)
@@ -83,6 +91,7 @@ class ProjectsController < Projects::ApplicationController
flash[:alert] = @project.errors[:new_namespace].first
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def remove_fork
return access_denied! unless can?(current_user, :remove_fork_project, @project)
@@ -189,10 +198,8 @@ class ProjectsController < Projects::ApplicationController
end
def download_export
- if export_project_object_storage?
- send_upload(@project.import_export_upload.export_file)
- elsif export_project_path
- send_file export_project_path, disposition: 'attachment'
+ if @project.export_file_exists?
+ send_upload(@project.export_file, attachment: @project.export_file.filename)
else
redirect_to(
edit_project_path(@project, anchor: 'js-export-project'),
@@ -231,6 +238,7 @@ class ProjectsController < Projects::ApplicationController
}
end
+ # rubocop: disable CodeReuse/ActiveRecord
def refs
find_refs = params['find']
@@ -265,6 +273,7 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Render project landing depending of which features are available
# So if page is not availble in the list it renders the next page
@@ -303,6 +312,7 @@ class ProjectsController < Projects::ApplicationController
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def load_events
projects = Project.where(id: @project.id)
@@ -312,6 +322,7 @@ class ProjectsController < Projects::ApplicationController
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def project_params
params.require(:project)
@@ -355,6 +366,7 @@ class ProjectsController < Projects::ApplicationController
repository_access_level
snippets_access_level
wiki_access_level
+ pages_access_level
]
]
end
@@ -424,11 +436,7 @@ class ProjectsController < Projects::ApplicationController
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440')
end
- def export_project_path
- @export_project_path ||= @project.export_project_path
- end
-
- def export_project_object_storage?
- @project.export_project_object_exists?
+ def present_project
+ @project = @project.present(current_user: current_user)
end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index e6d6965036e..8b8d87524a8 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
include AcceptsPendingInvitations
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 651b82f04f4..ebf70f25bda 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# RootController
#
# This controller exists solely to handle requests to `root_url`. When a user is
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 983f888b8ec..1b22907c10f 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SearchController < ApplicationController
include ControllerWithCrossProjectAccessCheck
include SearchHelper
@@ -31,6 +33,7 @@ class SearchController < ApplicationController
check_single_commit_result
end
+ # rubocop: disable CodeReuse/ActiveRecord
def autocomplete
term = params[:term]
@@ -43,6 +46,7 @@ class SearchController < ApplicationController
render json: search_autocomplete_opts(term).to_json
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 93a71103a09..2b76921ebd8 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SentNotificationsController < ApplicationController
skip_before_action :authenticate_user!
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index ab8e2e35b98..643eb75c83c 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SessionsController < Devise::SessionsController
include InternalRedirect
include AuthenticatesWithTwoFactor
@@ -107,6 +109,7 @@ class SessionsController < Devise::SessionsController
# Handle an "initial setup" state, where there's only one user, it's an admin,
# and they require a password change.
+ # rubocop: disable CodeReuse/ActiveRecord
def check_initial_setup
return unless User.limit(2).count == 1 # Count as much 2 to know if we have exactly one
@@ -121,6 +124,7 @@ class SessionsController < Devise::SessionsController
redirect_to edit_user_password_path(reset_password_token: @token),
notice: "Please create a password for your new account."
end
+ # rubocop: enable CodeReuse/ActiveRecord
def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
diff --git a/app/controllers/sherlock/application_controller.rb b/app/controllers/sherlock/application_controller.rb
index 6bdd3568a78..c048254d348 100644
--- a/app/controllers/sherlock/application_controller.rb
+++ b/app/controllers/sherlock/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Sherlock
class ApplicationController < ::ApplicationController
before_action :find_transaction
diff --git a/app/controllers/sherlock/file_samples_controller.rb b/app/controllers/sherlock/file_samples_controller.rb
index 0c3bc100106..900446bb75a 100644
--- a/app/controllers/sherlock/file_samples_controller.rb
+++ b/app/controllers/sherlock/file_samples_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Sherlock
class FileSamplesController < Sherlock::ApplicationController
def show
diff --git a/app/controllers/sherlock/queries_controller.rb b/app/controllers/sherlock/queries_controller.rb
index 63b26aab1a4..49a25c682b5 100644
--- a/app/controllers/sherlock/queries_controller.rb
+++ b/app/controllers/sherlock/queries_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Sherlock
class QueriesController < Sherlock::ApplicationController
def show
diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb
index ae4953c3259..46e382e594e 100644
--- a/app/controllers/sherlock/transactions_controller.rb
+++ b/app/controllers/sherlock/transactions_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Sherlock
class TransactionsController < Sherlock::ApplicationController
def index
diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb
index 217da89a1fd..091bcb1253d 100644
--- a/app/controllers/snippets/notes_controller.rb
+++ b/app/controllers/snippets/notes_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Snippets::NotesController < ApplicationController
include NotesActions
include ToggleAwardEmoji
@@ -17,9 +19,11 @@ class Snippets::NotesController < ApplicationController
nil
end
+ # rubocop: disable CodeReuse/ActiveRecord
def snippet
PersonalSnippet.find_by(id: params[:snippet_id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
alias_method :noteable, :snippet
def note_params
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index dcf18c1f751..694c3a59e2b 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SnippetsController < ApplicationController
include RendersNotes
include ToggleAwardEmoji
@@ -24,6 +26,7 @@ class SnippetsController < ApplicationController
layout 'snippets'
respond_to :html
+ # rubocop: disable CodeReuse/ActiveRecord
def index
if params[:username].present?
@user = User.find_by(username: params[:username])
@@ -38,6 +41,7 @@ class SnippetsController < ApplicationController
redirect_to(current_user ? dashboard_snippets_path : explore_snippets_path)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def new
@snippet = PersonalSnippet.new
@@ -94,9 +98,11 @@ class SnippetsController < ApplicationController
protected
+ # rubocop: disable CodeReuse/ActiveRecord
def snippet
@snippet ||= PersonalSnippet.inc_relations_for_view.find_by(id: params[:id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
alias_method :awardable, :snippet
alias_method :spammable, :snippet
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 3d227b0a955..fa5d84633b5 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UploadsController < ApplicationController
include UploadsActions
diff --git a/app/controllers/user_callouts_controller.rb b/app/controllers/user_callouts_controller.rb
index 18cde4a7b1a..ebf1dd8ca02 100644
--- a/app/controllers/user_callouts_controller.rb
+++ b/app/controllers/user_callouts_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UserCalloutsController < ApplicationController
def create
if ensure_callout.persisted?
@@ -13,9 +15,11 @@ class UserCalloutsController < ApplicationController
private
+ # rubocop: disable CodeReuse/ActiveRecord
def ensure_callout
current_user.callouts.find_or_create_by(feature_name: UserCallout.feature_names[feature_name])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def feature_name
params.require(:feature_name)
diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb
index 1b1560a2a00..3c16d934b4d 100644
--- a/app/controllers/users/terms_controller.rb
+++ b/app/controllers/users/terms_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Users
class TermsController < ApplicationController
include InternalRedirect
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 2f65f4a7403..d16240af404 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UsersController < ApplicationController
include RoutableActions
include RendersMemberAccess
@@ -27,11 +29,17 @@ class UsersController < ApplicationController
format.json do
load_events
- pager_json("events/_events", @events.count)
+ pager_json("events/_events", @events.count, events: @events)
end
end
end
+ def activity
+ respond_to do |format|
+ format.html { render 'show' }
+ end
+ end
+
def groups
load_groups
@@ -51,9 +59,7 @@ class UsersController < ApplicationController
respond_to do |format|
format.html { render 'show' }
format.json do
- render json: {
- html: view_to_html_string("shared/projects/_list", projects: @projects)
- }
+ pager_json("shared/projects/_list", @projects.count, projects: @projects)
end
end
end
@@ -123,6 +129,7 @@ class UsersController < ApplicationController
@projects =
PersonalProjectsFinder.new(user).execute(current_user)
.page(params[:page])
+ .per(params[:limit])
prepare_projects_for_rendering(@projects)
end
diff --git a/app/finders/access_requests_finder.rb b/app/finders/access_requests_finder.rb
index b6ee49df99b..2cc8a978877 100644
--- a/app/finders/access_requests_finder.rb
+++ b/app/finders/access_requests_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AccessRequestsFinder
attr_accessor :source
diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb
index 543bf1a1415..e2b9b0b44c1 100644
--- a/app/finders/admin/projects_finder.rb
+++ b/app/finders/admin/projects_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Admin::ProjectsFinder
attr_reader :params, :current_user
@@ -6,6 +8,7 @@ class Admin::ProjectsFinder
@current_user = current_user
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
items = Project.without_deleted.with_statistics.with_route
items = by_namespace_id(items)
@@ -19,6 +22,7 @@ class Admin::ProjectsFinder
items = items.includes(namespace: [:owner, :route])
sort(items).page(params[:page])
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
@@ -26,9 +30,11 @@ class Admin::ProjectsFinder
params[:namespace_id].present? ? items.in_namespace(params[:namespace_id]) : items
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_visibilty_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_with_push(items)
params[:with_push].present? ? items.with_push : items
@@ -38,9 +44,11 @@ class Admin::ProjectsFinder
params[:abandoned].present? ? items.abandoned : items
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_last_repository_check_failed(items)
params[:last_repository_check_failed].present? ? items.where(last_repository_check_failed: true) : items
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_archived(items)
if params[:archived] == 'only'
diff --git a/app/finders/admin/runners_finder.rb b/app/finders/admin/runners_finder.rb
new file mode 100644
index 00000000000..fbb1cfc5c66
--- /dev/null
+++ b/app/finders/admin/runners_finder.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+class Admin::RunnersFinder < UnionFinder
+ NUMBER_OF_RUNNERS_PER_PAGE = 30
+
+ def initialize(params:)
+ @params = params
+ end
+
+ def execute
+ search!
+ filter_by_status!
+ filter_by_runner_type!
+ sort!
+ paginate!
+
+ @runners
+ end
+
+ def sort_key
+ if @params[:sort] == 'contacted_asc'
+ 'contacted_asc'
+ else
+ 'created_date'
+ end
+ end
+
+ private
+
+ def search!
+ @runners =
+ if @params[:search].present?
+ Ci::Runner.search(@params[:search])
+ else
+ Ci::Runner.all
+ end
+ end
+
+ def filter_by_status!
+ filter_by!(:status_status, Ci::Runner::AVAILABLE_STATUSES)
+ end
+
+ def filter_by_runner_type!
+ filter_by!(:type_type, Ci::Runner::AVAILABLE_TYPES)
+ end
+
+ def sort!
+ @runners = @runners.order_by(sort_key)
+ end
+
+ def paginate!
+ @runners = @runners.page(@params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
+ end
+
+ def filter_by!(scope_name, available_scopes)
+ scope = @params[scope_name]
+
+ if scope.present? && available_scopes.include?(scope)
+ @runners = @runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+end
diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb
index b2557469079..e2283f3266e 100644
--- a/app/finders/autocomplete/users_finder.rb
+++ b/app/finders/autocomplete/users_finder.rb
@@ -44,6 +44,7 @@ module Autocomplete
# Returns the users based on the input parameters, as an Array.
#
# This method is separate so it is easier to extend in EE.
+ # rubocop: disable CodeReuse/ActiveRecord
def limited_users
# When changing the order of these method calls, make sure that
# reorder_by_name() is called _before_ optionally_search(), otherwise
@@ -61,6 +62,7 @@ module Autocomplete
.limit(LIMIT)
.to_a
end
+ # rubocop: enable CodeReuse/ActiveRecord
def prepend_current_user?
filter_by_current_user.present? && current_user
@@ -70,6 +72,7 @@ module Autocomplete
author_id.present? && current_user
end
+ # rubocop: disable CodeReuse/ActiveRecord
def find_users
if project
project.authorized_users.union_with_user(author_id)
@@ -81,5 +84,6 @@ module Autocomplete
User.none
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb
index 8bb1366867c..970efa79dfb 100644
--- a/app/finders/branches_finder.rb
+++ b/app/finders/branches_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BranchesFinder
def initialize(repository, params = {})
@repository = repository
diff --git a/app/finders/clusters_finder.rb b/app/finders/clusters_finder.rb
index c13f98257bf..b40d6c41b71 100644
--- a/app/finders/clusters_finder.rb
+++ b/app/finders/clusters_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClustersFinder
def initialize(project, user, scope)
@project = project
diff --git a/app/finders/concerns/created_at_filter.rb b/app/finders/concerns/created_at_filter.rb
index ac9ac77732c..6b5863a5c53 100644
--- a/app/finders/concerns/created_at_filter.rb
+++ b/app/finders/concerns/created_at_filter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module CreatedAtFilter
def by_created_at(items)
items = items.created_before(params[:created_before]) if params[:created_before].present?
diff --git a/app/finders/concerns/custom_attributes_filter.rb b/app/finders/concerns/custom_attributes_filter.rb
index 5bbf9ca242d..825c3a6b5b7 100644
--- a/app/finders/concerns/custom_attributes_filter.rb
+++ b/app/finders/concerns/custom_attributes_filter.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
module CustomAttributesFilter
+ # rubocop: disable CodeReuse/ActiveRecord
def by_custom_attributes(items)
return items unless params[:custom_attributes].is_a?(Hash)
return items unless Ability.allowed?(current_user, :read_custom_attribute)
@@ -17,4 +20,5 @@ module CustomAttributesFilter
scope.where('EXISTS (?)', custom_attributes.where(key: key, value: value))
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/concerns/finder_methods.rb b/app/finders/concerns/finder_methods.rb
index 2e905fa5750..5290313585f 100644
--- a/app/finders/concerns/finder_methods.rb
+++ b/app/finders/concerns/finder_methods.rb
@@ -1,11 +1,17 @@
+# frozen_string_literal: true
+
module FinderMethods
+ # rubocop: disable CodeReuse/ActiveRecord
def find_by!(*args)
raise_not_found_unless_authorized execute.find_by!(*args)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def find_by(*args)
if_authorized execute.find_by(*args)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def find(*args)
raise_not_found_unless_authorized model.find(*args)
diff --git a/app/finders/concerns/finder_with_cross_project_access.rb b/app/finders/concerns/finder_with_cross_project_access.rb
index 92bf98d7cd2..e038636f0c4 100644
--- a/app/finders/concerns/finder_with_cross_project_access.rb
+++ b/app/finders/concerns/finder_with_cross_project_access.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Module to prepend into finders to specify wether or not the finder requires
# cross project access
#
@@ -14,6 +16,7 @@ module FinderWithCrossProjectAccess
end
override :execute
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(*args)
check = Gitlab::CrossProjectAccess.find_check(self)
original = super
@@ -27,6 +30,7 @@ module FinderWithCrossProjectAccess
original
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
# We can skip the cross project check for finding indivitual records.
# this would be handled by the `can?(:read_*, result)` call in `FinderMethods`
diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb
index a685719555c..c1ef9dfefa7 100644
--- a/app/finders/contributed_projects_finder.rb
+++ b/app/finders/contributed_projects_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ContributedProjectsFinder < UnionFinder
def initialize(user)
@user = user
@@ -10,11 +12,13 @@ class ContributedProjectsFinder < UnionFinder
# visible by this user.
#
# Returns an ActiveRecord::Relation.
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(current_user = nil)
segments = all_projects(current_user)
find_union(segments, Project).includes(:namespace).order_id_desc
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb
index a59f8c1efa3..419be46fafe 100644
--- a/app/finders/environments_finder.rb
+++ b/app/finders/environments_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EnvironmentsFinder
attr_reader :project, :current_user, :params
@@ -5,6 +7,7 @@ class EnvironmentsFinder
@project, @current_user, @params = project, current_user, params
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
deployments = project.deployments
deployments =
@@ -42,6 +45,7 @@ class EnvironmentsFinder
environments
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb
index 8676925a540..2e82bda8730 100644
--- a/app/finders/events_finder.rb
+++ b/app/finders/events_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EventsFinder
prepend FinderMethods
prepend FinderWithCrossProjectAccess
@@ -10,6 +12,7 @@ class EventsFinder
# Arguments:
# source - which user or project to looks for events on
# current_user - only return events for projects visible to this user
+ # WARNING: does not consider project feature visibility!
# params:
# action: string
# target_type: string
@@ -36,32 +39,42 @@ class EventsFinder
private
+ # rubocop: disable CodeReuse/ActiveRecord
def by_current_user_access(events)
- events.merge(ProjectsFinder.new(current_user: current_user).execute)
+ events.merge(ProjectsFinder.new(current_user: current_user).execute) # rubocop: disable CodeReuse/Finder
.joins(:project)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_action(events)
return events unless Event::ACTIONS[params[:action]]
events.where(action: Event::ACTIONS[params[:action]])
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_target_type(events)
return events unless Event::TARGET_TYPES[params[:target_type]]
events.where(target_type: Event::TARGET_TYPES[params[:target_type]])
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_created_at_before(events)
return events unless params[:before]
events.where('events.created_at < ?', params[:before].beginning_of_day)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_created_at_after(events)
return events unless params[:after]
events.where('events.created_at > ?', params[:after].end_of_day)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/fork_projects_finder.rb b/app/finders/fork_projects_finder.rb
index 28d1b31868e..03ace7e8057 100644
--- a/app/finders/fork_projects_finder.rb
+++ b/app/finders/fork_projects_finder.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
class ForkProjectsFinder < ProjectsFinder
+ # rubocop: disable CodeReuse/ActiveRecord
def initialize(project, params: {}, current_user: nil)
project_ids = project.forks.includes(:creator).select(:id)
super(params: params, current_user: current_user, project_ids_relation: project_ids)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index 051ea108e06..9d57d2d3bc9 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# GroupDescendantsFinder
#
# Used to find and filter all subgroups and projects of a passed parent group
@@ -61,12 +63,16 @@ class GroupDescendantsFinder
end
def direct_child_groups
+ # rubocop: disable CodeReuse/Finder
GroupsFinder.new(current_user,
parent: parent_group,
all_available: true).execute
+ # rubocop: enable CodeReuse/Finder
end
+ # rubocop: disable CodeReuse/ActiveRecord
def all_visible_descendant_groups
+ # rubocop: disable CodeReuse/Finder
groups_table = Group.arel_table
visible_to_user = groups_table[:visibility_level]
.in(Gitlab::VisibilityLevel.levels_for_user(current_user))
@@ -84,7 +90,9 @@ class GroupDescendantsFinder
hierarchy_for_parent
.descendants
.where(visible_to_user)
+ # rubocop: enable CodeReuse/Finder
end
+ # rubocop: enable CodeReuse/ActiveRecord
def subgroups_matching_filter
all_visible_descendant_groups
@@ -101,24 +109,29 @@ class GroupDescendantsFinder
#
# So when searching 'project', on the 'subgroup' page we want to preload
# 'nested-group' but not 'subgroup' or 'root'
+ # rubocop: disable CodeReuse/ActiveRecord
def ancestors_of_groups(base_for_ancestors)
group_ids = base_for_ancestors.except(:select, :sort).select(:id)
Gitlab::GroupHierarchy.new(Group.where(id: group_ids))
.base_and_ancestors(upto: parent_group.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def ancestors_of_filtered_projects
projects_to_load_ancestors_of = projects.where.not(namespace: parent_group)
groups_to_load_ancestors_of = Group.where(id: projects_to_load_ancestors_of.select(:namespace_id))
ancestors_of_groups(groups_to_load_ancestors_of)
.with_selects_for_list(archived: params[:archived])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def ancestors_of_filtered_subgroups
ancestors_of_groups(subgroups)
.with_selects_for_list(archived: params[:archived])
end
+ # rubocop: disable CodeReuse/ActiveRecord
def subgroups
return Group.none unless Group.supports_nested_groups?
@@ -132,22 +145,29 @@ class GroupDescendantsFinder
groups.with_selects_for_list(archived: params[:archived]).order_by(sort)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/Finder
def direct_child_projects
- GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
+ GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params, options: { only_owned: true })
.execute
end
+ # rubocop: enable CodeReuse/Finder
# Finds all projects nested under `parent_group` or any of its descendant
# groups
+ # rubocop: disable CodeReuse/ActiveRecord
def projects_matching_filter
+ # rubocop: disable CodeReuse/Finder
projects_nested_in_group = Project.where(namespace_id: hierarchy_for_parent.base_and_descendants.select(:id))
params_with_search = params.merge(search: params[:filter])
ProjectsFinder.new(params: params_with_search,
current_user: current_user,
project_ids_relation: projects_nested_in_group).execute
+ # rubocop: enable CodeReuse/Finder
end
+ # rubocop: enable CodeReuse/ActiveRecord
def projects
projects = if params[:filter]
@@ -163,7 +183,9 @@ class GroupDescendantsFinder
params.fetch(:sort, 'id_asc')
end
+ # rubocop: disable CodeReuse/ActiveRecord
def hierarchy_for_parent
@hierarchy ||= Gitlab::GroupHierarchy.new(Group.where(id: parent_group.id))
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/group_finder.rb b/app/finders/group_finder.rb
index 24c84d2d1aa..d2ad8a372b1 100644
--- a/app/finders/group_finder.rb
+++ b/app/finders/group_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GroupFinder
include Gitlab::Allowable
@@ -5,6 +7,7 @@ class GroupFinder
@current_user = current_user
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(*params)
group = Group.find_by(*params)
@@ -14,4 +17,5 @@ class GroupFinder
nil
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/group_labels_finder.rb b/app/finders/group_labels_finder.rb
new file mode 100644
index 00000000000..a668a0f0fae
--- /dev/null
+++ b/app/finders/group_labels_finder.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class GroupLabelsFinder
+ attr_reader :current_user, :group, :params
+
+ def initialize(current_user, group, params = {})
+ @current_user = current_user
+ @group = group
+ @params = params
+ end
+
+ def execute
+ group.labels
+ .optionally_subscribed_by(subscriber_id)
+ .optionally_search(params[:search])
+ .order_by(params[:sort])
+ .page(params[:page])
+ end
+
+ private
+
+ def subscriber_id
+ current_user&.id if subscribed?
+ end
+
+ def subscribed?
+ params[:subscribed] == 'true'
+ end
+end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 2a656c0d31c..eebc67cfa9e 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
class GroupMembersFinder
def initialize(group)
@group = group
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(include_descendants: false)
group_members = @group.members
wheres = []
@@ -29,4 +32,5 @@ class GroupMembersFinder
GroupMember.where(wheres.join(' OR '))
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index b6bdb2b7b0f..4155b6af8da 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# GroupProjectsFinder
#
# Used to filter Projects by set of params
@@ -82,6 +84,7 @@ class GroupProjectsFinder < ProjectsFinder
options.fetch(:include_subgroups, false)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def owned_projects
if include_subgroups?
Project.where(namespace_id: group.self_and_descendants.select(:id))
@@ -89,6 +92,7 @@ class GroupProjectsFinder < ProjectsFinder
group.projects
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def shared_projects
group.shared_projects
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 0eeba1d2428..a35a3ed6142 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# GroupsFinder
#
# Used to filter Groups by a set of params
@@ -38,6 +40,7 @@ class GroupsFinder < UnionFinder
attr_reader :current_user, :params
+ # rubocop: disable CodeReuse/ActiveRecord
def all_groups
return [owned_groups] if params[:owned]
return [groups_with_min_access_level] if min_access_level?
@@ -49,6 +52,7 @@ class GroupsFinder < UnionFinder
groups << Group.none if groups.empty?
groups
end
+ # rubocop: enable CodeReuse/ActiveRecord
def groups_for_ancestors
current_user.authorized_groups
@@ -58,6 +62,7 @@ class GroupsFinder < UnionFinder
current_user.groups
end
+ # rubocop: disable CodeReuse/ActiveRecord
def groups_with_min_access_level
groups = current_user
.groups
@@ -67,16 +72,21 @@ class GroupsFinder < UnionFinder
.new(groups)
.base_and_descendants
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_parent(groups)
return groups unless params[:parent]
groups.where(parent: params[:parent])
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def owned_groups
current_user&.owned_groups || Group.none
end
+ # rubocop: enable CodeReuse/ActiveRecord
def include_public_groups?
current_user.nil? || all_available?
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 372e2a96c2c..1f98ecf95ca 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# IssuableFinder
#
# Used to filter Issues and MergeRequests collections by set of params
@@ -109,6 +111,7 @@ class IssuableFinder
# (even if that query is slower than any of the individual state queries) and
# grouping and counting within that query.
#
+ # rubocop: disable CodeReuse/ActiveRecord
def count_by_state
count_params = params.merge(state: nil, sort: nil)
finder = self.class.new(current_user, count_params)
@@ -125,13 +128,14 @@ class IssuableFinder
labels_count = 1 if use_cte_for_search?
finder.execute.reorder(nil).group(:state).count.each do |key, value|
- counts[Array(key).last.to_sym] += value / labels_count
+ counts[count_key(key)] += value / labels_count
end
counts[:all] = counts.values.sum
counts.with_indifferent_access
end
+ # rubocop: enable CodeReuse/ActiveRecord
def group
return @group if defined?(@group)
@@ -157,6 +161,7 @@ class IssuableFinder
@project = project
end
+ # rubocop: disable CodeReuse/ActiveRecord
def projects(items = nil)
return @projects = project if project?
@@ -165,13 +170,14 @@ class IssuableFinder
current_user.authorized_projects
elsif group
finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
- GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute
+ GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute # rubocop: disable CodeReuse/Finder
else
- ProjectsFinder.new(current_user: current_user).execute
+ ProjectsFinder.new(current_user: current_user).execute # rubocop: disable CodeReuse/Finder
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def search
params[:search].presence
@@ -185,6 +191,7 @@ class IssuableFinder
milestones? && params[:milestone_title] == Milestone::None.title
end
+ # rubocop: disable CodeReuse/ActiveRecord
def milestones
return @milestones if defined?(@milestones)
@@ -200,11 +207,12 @@ class IssuableFinder
search_params =
{ title: params[:milestone_title], project_ids: project_id, group_ids: group_id }
- MilestonesFinder.new(search_params).execute
+ MilestonesFinder.new(search_params).execute # rubocop: disable CodeReuse/Finder
else
Milestone.none
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def labels?
params[:label_name].present?
@@ -214,30 +222,33 @@ class IssuableFinder
labels? && params[:label_name].include?(Label::None.title)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def labels
return @labels if defined?(@labels)
@labels =
if labels? && !filter_by_no_label?
- LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true)
+ LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true) # rubocop: disable CodeReuse/Finder
else
Label.none
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def assignee_id?
- params[:assignee_id].present? && params[:assignee_id] != NONE
+ params[:assignee_id].present? && params[:assignee_id].to_s != NONE
end
def assignee_username?
- params[:assignee_username].present? && params[:assignee_username] != NONE
+ params[:assignee_username].present? && params[:assignee_username].to_s != NONE
end
def no_assignee?
# Assignee_id takes precedence over assignee_username
- params[:assignee_id] == NONE || params[:assignee_username] == NONE
+ params[:assignee_id].to_s == NONE || params[:assignee_username].to_s == NONE
end
+ # rubocop: disable CodeReuse/ActiveRecord
def assignee
return @assignee if defined?(@assignee)
@@ -250,6 +261,7 @@ class IssuableFinder
nil
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def author_id?
params[:author_id].present? && params[:author_id] != NONE
@@ -264,6 +276,7 @@ class IssuableFinder
params[:author_id] == NONE || params[:author_username] == NONE
end
+ # rubocop: disable CodeReuse/ActiveRecord
def author
return @author if defined?(@author)
@@ -276,6 +289,7 @@ class IssuableFinder
nil
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
@@ -283,6 +297,11 @@ class IssuableFinder
klass.all
end
+ def count_key(value)
+ Array(value).last.to_sym
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
def by_scope(items)
return items.none if current_user_related? && !current_user
@@ -295,6 +314,7 @@ class IssuableFinder
items
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_updated_at(items)
items = items.updated_after(params[:updated_after]) if params[:updated_after].present?
@@ -303,6 +323,7 @@ class IssuableFinder
items
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_state(items)
case params[:state].to_s
when 'closed'
@@ -317,12 +338,14 @@ class IssuableFinder
items
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_group(items)
# Selection by group is already covered by `by_project` and `projects`
items
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_project(items)
items =
if project?
@@ -335,14 +358,17 @@ class IssuableFinder
items
end
+ # rubocop: enable CodeReuse/ActiveRecord
def use_cte_for_search?
return false unless search
return false unless Gitlab::Database.postgresql?
+ return false unless Feature.enabled?(:use_cte_for_group_issues_search, default_enabled: true)
params[:use_cte_for_search]
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_search(items)
return items unless search
@@ -355,17 +381,23 @@ class IssuableFinder
items.full_search(search)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_iids(items)
params[:iids].present? ? items.where(iid: params[:iids]) : items
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def sort(items)
# Ensure we always have an explicit sort order (instead of inheriting
# multiple orders when combining ActiveRecord::Relation objects).
params[:sort] ? items.sort_by_attribute(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_assignee(items)
if assignee
items = items.where(assignee_id: assignee.id)
@@ -377,7 +409,9 @@ class IssuableFinder
items
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_author(items)
if author
items = items.where(author_id: author.id)
@@ -389,19 +423,27 @@ class IssuableFinder
items
end
+ # rubocop: enable CodeReuse/ActiveRecord
def filter_by_upcoming_milestone?
params[:milestone_title] == Milestone::Upcoming.name
end
+ def filter_by_any_milestone?
+ params[:milestone_title] == Milestone::Any.title
+ end
+
def filter_by_started_milestone?
params[:milestone_title] == Milestone::Started.name
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
items = items.left_joins_milestones.where(milestone_id: [-1, nil])
+ elsif filter_by_any_milestone?
+ items = items.any_milestone
elsif filter_by_upcoming_milestone?
upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
@@ -414,6 +456,7 @@ class IssuableFinder
items
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_label(items)
return items unless labels?
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 24a6b9349a0..abdc47b9866 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Finders::Issues class
#
# Used to filter Issues collections by set of params
@@ -29,10 +31,13 @@ class IssuesFinder < IssuableFinder
@scalar_params ||= super + [:due_date]
end
+ # rubocop: disable CodeReuse/ActiveRecord
def klass
Issue.includes(:author)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def with_confidentiality_access_check
return Issue.all if user_can_see_all_confidential_issues?
return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues?
@@ -46,6 +51,7 @@ class IssuesFinder < IssuableFinder
user_id: current_user.id,
project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id))
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
@@ -114,9 +120,13 @@ class IssuesFinder < IssuableFinder
return @user_can_see_all_confidential_issues = true if current_user.full_private_access?
@user_can_see_all_confidential_issues =
- project? &&
- project &&
- project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
+ if project? && project
+ project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
+ elsif group
+ group.max_member_access_for_user(current_user) >= CONFIDENTIAL_ACCESS_LEVEL
+ else
+ false
+ end
end
def user_cannot_see_confidential_issues?
@@ -125,6 +135,7 @@ class IssuesFinder < IssuableFinder
current_user.blank?
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_assignee(items)
if assignee
items.assigned_to(assignee)
@@ -136,4 +147,5 @@ class IssuesFinder < IssuableFinder
items
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb
index 47174980258..4d8128dd824 100644
--- a/app/finders/joined_groups_finder.rb
+++ b/app/finders/joined_groups_finder.rb
@@ -1,4 +1,6 @@
-class JoinedGroupsFinder < UnionFinder
+# frozen_string_literal: true
+
+class JoinedGroupsFinder
def initialize(user)
@user = user
end
@@ -6,19 +8,8 @@ class JoinedGroupsFinder < UnionFinder
# Finds the groups of the source user, optionally limited to those visible to
# the current user.
def execute(current_user = nil)
- segments = all_groups(current_user)
-
- find_union(segments, Group).order_id_desc
- end
-
- private
-
- def all_groups(current_user)
- groups = []
-
- groups << @user.authorized_groups.visible_to_user(current_user) if current_user
- groups << @user.authorized_groups.public_to_user(current_user)
-
- groups
+ @user.authorized_groups
+ .public_or_visible_to_user(current_user)
+ .order_id_desc
end
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 1d05bf28438..d000af21be3 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LabelsFinder < UnionFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
@@ -10,18 +12,22 @@ class LabelsFinder < UnionFinder
@params = params
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(skip_authorization: false)
@skip_authorization = skip_authorization
items = find_union(label_ids, Label) || Label.none
items = with_title(items)
+ items = by_subscription(items)
items = by_search(items)
sort(items)
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
attr_reader :current_user, :params, :skip_authorization
+ # rubocop: disable CodeReuse/ActiveRecord
def label_ids
label_ids = []
@@ -52,17 +58,26 @@ class LabelsFinder < UnionFinder
label_ids
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def sort(items)
- items.reorder(title: :asc)
+ if params[:sort]
+ items.order_by(params[:sort])
+ else
+ items.reorder(title: :asc)
+ end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def with_title(items)
return items if title.nil?
return items.none if title.blank?
items.where(title: title)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_search(labels)
return labels unless search?
@@ -70,6 +85,18 @@ class LabelsFinder < UnionFinder
labels.search(params[:search])
end
+ def by_subscription(labels)
+ labels.optionally_subscribed_by(subscriber_id)
+ end
+
+ def subscriber_id
+ current_user&.id if subscribed?
+ end
+
+ def subscribed?
+ params[:subscribed] == 'true'
+ end
+
# Gets redacted array of group ids
# which can include the ancestors and descendants of the requested group.
def group_ids_for(group)
@@ -102,7 +129,7 @@ class LabelsFinder < UnionFinder
end
def project?
- params[:project_id].present?
+ params[:project].present? || params[:project_id].present?
end
def projects?
@@ -125,7 +152,7 @@ class LabelsFinder < UnionFinder
return @project if defined?(@project)
if project?
- @project = Project.find(params[:project_id])
+ @project = params[:project] || Project.find(params[:project_id])
@project = nil unless authorized_to_read_labels?(@project)
else
@project = nil
@@ -134,13 +161,14 @@ class LabelsFinder < UnionFinder
@project
end
+ # rubocop: disable CodeReuse/ActiveRecord
def projects
return @projects if defined?(@projects)
@projects = if skip_authorization
Project.all
else
- ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute
+ ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute # rubocop: disable CodeReuse/Finder
end
@projects = @projects.in_namespace(params[:group_id]) if group?
@@ -149,6 +177,7 @@ class LabelsFinder < UnionFinder
@projects
end
+ # rubocop: enable CodeReuse/ActiveRecord
def authorized_to_read_labels?(label_parent)
return true if skip_authorization
diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb
index fad33f0eca2..d735a4c1d69 100644
--- a/app/finders/license_template_finder.rb
+++ b/app/finders/license_template_finder.rb
@@ -1,35 +1,51 @@
+# frozen_string_literal: true
+
# LicenseTemplateFinder
#
# Used to find license templates, which may come from a variety of external
# sources
#
-# Arguments:
+# Params can be any of the following:
# popular: boolean. When set to true, only "popular" licenses are shown. When
# false, all licenses except popular ones are shown. When nil (the
# default), *all* licenses will be shown.
+# name: string. If set, return a single license matching that name (or nil)
class LicenseTemplateFinder
- attr_reader :params
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :project, :params
- def initialize(params = {})
+ def initialize(project, params = {})
+ @project = project
@params = params
end
def execute
- Licensee::License.all(featured: popular_only?).map do |license|
- LicenseTemplate.new(
- id: license.key,
- name: license.name,
- nickname: license.nickname,
- category: (license.featured? ? :Popular : :Other),
- content: license.content,
- url: license.url,
- meta: license.meta
- )
+ if params[:name]
+ vendored_licenses.find { |template| template.key == params[:name] }
+ else
+ vendored_licenses
end
end
private
+ def vendored_licenses
+ strong_memoize(:vendored_licenses) do
+ Licensee::License.all(featured: popular_only?).map do |license|
+ LicenseTemplate.new(
+ key: license.key,
+ name: license.name,
+ nickname: license.nickname,
+ category: (license.featured? ? :Popular : :Other),
+ content: license.content,
+ url: license.url,
+ meta: license.meta
+ )
+ end
+ end
+ end
+
def popular_only?
params.fetch(:popular, nil)
end
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index 4c893ae2de6..f90a7868102 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class MembersFinder
attr_reader :project, :current_user, :group
@@ -7,15 +9,16 @@ class MembersFinder
@group = project.group
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(include_descendants: false)
project_members = project.project_members
project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
if group
- group_members = GroupMembersFinder.new(group).execute(include_descendants: include_descendants)
+ group_members = GroupMembersFinder.new(group).execute(include_descendants: include_descendants) # rubocop: disable CodeReuse/Finder
group_members = group_members.non_invite
- union = Gitlab::SQL::Union.new([project_members, group_members], remove_duplicates: false)
+ union = Gitlab::SQL::Union.new([project_members, group_members], remove_duplicates: false) # rubocop: disable Gitlab/Union
sql = distinct_on(union)
@@ -24,6 +27,7 @@ class MembersFinder
project_members
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def can?(*args)
Ability.allowed?(*args)
diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb
index 188ec447a94..5f0589f6c8b 100644
--- a/app/finders/merge_request_target_project_finder.rb
+++ b/app/finders/merge_request_target_project_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class MergeRequestTargetProjectFinder
include FinderMethods
@@ -8,6 +10,7 @@ class MergeRequestTargetProjectFinder
@source_project = source_project
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
if @source_project.fork_network
@source_project.fork_network.projects
@@ -18,4 +21,5 @@ class MergeRequestTargetProjectFinder
Project.where(id: source_project)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 40089c082c1..e190d5d90c9 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Finders::MergeRequest class
#
# Used to filter MergeRequests collections by set of params
@@ -25,13 +27,17 @@
# updated_before: datetime
#
class MergeRequestsFinder < IssuableFinder
+ def self.scalar_params
+ @scalar_params ||= super + [:wip]
+ end
+
def klass
MergeRequest
end
def filter_items(_items)
items = by_source_branch(super)
-
+ items = by_wip(items)
by_target_branch(items)
end
@@ -41,19 +47,38 @@ class MergeRequestsFinder < IssuableFinder
@source_branch ||= params[:source_branch].presence
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_source_branch(items)
return items unless source_branch
items.where(source_branch: source_branch)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def target_branch
@target_branch ||= params[:target_branch].presence
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_target_branch(items)
return items unless target_branch
items.where(target_branch: target_branch)
end
+
+ def by_wip(items)
+ if params[:wip] == 'yes'
+ items.where(wip_match(items.arel_table))
+ elsif params[:wip] == 'no'
+ items.where.not(wip_match(items.arel_table))
+ else
+ items
+ end
+ end
+
+ def wip_match(table)
+ table[:title].matches('WIP:%')
+ .or(table[:title].matches('WIP %'))
+ .or(table[:title].matches('[WIP]%'))
+ end
end
diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb
index f5d2b9f253a..47231ea80c7 100644
--- a/app/finders/milestones_finder.rb
+++ b/app/finders/milestones_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Search for milestones
#
# params - Hash
@@ -18,6 +20,7 @@ class MilestonesFinder
@params = params
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
return Milestone.none if project_ids.empty? && group_ids.empty?
@@ -28,6 +31,7 @@ class MilestonesFinder
order(items)
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
@@ -35,6 +39,7 @@ class MilestonesFinder
items.for_projects_and_groups(project_ids, group_ids)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_title(items)
if params[:title]
items.where(title: params[:title])
@@ -42,13 +47,16 @@ class MilestonesFinder
items
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_state(items)
Milestone.filter_by_state(items, params[:state])
end
+ # rubocop: disable CodeReuse/ActiveRecord
def order(items)
order_statement = Gitlab::Database.nulls_last_order('due_date', 'ASC')
items.reorder(order_statement).order('title ASC')
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 9b7a35fb3b5..c67c2065440 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NotesFinder
FETCH_OVERLAP = 5.seconds
@@ -65,21 +67,23 @@ class NotesFinder
@params[:target_type]
end
+ # rubocop: disable CodeReuse/ActiveRecord
def notes_of_any_type
types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) }
note_relations.map! { |notes| search(notes) }
- UnionFinder.new.find_union(note_relations, Note.includes(:author))
+ UnionFinder.new.find_union(note_relations, Note.includes(:author)) # rubocop: disable CodeReuse/Finder
end
+ # rubocop: enable CodeReuse/ActiveRecord
def noteables_for_type(noteable_type)
case noteable_type
when "issue"
- IssuesFinder.new(@current_user, project_id: @project.id).execute
+ IssuesFinder.new(@current_user, project_id: @project.id).execute # rubocop: disable CodeReuse/Finder
when "merge_request"
- MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
+ MergeRequestsFinder.new(@current_user, project_id: @project.id).execute # rubocop: disable CodeReuse/Finder
when "snippet", "project_snippet"
- SnippetsFinder.new(@current_user, project: @project).execute
+ SnippetsFinder.new(@current_user, project: @project).execute # rubocop: disable CodeReuse/Finder
when "personal_snippet"
PersonalSnippet.all
else
@@ -87,6 +91,7 @@ class NotesFinder
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def notes_for_type(noteable_type)
if noteable_type == "commit"
if Ability.allowed?(@current_user, :download_code, @project)
@@ -99,6 +104,7 @@ class NotesFinder
@project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil))
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def notes_on_target
if target.respond_to?(:related_notes)
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index d975f354a88..5beea92689f 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PersonalAccessTokensFinder
attr_accessor :params
@@ -16,11 +18,13 @@ class PersonalAccessTokensFinder
private
+ # rubocop: disable CodeReuse/ActiveRecord
def by_user(tokens)
return tokens unless @params[:user]
tokens.where(user: @params[:user])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_impersonation(tokens)
case @params[:impersonation]
diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb
index a56a3a1e1a9..20f5b221a89 100644
--- a/app/finders/personal_projects_finder.rb
+++ b/app/finders/personal_projects_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PersonalProjectsFinder < UnionFinder
include Gitlab::Allowable
@@ -15,6 +17,7 @@ class PersonalProjectsFinder < UnionFinder
# min_access_level: integer
#
# Returns an ActiveRecord::Relation.
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(current_user = nil)
return Project.none unless can?(current_user, :read_user_profile, @user)
@@ -22,6 +25,7 @@ class PersonalProjectsFinder < UnionFinder
find_union(segments, Project).includes(:namespace).order_updated_desc
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/finders/pipeline_schedules_finder.rb b/app/finders/pipeline_schedules_finder.rb
index 2ac4289fbbe..3beee608268 100644
--- a/app/finders/pipeline_schedules_finder.rb
+++ b/app/finders/pipeline_schedules_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineSchedulesFinder
attr_reader :project, :pipeline_schedules
@@ -6,6 +8,7 @@ class PipelineSchedulesFinder
@pipeline_schedules = project.pipeline_schedules
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(scope: nil)
scoped_schedules =
case scope
@@ -19,4 +22,5 @@ class PipelineSchedulesFinder
scoped_schedules.order(id: :desc)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index a99a889a7e9..3d0d3219a94 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelinesFinder
attr_reader :project, :pipelines, :params, :current_user
@@ -10,6 +12,7 @@ class PipelinesFinder
@params = params
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
unless Ability.allowed?(current_user, :read_pipeline, project)
return Ci::Pipeline.none
@@ -25,16 +28,21 @@ class PipelinesFinder
items = by_yaml_errors(items)
sort_items(items)
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
+ # rubocop: disable CodeReuse/ActiveRecord
def ids_for_ref(refs)
pipelines.where(ref: refs).group(:ref).select('max(id)')
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def from_ids(ids)
pipelines.unscoped.where(id: ids)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def branches
project.repository.branch_names
@@ -61,12 +69,15 @@ class PipelinesFinder
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_status(items)
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
items.where(status: params[:status])
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_ref(items)
if params[:ref].present?
items.where(ref: params[:ref])
@@ -74,7 +85,9 @@ class PipelinesFinder
items
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_sha(items)
if params[:sha].present?
items.where(sha: params[:sha])
@@ -82,7 +95,9 @@ class PipelinesFinder
items
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_name(items)
if params[:name].present?
items.joins(:user).where(users: { name: params[:name] })
@@ -90,7 +105,9 @@ class PipelinesFinder
items
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_username(items)
if params[:username].present?
items.joins(:user).where(users: { username: params[:username] })
@@ -98,7 +115,9 @@ class PipelinesFinder
items
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_yaml_errors(items)
case Gitlab::Utils.to_boolean(params[:yaml_errors])
when true
@@ -109,7 +128,9 @@ class PipelinesFinder
items
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def sort_items(items)
order_by = if ALLOWED_INDEXED_COLUMNS.include?(params[:order_by])
params[:order_by]
@@ -125,4 +146,5 @@ class PipelinesFinder
items.order(order_by => sort)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index cac6643eff3..c2404412006 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# ProjectsFinder
#
# Used to filter Projects by set of params
@@ -35,7 +37,7 @@ class ProjectsFinder < UnionFinder
user = params.delete(:user)
collection =
if user
- PersonalProjectsFinder.new(user, finder_params).execute(current_user)
+ PersonalProjectsFinder.new(user, finder_params).execute(current_user) # rubocop: disable CodeReuse/Finder
else
init_collection
end
@@ -49,6 +51,7 @@ class ProjectsFinder < UnionFinder
collection = by_search(collection)
collection = by_archived(collection)
collection = by_custom_attributes(collection)
+ collection = by_deleted_status(collection)
sort(collection)
end
@@ -63,6 +66,7 @@ class ProjectsFinder < UnionFinder
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def collection_with_user
if owned_projects?
current_user.owned_projects
@@ -76,8 +80,10 @@ class ProjectsFinder < UnionFinder
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Builds a collection for an anonymous user.
+ # rubocop: disable CodeReuse/ActiveRecord
def collection_without_user
if private_only? || owned_projects? || min_access_level?
Project.none
@@ -85,6 +91,7 @@ class ProjectsFinder < UnionFinder
Project.public_to_user
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def owned_projects?
params[:owned].present?
@@ -98,9 +105,11 @@ class ProjectsFinder < UnionFinder
params[:min_access_level].present?
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_ids(items)
project_ids_relation ? items.where(id: project_ids_relation) : items
end
+ # rubocop: enable CodeReuse/ActiveRecord
def union(items)
find_union(items, Project).with_route
@@ -118,9 +127,11 @@ class ProjectsFinder < UnionFinder
params[:trending].present? ? items.trending : items
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_visibilty_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_tags(items)
params[:tag].present? ? items.tagged_with(params[:tag]) : items
@@ -131,6 +142,10 @@ class ProjectsFinder < UnionFinder
params[:search].present? ? items.search(params[:search]) : items
end
+ def by_deleted_status(items)
+ params[:without_deleted].present? ? items.without_deleted : items
+ end
+
def sort(items)
params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.order_id_desc
end
diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb
index 52340f94523..4fca4ec94f3 100644
--- a/app/finders/runner_jobs_finder.rb
+++ b/app/finders/runner_jobs_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RunnerJobsFinder
attr_reader :runner, :params
@@ -14,9 +16,11 @@ class RunnerJobsFinder
private
+ # rubocop: disable CodeReuse/ActiveRecord
def by_status(items)
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
items.where(status: params[:status])
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 9d3772d7541..3528e4228b2 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Snippets Finder
#
# Used to filter Snippets collections by a set of params
@@ -41,6 +43,7 @@ class SnippetsFinder < UnionFinder
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def authorized_snippets_from_project
if can?(current_user, :read_project_snippet, project)
if project.team.member?(current_user)
@@ -52,7 +55,9 @@ class SnippetsFinder < UnionFinder
Snippet.none
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def authorized_snippets
# This query was intentionally converted to a raw one to get it work in Rails 5.0.
# In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
@@ -60,6 +65,7 @@ class SnippetsFinder < UnionFinder
Snippet.where("#{feature_available_projects} OR #{not_project_related}")
.public_or_visible_to_user(current_user)
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Returns a collection of projects that is either public or visible to the
# logged in user.
@@ -68,6 +74,7 @@ class SnippetsFinder < UnionFinder
# the query, e.g. to apply .with_feature_available_for_user on top of it.
# This is useful for performance as we can stick those additional filters
# at the bottom of e.g. the UNION.
+ # rubocop: disable CodeReuse/ActiveRecord
def projects_for_user
return yield(Project.public_to_user) unless current_user
@@ -82,10 +89,9 @@ class SnippetsFinder < UnionFinder
# We use a UNION here instead of OR clauses since this results in better
# performance.
- union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
-
- Project.from("(#{union.to_sql}) AS #{Project.table_name}")
+ Project.from_union([authorized_projects, visible_projects])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project
@@ -109,6 +115,7 @@ class SnippetsFinder < UnionFinder
Snippet.arel_table
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_visibility(items)
visibility = params[:visibility] || visibility_from_scope
@@ -116,12 +123,15 @@ class SnippetsFinder < UnionFinder
items.where(visibility_level: visibility)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_author(items)
return items unless params[:author]
items.where(author_id: params[:author].id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def visibility_from_scope
case params[:scope].to_s
diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb
index b474f0805dc..2ffd46245e9 100644
--- a/app/finders/tags_finder.rb
+++ b/app/finders/tags_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TagsFinder
def initialize(repository, params)
@repository = repository
diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb
new file mode 100644
index 00000000000..3e483716064
--- /dev/null
+++ b/app/finders/template_finder.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+class TemplateFinder
+ include Gitlab::Utils::StrongMemoize
+
+ VENDORED_TEMPLATES = HashWithIndifferentAccess.new(
+ dockerfiles: ::Gitlab::Template::DockerfileTemplate,
+ gitignores: ::Gitlab::Template::GitignoreTemplate,
+ gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate
+ ).freeze
+
+ class << self
+ def build(type, project, params = {})
+ if type.to_s == 'licenses'
+ LicenseTemplateFinder.new(project, params) # rubocop: disable CodeReuse/Finder
+ else
+ new(type, project, params)
+ end
+ end
+ end
+
+ attr_reader :type, :project, :params
+
+ attr_reader :vendored_templates
+ private :vendored_templates
+
+ def initialize(type, project, params = {})
+ @type = type
+ @project = project
+ @params = params
+
+ @vendored_templates = VENDORED_TEMPLATES.fetch(type)
+ end
+
+ def execute
+ if params[:name]
+ vendored_templates.find(params[:name])
+ else
+ vendored_templates.all
+ end
+ end
+end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 6e9c8ea6fde..74baf79e4f2 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# TodosFinder
#
# Used to filter Todos by set of params
@@ -120,6 +122,7 @@ class TodosFinder
params[:sort] ? items.sort_by_attribute(params[:sort]) : items.order_id_desc
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_action(items)
if action?
items = items.where(action: to_action_id)
@@ -127,7 +130,9 @@ class TodosFinder
items
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_action_id(items)
if action_id?
items = items.where(action: action_id)
@@ -135,7 +140,9 @@ class TodosFinder
items
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_author(items)
if author?
items = items.where(author_id: author.try(:id))
@@ -143,7 +150,9 @@ class TodosFinder
items
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_project(items)
if project?
items = items.where(project: project)
@@ -151,19 +160,19 @@ class TodosFinder
items
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_group(items)
- if group?
- groups = group.self_and_descendants
- project_todos = items.where(project_id: Project.where(group: groups).select(:id))
- group_todos = items.where(group_id: groups.select(:id))
+ return items unless group?
- union = Gitlab::SQL::Union.new([project_todos, group_todos])
- items = Todo.from("(#{union.to_sql}) #{Todo.table_name}")
- end
+ groups = group.self_and_descendants
+ project_todos = items.where(project_id: Project.where(group: groups).select(:id))
+ group_todos = items.where(group_id: groups.select(:id))
- items
+ Todo.from_union([project_todos, group_todos])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_state(items)
case params[:state].to_s
@@ -174,6 +183,7 @@ class TodosFinder
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_type(items)
if type?
items = items.where(target_type: type)
@@ -181,4 +191,5 @@ class TodosFinder
items
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/union_finder.rb b/app/finders/union_finder.rb
index 33cd1a491f3..c3b02f7e52f 100644
--- a/app/finders/union_finder.rb
+++ b/app/finders/union_finder.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
class UnionFinder
def find_union(segments, klass)
- if segments.length > 1
- union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
+ unless klass < FromUnion
+ raise TypeError, "#{klass.inspect} must include the FromUnion module"
+ end
- klass.where("#{klass.table_name}.id IN (#{union.to_sql})")
+ if segments.length > 1
+ klass.from_union(segments)
else
segments.first
end
diff --git a/app/finders/user_finder.rb b/app/finders/user_finder.rb
index 484a93c9873..815388c894e 100644
--- a/app/finders/user_finder.rb
+++ b/app/finders/user_finder.rb
@@ -14,9 +14,11 @@ class UserFinder
end
# Tries to find a User, returning nil if none could be found.
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
User.find_by(id: params[:id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Tries to find a User, raising a `ActiveRecord::RecordNotFound` if it could
# not be found.
diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb
index 876f086a3ef..3f2e813d381 100644
--- a/app/finders/user_recent_events_finder.rb
+++ b/app/finders/user_recent_events_finder.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
# Get user activity feed for projects common for a user and a logged in user
#
# - current_user: The user viewing the events
+# WARNING: does not consider project feature visibility!
# - user: The user for which to load the events
# - params:
# - offset: The page of events to return
@@ -21,17 +24,20 @@ class UserRecentEventsFinder
@params = params
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
return Event.none unless can?(current_user, :read_user_profile, target_user)
recent_events(params[:offset] || 0)
.joins(:project)
.with_associations
- .limit_recent(LIMIT, params[:offset])
+ .limit_recent(params[:limit].presence || LIMIT, params[:offset])
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
+ # rubocop: disable CodeReuse/ActiveRecord
def recent_events(offset)
sql = <<~SQL
(#{projects}) AS projects_for_join
@@ -42,26 +48,15 @@ class UserRecentEventsFinder
# Workaround for https://github.com/rails/rails/issues/24193
Event.from([Arel.sql(sql)])
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def target_events
Event.where(author: target_user)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def projects
- # Compile a list of projects `current_user` interacted with
- # and `target_user` is allowed to see.
-
- authorized = target_user
- .project_interactions
- .joins(:project_authorizations)
- .where(project_authorizations: { user: current_user })
- .select(:id)
-
- visible = target_user
- .project_interactions
- .where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(current_user))
- .select(:id)
-
- Gitlab::SQL::Union.new([authorized, visible]).to_sql
+ target_user.project_interactions.to_sql
end
end
diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
index 65824a51919..f2ad9b4bda5 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# UsersFinder
#
# Used to filter users by set of params
@@ -41,11 +43,13 @@ class UsersFinder
private
+ # rubocop: disable CodeReuse/ActiveRecord
def by_username(users)
return users unless params[:username]
users.where(username: params[:username])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_search(users)
return users unless params[:search].present?
@@ -65,18 +69,22 @@ class UsersFinder
users.active
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_external_identity(users)
return users unless current_user&.admin? && params[:extern_uid] && params[:provider]
users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid]))
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_external(users)
return users = users.where.not(external: true) unless current_user&.admin?
return users unless params[:external]
users.external
end
+ # rubocop: enable CodeReuse/ActiveRecord
def by_2fa(users)
case params[:two_factor]
diff --git a/app/graphql/functions/base_function.rb b/app/graphql/functions/base_function.rb
index 42fb8f99acc..2512ecbd255 100644
--- a/app/graphql/functions/base_function.rb
+++ b/app/graphql/functions/base_function.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Functions
class BaseFunction < GraphQL::Function
end
diff --git a/app/graphql/functions/echo.rb b/app/graphql/functions/echo.rb
index e5bf109b8d7..3104486faac 100644
--- a/app/graphql/functions/echo.rb
+++ b/app/graphql/functions/echo.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Functions
class Echo < BaseFunction
argument :text, GraphQL::STRING_TYPE
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 8755a1a62e7..06d26309b5b 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GitlabSchema < GraphQL::Schema
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
diff --git a/app/graphql/mutations/concerns/mutations/resolves_project.rb b/app/graphql/mutations/concerns/mutations/resolves_project.rb
index 0dd1f264a52..da9814e88b0 100644
--- a/app/graphql/mutations/concerns/mutations/resolves_project.rb
+++ b/app/graphql/mutations/concerns/mutations/resolves_project.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Mutations
module ResolvesProject
extend ActiveSupport::Concern
diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb
index 2149e72e2df..54f01c99d78 100644
--- a/app/graphql/mutations/merge_requests/base.rb
+++ b/app/graphql/mutations/merge_requests/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Mutations
module MergeRequests
class Base < BaseMutation
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 89b7f9dad6f..459933af9d3 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Resolvers
class BaseResolver < GraphQL::Schema::Resolver
end
diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb
index 9ec45378d8e..8fd26d85994 100644
--- a/app/graphql/resolvers/concerns/resolves_pipelines.rb
+++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ResolvesPipelines
extend ActiveSupport::Concern
diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb
index 4eb28aaed6c..8d3da33e8d2 100644
--- a/app/graphql/resolvers/full_path_resolver.rb
+++ b/app/graphql/resolvers/full_path_resolver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Resolvers
module FullPathResolver
extend ActiveSupport::Concern
diff --git a/app/graphql/resolvers/merge_request_pipelines_resolver.rb b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
index 00b51ee1381..b371f1335f8 100644
--- a/app/graphql/resolvers/merge_request_pipelines_resolver.rb
+++ b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Resolvers
class MergeRequestPipelinesResolver < BaseResolver
include ::ResolvesPipelines
diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb
index 9f2d348e95f..b87c95217f7 100644
--- a/app/graphql/resolvers/merge_request_resolver.rb
+++ b/app/graphql/resolvers/merge_request_resolver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Resolvers
class MergeRequestResolver < BaseResolver
argument :iid, GraphQL::ID_TYPE,
@@ -8,6 +10,7 @@ module Resolvers
alias_method :project, :object
+ # rubocop: disable CodeReuse/ActiveRecord
def resolve(iid:)
return unless project.present?
@@ -16,5 +19,6 @@ module Resolvers
results.each { |mr| loader.call(mr.iid.to_s, mr) }
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/graphql/resolvers/project_pipelines_resolver.rb b/app/graphql/resolvers/project_pipelines_resolver.rb
index 7f175a3b26c..86094c46c2a 100644
--- a/app/graphql/resolvers/project_pipelines_resolver.rb
+++ b/app/graphql/resolvers/project_pipelines_resolver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Resolvers
class ProjectPipelinesResolver < BaseResolver
include ResolvesPipelines
diff --git a/app/graphql/resolvers/project_resolver.rb b/app/graphql/resolvers/project_resolver.rb
index ec115bad896..ac7c9b0ce2e 100644
--- a/app/graphql/resolvers/project_resolver.rb
+++ b/app/graphql/resolvers/project_resolver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Resolvers
class ProjectResolver < BaseResolver
prepend FullPathResolver
diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb
index b45a845f74f..cf43fea45e6 100644
--- a/app/graphql/types/base_enum.rb
+++ b/app/graphql/types/base_enum.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
class BaseEnum < GraphQL::Schema::Enum
end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index c5740a334d7..2b2ea64c00b 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
class BaseField < GraphQL::Schema::Field
prepend Gitlab::Graphql::Authorize
diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb
index 309e336e6c8..aebed035d3b 100644
--- a/app/graphql/types/base_input_object.rb
+++ b/app/graphql/types/base_input_object.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
class BaseInputObject < GraphQL::Schema::InputObject
end
diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb
index 69e72dc5808..3451a195c33 100644
--- a/app/graphql/types/base_interface.rb
+++ b/app/graphql/types/base_interface.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
module BaseInterface
include GraphQL::Schema::Interface
diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb
index 754adf4c04d..82b78abd573 100644
--- a/app/graphql/types/base_object.rb
+++ b/app/graphql/types/base_object.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
class BaseObject < GraphQL::Schema::Object
prepend Gitlab::Graphql::Present
diff --git a/app/graphql/types/base_scalar.rb b/app/graphql/types/base_scalar.rb
index c0aa38be239..719bc808f47 100644
--- a/app/graphql/types/base_scalar.rb
+++ b/app/graphql/types/base_scalar.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
class BaseScalar < GraphQL::Schema::Scalar
end
diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb
index 36337fc6ee5..30a5668c0bb 100644
--- a/app/graphql/types/base_union.rb
+++ b/app/graphql/types/base_union.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
class BaseUnion < GraphQL::Schema::Union
end
diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb
index 2c12e5001d8..c19ddf5bb25 100644
--- a/app/graphql/types/ci/pipeline_status_enum.rb
+++ b/app/graphql/types/ci/pipeline_status_enum.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
module Ci
class PipelineStatusEnum < BaseEnum
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index bbb7d9354d0..2bbffad4563 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
module Ci
class PipelineType < BaseObject
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 88cd2adc6dc..fb740b6fb1c 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
class MergeRequestType < BaseObject
expose_permissions Types::PermissionTypes::MergeRequest
diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb
index 934ed572e56..26a71e2bfbb 100644
--- a/app/graphql/types/permission_types/base_permission_type.rb
+++ b/app/graphql/types/permission_types/base_permission_type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
module PermissionTypes
class BasePermissionType < BaseObject
diff --git a/app/graphql/types/permission_types/ci/pipeline.rb b/app/graphql/types/permission_types/ci/pipeline.rb
index 942539c7cf7..73e44a33eba 100644
--- a/app/graphql/types/permission_types/ci/pipeline.rb
+++ b/app/graphql/types/permission_types/ci/pipeline.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
module PermissionTypes
module Ci
diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb
index 5c21f6ee9c6..13995d3ea8f 100644
--- a/app/graphql/types/permission_types/merge_request.rb
+++ b/app/graphql/types/permission_types/merge_request.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
module PermissionTypes
class MergeRequest < BasePermissionType
diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb
index 755699a4415..ab37c282fe5 100644
--- a/app/graphql/types/permission_types/project.rb
+++ b/app/graphql/types/permission_types/project.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
module PermissionTypes
class Project < BasePermissionType
@@ -14,7 +16,7 @@ module Types
:create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages,
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
- :create_pages, :destroy_pages
+ :create_pages, :destroy_pages, :read_pages_content
end
end
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 97707215b4e..7b879608b34 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
class ProjectType < BaseObject
expose_permissions Types::PermissionTypes::Project
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 010ec2d7942..7c41716b82a 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
class QueryType < BaseObject
graphql_name 'Query'
diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb
index 2333d82ad1e..f045a50e672 100644
--- a/app/graphql/types/time_type.rb
+++ b/app/graphql/types/time_type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Types
class TimeType < BaseScalar
graphql_name 'Time'
diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
index 5d27d30eaa3..a4f19480539 100644
--- a/app/helpers/accounts_helper.rb
+++ b/app/helpers/accounts_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AccountsHelper
def incoming_email_token_enabled?
current_user.incoming_email_token && Gitlab::IncomingEmail.supports_issue_creation?
diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb
index 97b6dac67c5..84aa1160f12 100644
--- a/app/helpers/active_sessions_helper.rb
+++ b/app/helpers/active_sessions_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSessionsHelper
# Maps a device type as defined in `ActiveSession` to an svg icon name and
# outputs the icon html.
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index f48db024e3f..ed13c5cfdd6 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AppearancesHelper
def brand_title
current_appearance&.title.presence || 'GitLab Community Edition'
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0190aa90763..4f91e3e4117 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,15 +1,27 @@
+# frozen_string_literal: true
+
require 'digest/md5'
require 'uri'
module ApplicationHelper
# See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views
+ # rubocop: disable CodeReuse/ActiveRecord
def render_if_exists(partial, locals = {})
- render(partial, locals) if lookup_context.exists?(partial, [], true)
+ render(partial, locals) if partial_exists?(partial)
+ end
+
+ def partial_exists?(partial)
+ lookup_context.exists?(partial, [], true)
end
+ def template_exists?(template)
+ lookup_context.exists?(template, [], false)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
# Check if a particular controller is the current one
#
- # args - One or more controller names to check
+ # args - One or more controller names to check (using path notation when inside namespaces)
#
# Examples
#
@@ -17,6 +29,11 @@ module ApplicationHelper
# current_controller?(:tree) # => true
# current_controller?(:commits) # => false
# current_controller?(:commits, :tree) # => true
+ #
+ # # On Admin::ApplicationController
+ # current_controller?(:application) # => true
+ # current_controller?('admin/application') # => true
+ # current_controller?('gitlab/application') # => false
def current_controller?(*args)
args.any? do |v|
v.to_s.downcase == controller.controller_name || v.to_s.downcase == controller.controller_path
@@ -49,6 +66,7 @@ module ApplicationHelper
# Define whenever show last push event
# with suggestion to create MR
+ # rubocop: disable CodeReuse/ActiveRecord
def show_last_push_widget?(event)
# Skip if event is not about added or modified non-master branch
return false unless event && event.last_push_to_non_root? && !event.rm_ref?
@@ -66,6 +84,7 @@ module ApplicationHelper
true
end
+ # rubocop: enable CodeReuse/ActiveRecord
def hexdigest(string)
Digest::SHA1.hexdigest string
@@ -106,11 +125,11 @@ module ApplicationHelper
#
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false)
- css_classes = short_format ? 'js-short-timeago' : 'js-timeago'
- css_classes << " #{html_class}" unless html_class.blank?
+ css_classes = [short_format ? 'js-short-timeago' : 'js-timeago']
+ css_classes << html_class unless html_class.blank?
element = content_tag :time, l(time, format: "%b %d, %Y"),
- class: css_classes,
+ class: css_classes.join(' '),
title: l(time.to_time.in_time_zone, format: :timeago_tooltip),
datetime: time.to_time.getutc.iso8601,
data: {
@@ -273,7 +292,8 @@ module ApplicationHelper
mergeRequests: merge_requests_project_autocomplete_sources_path(object),
labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
milestones: milestones_project_autocomplete_sources_path(object),
- commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
+ commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ snippets: snippets_project_autocomplete_sources_path(object)
}
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 684c84c3006..15cbfeea609 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ApplicationSettingsHelper
extend self
@@ -73,12 +75,12 @@ module ApplicationSettingsHelper
def oauth_providers_checkboxes
button_based_providers.map do |source|
disabled = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources.include?(source.to_s)
- css_class = 'btn'
- css_class << ' active' unless disabled
+ css_class = ['btn']
+ css_class << 'active' unless disabled
checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]'
name = Gitlab::Auth::OAuth::Provider.label_for(source)
- label_tag(checkbox_name, class: css_class) do
+ label_tag(checkbox_name, class: css_class.join(' ')) do
check_box_tag(checkbox_name, source, !disabled,
autocomplete: 'off',
id: name.tr(' ', '_')) + name
@@ -106,10 +108,6 @@ module ApplicationSettingsHelper
options_for_select(options, selected)
end
- def sidekiq_queue_options_for_select
- options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
- end
-
def circuitbreaker_failure_count_help_text
health_link = link_to(s_('AdminHealthPageLink|health page'), admin_health_check_path)
api_link = link_to(s_('CircuitBreakerApiLink|circuitbreaker api'), help_page_path("api/repository_storage_health"))
@@ -220,6 +218,7 @@ module ApplicationSettingsHelper
:recaptcha_enabled,
:recaptcha_private_key,
:recaptcha_site_key,
+ :receive_max_input_size,
:repository_checks_enabled,
:repository_storages,
:require_two_factor_authentication,
@@ -231,9 +230,6 @@ module ApplicationSettingsHelper
:session_expire_delay,
:shared_runners_enabled,
:shared_runners_text,
- :sidekiq_throttling_enabled,
- :sidekiq_throttling_factor,
- :sidekiq_throttling_queues,
:sign_in_text,
:signup_enabled,
:terminal_max_session_time,
@@ -258,7 +254,12 @@ module ApplicationSettingsHelper
:user_default_internal_regex,
:user_oauth_applications,
:version_check_enabled,
- :web_ide_clientside_preview_enabled
+ :web_ide_clientside_preview_enabled,
+ :diff_max_patch_bytes
]
end
+
+ def expanded_by_default?
+ Rails.env.test?
+ end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 18f0979fc86..c158cf20dd6 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
LDAP_PROVIDER = /\Aldap/
@@ -64,9 +66,11 @@ module AuthHelper
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def auth_active?(provider)
current_user.identities.exists?(provider: provider.to_s)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def unlink_allowed?(provider)
%w(saml cas3).exclude?(provider.to_s)
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index 7b076728685..516c8a353ea 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AutoDevopsHelper
def show_auto_devops_callout?(project)
Feature.get(:auto_devops_banner_disabled).off? &&
@@ -24,6 +26,7 @@ module AutoDevopsHelper
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def cluster_ingress_ip(project)
project
.cluster_ingresses
@@ -32,6 +35,7 @@ module AutoDevopsHelper
.pluck(:external_ip)
.first
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 494f785e305..7fc4c1a023f 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module AvatarsHelper
- def project_icon(project_id, options = {})
- source_icon(Project, project_id, options)
+ def project_icon(project, options = {})
+ source_icon(project, options)
end
- def group_icon(group_id, options = {})
- source_icon(Group, group_id, options)
+ def group_icon(group, options = {})
+ source_icon(group, options)
end
# Takes both user and email and returns the avatar_icon by
@@ -108,16 +110,11 @@ module AvatarsHelper
private
- def source_icon(klass, source_id, options = {})
- source =
- if source_id.respond_to?(:avatar_url)
- source_id
- else
- klass.find_by_full_path(source_id)
- end
+ def source_icon(source, options = {})
+ avatar_url = source.try(:avatar_url)
- if source.avatar_url
- image_tag source.avatar_url, options
+ if avatar_url
+ image_tag avatar_url, options
else
source_identicon(source, options)
end
@@ -125,9 +122,9 @@ module AvatarsHelper
def source_identicon(source, options = {})
bg_key = (source.id % 7) + 1
- options[:class] ||= ''
- options[:class] << ' identicon'
- options[:class] << " bg#{bg_key}"
+
+ options[:class] =
+ [*options[:class], "identicon bg#{bg_key}"].join(' ')
content_tag(:div, class: options[:class].strip) do
source.name[0, 1].upcase
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
index 86b19368cfd..b97a95629f7 100644
--- a/app/helpers/award_emoji_helper.rb
+++ b/app/helpers/award_emoji_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AwardEmojiHelper
def toggle_award_url(awardable)
return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note)
diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb
index 089d9e3e387..82c74e2416d 100644
--- a/app/helpers/blame_helper.rb
+++ b/app/helpers/blame_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BlameHelper
def age_map_duration(blame_groups, project)
now = Time.zone.now
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 00ebafd177b..883e5ddff57 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BlobHelper
def highlight(blob_name, blob_content, repository: nil, plain: false)
plain ||= blob_content.length > Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE
@@ -157,40 +159,44 @@ module BlobHelper
end
end
- def licenses_for_select
- return @licenses_for_select if defined?(@licenses_for_select)
+ def ref_project
+ @ref_project ||= @target_project || @project
+ end
- grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category)
- categories = grouped_licenses.keys
+ def template_dropdown_names(items)
+ grouped = items.group_by(&:category)
+ categories = grouped.keys
- @licenses_for_select = categories.each_with_object({}) do |category, hash|
- hash[category] = grouped_licenses[category].map do |license|
- { name: license.name, id: license.id }
+ categories.each_with_object({}) do |category, hash|
+ hash[category] = grouped[category].map do |item|
+ { name: item.name, id: item.key }
end
end
end
+ private :template_dropdown_names
- def ref_project
- @ref_project ||= @target_project || @project
+ def licenses_for_select(project = @project)
+ @licenses_for_select ||= template_dropdown_names(TemplateFinder.build(:licenses, project).execute)
end
- def gitignore_names
- @gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names
+ def gitignore_names(project = @project)
+ @gitignore_names ||= template_dropdown_names(TemplateFinder.build(:gitignores, project).execute)
end
- def gitlab_ci_ymls
- @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context])
+ def gitlab_ci_ymls(project = @project)
+ @gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls, project).execute)
end
- def dockerfile_names
- @dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names
+ def dockerfile_names(project = @project)
+ @dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles, project).execute)
end
- def blob_editor_paths
+ def blob_editor_paths(project = @project)
{
'relative-url-root' => Rails.application.config.relative_url_root,
'assets-prefix' => Gitlab::Application.config.assets.prefix,
- 'blob-language' => @blob && @blob.language.try(:ace_mode)
+ 'blob-language' => @blob && @blob.language.try(:ace_mode),
+ 'project-id' => project.id
}
end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index af878bcf9a0..be1e7016a1e 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BoardsHelper
def board
@board ||= @board || @boards.first
@@ -57,8 +59,8 @@ module BoardsHelper
{
toggle: "dropdown",
- list_labels_path: labels_filter_path(true, include_ancestor_groups: true),
- labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups),
+ list_labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_ancestor_groups: true),
+ labels: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: include_descendant_groups),
labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path,
project_path: @project&.path,
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 07b1fc3d7cf..eadf48205fc 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BranchesHelper
def project_branches
options_for_select(@project.repository.branch_names, @project.default_branch)
diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb
index e88fe6bcd7e..b067376cea0 100644
--- a/app/helpers/breadcrumbs_helper.rb
+++ b/app/helpers/breadcrumbs_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BreadcrumbsHelper
def add_to_breadcrumbs(text, link)
@breadcrumbs_extra_links ||= []
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 0a15c29cfb5..289cb44f1e8 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BroadcastMessagesHelper
def broadcast_message(message)
return unless message.present?
@@ -8,18 +10,17 @@ module BroadcastMessagesHelper
end
def broadcast_message_style(broadcast_message)
- style = ''
+ style = []
if broadcast_message.color.present?
style << "background-color: #{broadcast_message.color}"
- style << '; ' if broadcast_message.font.present?
end
if broadcast_message.font.present?
style << "color: #{broadcast_message.font}"
end
- style
+ style.join('; ')
end
def broadcast_message_status(broadcast_message)
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index 4ec63fdaffc..3c8caec3fe5 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BuildsHelper
def build_summary(build, skip: false)
if build.has_trace?
@@ -12,10 +14,10 @@ module BuildsHelper
end
def sidebar_build_class(build, current_build)
- build_class = ''
- build_class += ' active' if build.id === current_build.id
- build_class += ' retried' if build.retried?
- build_class
+ build_class = []
+ build_class << 'active' if build.id === current_build.id
+ build_class << 'retried' if build.retried?
+ build_class.join(' ')
end
def javascript_build_options
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 7adc882bc47..7f071d55a6b 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ButtonHelper
# Output a "Copy to Clipboard" button
#
@@ -61,13 +63,13 @@ module ButtonHelper
dropdown_description = http_dropdown_description(protocol)
append_url = project.http_url_to_repo if append_link
- dropdown_item_with_description(protocol, dropdown_description, href: append_url)
+ dropdown_item_with_description(protocol, dropdown_description, href: append_url, data: { clone_type: 'http' })
end
def http_dropdown_description(protocol)
if current_user.try(:require_password_creation_for_git?)
_("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
- else
+ elsif current_user.try(:require_personal_access_token_creation_for_git_auth?)
_("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
end
end
@@ -80,16 +82,17 @@ module ButtonHelper
append_url = project.ssh_url_to_repo if append_link
- dropdown_item_with_description('SSH', dropdown_description, href: append_url)
+ dropdown_item_with_description('SSH', dropdown_description, href: append_url, data: { clone_type: 'ssh' })
end
- def dropdown_item_with_description(title, description, href: nil)
+ def dropdown_item_with_description(title, description, href: nil, data: nil)
button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
content_tag (href ? :a : :span),
(href ? button_content : title),
class: "#{title.downcase}-selector",
- href: (href if href)
+ href: (href if href),
+ data: (data if data)
end
end
diff --git a/app/helpers/calendar_helper.rb b/app/helpers/calendar_helper.rb
index c54b91b0ce5..ad4116fc3da 100644
--- a/app/helpers/calendar_helper.rb
+++ b/app/helpers/calendar_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module CalendarHelper
def calendar_url_options
{ format: :ics,
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 330959e536d..6f9e2ef78cd 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
##
# DEPRECATED
#
@@ -18,6 +20,8 @@ module CiStatusHelper
'passed with warnings'
when 'manual'
'waiting for manual action'
+ when 'scheduled'
+ 'waiting for delayed job'
else
status
end
@@ -37,6 +41,8 @@ module CiStatusHelper
s_('CiStatusText|passed')
when 'manual'
s_('CiStatusText|blocked')
+ when 'scheduled'
+ s_('CiStatusText|scheduled')
else
# All states are already being translated inside the detailed statuses:
# :running => Gitlab::Ci::Status::Running
@@ -81,6 +87,8 @@ module CiStatusHelper
'status_skipped'
when 'manual'
'status_manual'
+ when 'scheduled'
+ 'status_scheduled'
else
'status_canceled'
end
@@ -121,11 +129,6 @@ module CiStatusHelper
render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement)
end
- def no_runners_for_project?(project)
- project.runners.blank? &&
- Ci::Runner.instance_type.blank?
- end
-
def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16)
klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 8fd0b6f14c6..19eb763e1de 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ClustersHelper
def has_multiple_clusters?(project)
false
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 7a942c44ac4..d52cfd6e37a 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module CommitsHelper
# Returns a link to the commit author. If the author has a matching user and
# is a member of the current @project it will link to the team member page.
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index 2df5b5d1695..9ece8b0bc5b 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module CompareHelper
def create_mr_button?(from = params[:from], to = params[:to], project = @project)
from.present? &&
diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb
index 8893209b314..d0ef86851ad 100644
--- a/app/helpers/components_helper.rb
+++ b/app/helpers/components_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ComponentsHelper
def gitlab_workhorse_version
if request.headers['Gitlab-Workhorse'].present?
diff --git a/app/helpers/conversational_development_index_helper.rb b/app/helpers/conversational_development_index_helper.rb
index 1ff54415811..37e5bb325fb 100644
--- a/app/helpers/conversational_development_index_helper.rb
+++ b/app/helpers/conversational_development_index_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ConversationalDevelopmentIndexHelper
def score_level(score)
if score < 33.33
diff --git a/app/helpers/cookies_helper.rb b/app/helpers/cookies_helper.rb
new file mode 100644
index 00000000000..3a7e9987190
--- /dev/null
+++ b/app/helpers/cookies_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module CookiesHelper
+ def set_secure_cookie(key, value, httponly: false, permanent: false)
+ cookie_jar = permanent ? cookies.permanent : cookies
+
+ cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly }
+ end
+end
diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb
index 5cd98f40f78..e16223a82c9 100644
--- a/app/helpers/count_helper.rb
+++ b/app/helpers/count_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module CountHelper
def approximate_count_with_delimiters(count_data, model)
count = count_data[model]
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index 19aa55a8d49..33c53021c11 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DashboardHelper
def assigned_issues_dashboard_path
issues_dashboard_path(assignee_id: current_user.id)
@@ -19,6 +21,29 @@ module DashboardHelper
links.any? { |link| dashboard_nav_link?(link) }
end
+ def controller_action_to_child_dashboards(controller = controller_name, action = action_name)
+ case "#{controller}##{action}"
+ when 'projects#index', 'root#index', 'projects#starred', 'projects#trending'
+ %w(projects stars)
+ when 'dashboard#activity'
+ %w(starred_project_activity project_activity)
+ when 'groups#index'
+ %w(groups)
+ when 'todos#index'
+ %w(todos)
+ when 'dashboard#issues'
+ %w(issues)
+ when 'dashboard#merge_requests'
+ %w(merge_requests)
+ else
+ []
+ end
+ end
+
+ def user_default_dashboard?(user = current_user)
+ controller_action_to_child_dashboards.any? {|dashboard| dashboard == user.dashboard }
+ end
+
private
def get_dashboard_nav_links
diff --git a/app/helpers/defer_script_tag_helper.rb b/app/helpers/defer_script_tag_helper.rb
index e1567556e5e..d91c6d52683 100644
--- a/app/helpers/defer_script_tag_helper.rb
+++ b/app/helpers/defer_script_tag_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DeferScriptTagHelper
# Override the default ActionView `javascript_include_tag` helper to support page specific deferred loading
def javascript_include_tag(*sources)
diff --git a/app/helpers/deploy_tokens_helper.rb b/app/helpers/deploy_tokens_helper.rb
index bd921322476..80a5bb44c69 100644
--- a/app/helpers/deploy_tokens_helper.rb
+++ b/app/helpers/deploy_tokens_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DeployTokensHelper
def expand_deploy_tokens_section?(deploy_token)
deploy_token.persisted? ||
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 1bb82fd8150..b6844d36052 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DiffHelper
def mark_inline_diffs(old_line, new_line)
old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs
@@ -39,7 +41,8 @@ module DiffHelper
line_num_class = %w[diff-line-num unfold js-unfold]
line_num_class << 'js-unfold-bottom' if bottom
- html = ''
+ html = []
+
if old_pos
html << content_tag(:td, '...', class: [*line_num_class, 'old_line'], data: { linenumber: old_pos })
html << content_tag(:td, text, class: [*content_line_class, 'left-side']) if view == :parallel
@@ -50,7 +53,7 @@ module DiffHelper
html << content_tag(:td, text, class: [*content_line_class, ('right-side' if view == :parallel)])
end
- html.html_safe
+ html.join.html_safe
end
def diff_line_content(line)
@@ -210,14 +213,14 @@ module DiffHelper
params[:w] == '1'
end
+ # rubocop: disable CodeReuse/ActiveRecord
def params_with_whitespace
hide_whitespace? ? request.query_parameters.except(:w) : request.query_parameters.merge(w: 1)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def toggle_whitespace_link(url, options)
- options[:class] ||= ''
- options[:class] << ' btn btn-default'
-
+ options[:class] = [*options[:class], 'btn btn-default'].join(' ')
link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class]
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 5a2360b4661..4b6c5b215e8 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DropdownsHelper
def dropdown_tag(toggle_text, options: {}, &block)
content_tag :div, class: "dropdown #{options[:wrapper_class] if options.key?(:wrapper_class)}" do
@@ -10,7 +12,7 @@ module DropdownsHelper
dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.key?(:dropdown_class)}") do
- output = ""
+ output = []
if options.key?(:title)
output << dropdown_title(options[:title])
@@ -31,8 +33,7 @@ module DropdownsHelper
end
output << dropdown_loading
-
- output.html_safe
+ output.join.html_safe
end
dropdown_output.html_safe
@@ -50,7 +51,7 @@ module DropdownsHelper
def dropdown_title(title, options: {})
content_tag :div, class: "dropdown-title" do
- title_output = ""
+ title_output = []
if options.fetch(:back, false)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
@@ -66,7 +67,7 @@ module DropdownsHelper
end
end
- title_output.html_safe
+ title_output.join.html_safe
end
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index c86a26ac30f..2d2e89a2a50 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module EmailsHelper
include AppearancesHelper
@@ -49,8 +51,8 @@ module EmailsHelper
def reset_token_expire_message
link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email))
- msg = "This link is valid for #{password_reset_token_valid_time}. "
- msg << "After it expires, you can #{link_tag}."
+ "This link is valid for #{password_reset_token_valid_time}. " \
+ "After it expires, you can #{link_tag}."
end
def header_logo
diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb
index 482f68f412b..51b7fd7f352 100644
--- a/app/helpers/emoji_helper.rb
+++ b/app/helpers/emoji_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module EmojiHelper
def emoji_icon(*args)
raw Gitlab::Emoji.gl_emoji_tag(*args)
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 1e78a189c08..2b7320817ed 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
module EnvironmentHelper
+ # rubocop: disable CodeReuse/ActiveRecord
def environment_for_build(project, build)
return unless build.environment
project.environments.find_by(name: build.expanded_environment_name)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def environment_link_for_build(project, build)
environment = environment_for_build(project, build)
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index c005ecbb56b..7b22bc8f98f 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module EnvironmentsHelper
def environments_list_data
{
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index cb6f709c604..c94946a04e7 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module EventsHelper
ICON_NAMES_BY_EVENT_TYPE = {
'pushed to' => 'commit',
@@ -19,7 +21,7 @@ module EventsHelper
name = self_added ? 'You' : author.name
link_to name, user_path(author.username), title: name
else
- event.author_name
+ escape_once(event.author_name)
end
end
@@ -110,10 +112,12 @@ module EventsHelper
event.note_target)
elsif event.note?
if event.note_target
- event_note_target_path(event)
+ event_note_target_url(event)
end
elsif event.push?
push_event_feed_url(event)
+ elsif event.created_project?
+ project_url(event.project)
end
end
@@ -145,14 +149,14 @@ module EventsHelper
end
end
- def event_note_target_path(event)
+ def event_note_target_url(event)
if event.commit_note?
- project_commit_path(event.project, event.note_target, anchor: dom_id(event.target))
+ project_commit_url(event.project, event.note_target, anchor: dom_id(event.target))
elsif event.project_snippet_note?
- project_snippet_path(event.project, event.note_target, anchor: dom_id(event.target))
+ project_snippet_url(event.project, event.note_target, anchor: dom_id(event.target))
else
- polymorphic_path([event.project.namespace.becomes(Namespace),
- event.project, event.note_target],
+ polymorphic_url([event.project.namespace.becomes(Namespace),
+ event.project, event.note_target],
anchor: dom_id(event.target))
end
end
@@ -166,7 +170,7 @@ module EventsHelper
event.note_target_reference
end
- link_to(text, event_note_target_path(event), title: event.target_title, class: 'has-tooltip')
+ link_to(text, event_note_target_url(event), title: event.target_title, class: 'has-tooltip')
else
content_tag(:strong, '(deleted)')
end
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index f062a91a166..62be591ec47 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ExploreHelper
def filter_projects_path(options = {})
exist_opts = {
diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb
index 8cf890b74a8..e36d63b2946 100644
--- a/app/helpers/external_wiki_helper.rb
+++ b/app/helpers/external_wiki_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ExternalWikiHelper
def get_project_wiki_path(project)
external_wiki_service = project.external_wiki
diff --git a/app/helpers/favicon_helper.rb b/app/helpers/favicon_helper.rb
index 3a5342a8d9d..4a809731d97 100644
--- a/app/helpers/favicon_helper.rb
+++ b/app/helpers/favicon_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module FaviconHelper
def favicon_extension_whitelist
FaviconUploader::EXTENSION_WHITELIST
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 905e2002592..5705ee54cee 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module FormHelper
def form_errors(model, type: 'form')
return unless model.errors.any?
diff --git a/app/helpers/git_helper.rb b/app/helpers/git_helper.rb
index 8ab394384f3..5edc6dcf454 100644
--- a/app/helpers/git_helper.rb
+++ b/app/helpers/git_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module GitHelper
def strip_gpg_signature(text)
text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "")
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 61e12b0f31e..04cf43be452 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Shorter routing method for some project items
module GitlabRoutingHelper
extend ActiveSupport::Concern
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index 1022070ab6f..49b15cde009 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
module GraphHelper
def refs(repo, commit)
- refs = commit.ref_names(repo).join(' ')
+ refs = [commit.ref_names(repo).join(' ')]
# append note count
notes_count = @graph.notes[commit.id]
refs << "[#{pluralize(notes_count, 'note')}]" if notes_count > 0
- refs
+ refs.join
end
def parents_zip_spaces(parents, parent_spaces)
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 5b51d2f2425..f573fd399a5 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module GroupsHelper
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]
@@ -43,22 +45,22 @@ module GroupsHelper
def group_title(group, name = nil, url = nil)
@has_group_title = true
- full_title = ''
+ full_title = []
group.ancestors.reverse.each_with_index do |parent, index|
if index > 0
add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before)
else
- full_title += breadcrumb_list_item group_title_link(parent, hidable: false)
+ full_title << breadcrumb_list_item(group_title_link(parent, hidable: false))
end
end
- full_title += render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups")
+ full_title << render("layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups"))
- full_title += breadcrumb_list_item group_title_link(group)
- full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path breadcrumb-item-text js-breadcrumb-item-text') if name
+ full_title << breadcrumb_list_item(group_title_link(group))
+ full_title << ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path breadcrumb-item-text js-breadcrumb-item-text') if name
- full_title.html_safe
+ full_title.join.html_safe
end
def projects_lfs_status(group)
@@ -138,15 +140,8 @@ module GroupsHelper
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
- output =
- if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
- group_icon(group, class: "avatar-tile", width: 15, height: 15)
- else
- ""
- end
-
- output << simple_sanitize(group.name)
- output.html_safe
+ icon = group_icon(group, class: "avatar-tile", width: 15, height: 15) if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
+ [icon, simple_sanitize(group.name)].join.html_safe
end
end
diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb
index 0a356ba55d2..c4b39939192 100644
--- a/app/helpers/hooks_helper.rb
+++ b/app/helpers/hooks_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module HooksHelper
def link_to_test_hook(hook, trigger)
path = case hook
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index a8a10c98d69..037004327b9 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'json'
module IconsHelper
@@ -47,9 +49,10 @@ module IconsHelper
end
end
- css_classes = size ? "s#{size}" : ""
- css_classes << " #{css_class}" unless css_class.blank?
- content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
+ css_classes = []
+ css_classes << "s#{size}" if size
+ css_classes << "#{css_class}" unless css_class.blank?
+ content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes.join(' '))
end
def external_snippet_icon(name)
@@ -70,10 +73,10 @@ module IconsHelper
end
def spinner(text = nil, visible = false)
- css_class = 'loading'
- css_class << ' hide' unless visible
+ css_class = ['loading']
+ css_class << 'hide' unless visible
- content_tag :div, class: css_class do
+ content_tag :div, class: css_class.join(' ') do
icon('spinner spin') + text
end
end
@@ -86,7 +89,7 @@ module IconsHelper
end
end
- def visibility_level_icon(level, fw: true)
+ def visibility_level_icon(level, fw: true, options: {})
name =
case level
when Gitlab::VisibilityLevel::PRIVATE
@@ -97,9 +100,10 @@ module IconsHelper
'globe'
end
- name << " fw" if fw
+ name = [name]
+ name << "fw" if fw
- icon(name)
+ icon(name.join(' '), options)
end
def file_type_icon_class(type, mode, name)
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index c65f1565425..3d0eb3d0d51 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ImportHelper
include ::Gitlab::Utils::StrongMemoize
diff --git a/app/helpers/instance_configuration_helper.rb b/app/helpers/instance_configuration_helper.rb
index cee319f20bc..f695be32743 100644
--- a/app/helpers/instance_configuration_helper.rb
+++ b/app/helpers/instance_configuration_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module InstanceConfigurationHelper
def instance_configuration_cell_html(value, &block)
return '-' unless value.to_s.presence
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index c84ed8091c3..97406fefd43 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module IssuablesHelper
include GitlabRoutingHelper
@@ -105,6 +107,7 @@ module IssuablesHelper
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def user_dropdown_label(user_id, default_label)
return default_label if user_id.nil?
return "Unassigned" if user_id == "0"
@@ -117,7 +120,9 @@ module IssuablesHelper
default_label
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def project_dropdown_label(project_id, default_label)
return default_label if project_id.nil?
return "Any project" if project_id == "0"
@@ -130,7 +135,9 @@ module IssuablesHelper
default_label
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def group_dropdown_label(group_id, default_label)
return default_label if group_id.nil?
return "Any group" if group_id == "0"
@@ -143,6 +150,7 @@ module IssuablesHelper
default_label
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def milestone_dropdown_label(milestone_title, default_label = "Milestone")
title =
@@ -167,33 +175,35 @@ module IssuablesHelper
end
def issuable_meta(issuable, project, text)
- output = ""
+ output = []
output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
+
output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none")
if status = user_status(issuable.author)
- author_output << "&ensp; #{status}".html_safe
+ author_output << "#{status}".html_safe
end
author_output
end
- output << "&ensp;".html_safe
output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!'))
- output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block")
+ output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block prepend-left-8")
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none")
- output.html_safe
+ output.join.html_safe
end
+ # rubocop: disable CodeReuse/ActiveRecord
def issuable_todo(issuable)
if current_user
current_user.todos.find_by(target: issuable, state: :pending)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index { |_, i| i < limit }
@@ -317,11 +327,15 @@ module IssuablesHelper
end
def issuable_button_visibility(issuable, closed)
+ return 'hidden' if issuable_button_hidden?(issuable, closed)
+ end
+
+ def issuable_button_hidden?(issuable, closed)
case issuable
when Issue
- issue_button_visibility(issuable, closed)
+ issue_button_hidden?(issuable, closed)
when MergeRequest
- merge_request_button_visibility(issuable, closed)
+ merge_request_button_hidden?(issuable, closed)
end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 5b27d1d9404..957ab06b0ca 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module IssuesHelper
def issue_css_classes(issue)
- classes = "issue"
- classes << " closed" if issue.closed?
- classes << " today" if issue.today?
- classes
+ classes = ["issue"]
+ classes << "closed" if issue.closed?
+ classes << "today" if issue.today?
+ classes.join(' ')
end
# Returns an OpenStruct object suitable for use by <tt>options_from_collection_for_select</tt>
@@ -62,7 +64,11 @@ module IssuesHelper
end
def issue_button_visibility(issue, closed)
- return 'hidden' if issue.closed? == closed
+ return 'hidden' if issue_button_hidden?(issue, closed)
+ end
+
+ def issue_button_hidden?(issue, closed)
+ issue.closed? == closed || (!closed && issue.discussion_locked)
end
def confidential_icon(issue)
@@ -105,8 +111,8 @@ module IssuesHelper
end
def link_to_discussions_to_resolve(merge_request, single_discussion = nil)
- link_text = merge_request.to_reference
- link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion
+ link_text = [merge_request.to_reference]
+ link_text << "(discussion #{single_discussion.first_note.id})" if single_discussion
path = if single_discussion
Gitlab::UrlBuilder.build(single_discussion.first_note)
@@ -115,7 +121,7 @@ module IssuesHelper
project_merge_request_path(project, merge_request)
end
- link_to link_text, path
+ link_to link_text.join(' '), path
end
def show_new_issue_link?(project)
diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
index cd4075b340d..7cb6da26236 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module JavascriptHelper
def page_specific_javascript_tag(js)
javascript_include_tag asset_path(js)
diff --git a/app/helpers/kerberos_spnego_helper.rb b/app/helpers/kerberos_spnego_helper.rb
index f5b0aa7549a..c0eb8f83f56 100644
--- a/app/helpers/kerberos_spnego_helper.rb
+++ b/app/helpers/kerberos_spnego_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module KerberosSpnegoHelper
def allow_basic_auth?
true # different behavior in GitLab Enterprise Edition
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index c7df25cecef..76ed8efe2c6 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module LabelsHelper
extend self
include ActionView::Helpers::TagHelper
@@ -129,20 +131,26 @@ module LabelsHelper
end
end
- def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false)
- project = @target_project || @project
-
+ def labels_filter_path_with_defaults(only_group_labels: false, include_ancestor_groups: true, include_descendant_groups: false)
options = {}
options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups
options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups
+ options[:only_group_labels] = only_group_labels if only_group_labels && @group
+ options[:format] = :json
+
+ labels_filter_path(options)
+ end
+
+ def labels_filter_path(options = {})
+ project = @target_project || @project
+ format = options.delete(:format) || :html
if project
- project_labels_path(project, :json, options)
+ project_labels_path(project, format, options)
elsif @group
- options[:only_group_labels] = only_group_labels if only_group_labels
- group_labels_path(@group, :json, options)
+ group_labels_path(@group, format, options)
else
- dashboard_labels_path(:json)
+ dashboard_labels_path(format, options)
end
end
diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb
index 603b9438e35..ac987a04895 100644
--- a/app/helpers/lazy_image_tag_helper.rb
+++ b/app/helpers/lazy_image_tag_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module LazyImageTagHelper
def placeholder_image
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
@@ -11,9 +13,11 @@ module LazyImageTagHelper
options[:data] ||= {}
options[:data][:src] = path_to_image(source)
- options[:class] ||= ""
- options[:class] << " lazy"
+ # options[:class] can be either String or Array.
+ klass_opts = Array.wrap(options[:class])
+ klass_opts << "lazy"
+ options[:class] = klass_opts.join(' ')
source = placeholder_image
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index cbb971cf8b7..0d638b850b4 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'nokogiri'
module MarkupHelper
@@ -72,14 +74,21 @@ module MarkupHelper
# the tag contents are truncated without removing the closing tag.
def first_line_in_markdown(object, attribute, max_chars = nil, options = {})
md = markdown_field(object, attribute, options)
+ return nil unless md.present?
- text = truncate_visible(md, max_chars || md.length) if md.present?
+ tags = %w(a gl-emoji b pre code p span)
+ tags << 'img' if options[:allow_images]
- sanitize(
+ text = truncate_visible(md, max_chars || md.length)
+ text = sanitize(
text,
- tags: %w(a img gl-emoji b pre code p span),
+ tags: tags,
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
)
+
+ # since <img> tags are stripped, this can leave empty <a> tags hanging around
+ # (as our markdown wraps images in links)
+ options[:allow_images] ? text : strip_empty_link_tags(text).html_safe
end
def markdown(text, context = {})
@@ -107,23 +116,23 @@ module MarkupHelper
def markup(file_name, text, context = {})
context[:project] ||= @project
- context[:markdown_engine] ||= :redcarpet
+ context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled?
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
prepare_for_rendering(html, context)
end
- def render_wiki_content(wiki_page)
+ def render_wiki_content(wiki_page, context = {})
text = wiki_page.content
return '' unless text.present?
- context = {
+ context.merge!(
pipeline: :wiki,
project: @project,
project_wiki: @project_wiki,
page_slug: wiki_page.slug,
- issuable_state_filter_enabled: true,
- markdown_engine: :redcarpet
- }
+ issuable_state_filter_enabled: true
+ )
+ context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled?
html =
case wiki_page.format
@@ -178,6 +187,10 @@ module MarkupHelper
end
end
+ def commonmark_for_repositories_enabled?
+ Feature.enabled?(:commonmark_for_repositories, default_enabled: true)
+ end
+
private
# Return +text+, truncated to +max_chars+ characters, excluding any HTML
@@ -229,6 +242,16 @@ module MarkupHelper
end
end
+ def strip_empty_link_tags(text)
+ scrubber = Loofah::Scrubber.new do |node|
+ node.remove if node.name == 'a' && node.content.blank?
+ end
+
+ # Use `Loofah` directly instead of `sanitize`
+ # as we still use the `rails-deprecated_sanitizer` gem
+ Loofah.fragment(text).scrub!(scrubber).to_s
+ end
+
def markdown_toolbar_button(options = {})
data = options[:data].merge({ container: 'body' })
content_tag :button,
diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb
index 27ff4051c8d..b211fe5076a 100644
--- a/app/helpers/mattermost_helper.rb
+++ b/app/helpers/mattermost_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MattermostHelper
def mattermost_teams_options(teams)
teams.map do |team|
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index a3129cac2b1..5a21403bc5e 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
module MembersHelper
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
+ text = 'Are you sure you want to'
- text = 'Are you sure you want to '
action =
if member.request?
if member.user == user
@@ -16,13 +18,12 @@ module MembersHelper
"remove #{member.user.name} from"
end
- text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
+ "#{text} #{action} the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
end
def remove_member_title(member)
- text = " from #{member.real_source_type.humanize(capitalize: false)}"
-
- text.prepend(member.request? ? 'Deny access request' : 'Remove user')
+ action = member.request? ? 'Deny access request' : 'Remove user'
+ "#{action} from #{member.real_source_type.humanize(capitalize: false)}"
end
def leave_confirmation_message(member_source)
@@ -32,9 +33,6 @@ module MembersHelper
def filter_group_project_member_path(options = {})
options = params.slice(:search, :sort).merge(options)
-
- path = request.path
- path << "?#{options.to_param}"
- path
+ "#{request.path}?#{options.to_param}"
end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 097be8a0643..23d7aa427bb 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MergeRequestsHelper
def new_mr_path_from_push_event(event)
target_project = event.project.default_merge_request_target
@@ -19,10 +21,10 @@ module MergeRequestsHelper
end
def mr_css_classes(mr)
- classes = "merge-request"
- classes << " closed" if mr.closed?
- classes << " merged" if mr.merged?
- classes
+ classes = ["merge-request"]
+ classes << "closed" if mr.closed?
+ classes << "merged" if mr.merged?
+ classes.join(' ')
end
def ci_build_details_path(merge_request)
@@ -78,7 +80,11 @@ module MergeRequestsHelper
end
def merge_request_button_visibility(merge_request, closed)
- return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
+ return 'hidden' if merge_request_button_hidden?(merge_request, closed)
+ end
+
+ def merge_request_button_hidden?(merge_request, closed)
+ merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end
def merge_request_version_path(project, merge_request, merge_request_diff, start_sha = nil)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 95da8f00aff..94a030d9d57 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MilestonesHelper
include EntityDateHelper
@@ -51,6 +53,7 @@ module MilestonesHelper
# Returns count of milestones for different states
# Uses explicit hash keys as the 'opened' state URL params differs from the db value
# and we need to add the total
+ # rubocop: disable CodeReuse/ActiveRecord
def milestone_counts(milestones)
counts = milestones.reorder(nil).group(:state).count
@@ -60,6 +63,7 @@ module MilestonesHelper
all: counts.values.sum || 0
}
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Show 'active' class if provided GET param matches check
# `or_blank` allows the function to return 'active' when given an empty param
@@ -119,20 +123,18 @@ module MilestonesHelper
title = date_type == :start ? "Start date" : "End date"
if date
- time_ago = time_ago_in_words(date)
- time_ago.slice!("about ")
-
- time_ago << if date.past?
- " ago"
- else
- " remaining"
- end
+ time_ago = time_ago_in_words(date).sub("about ", "")
+ state = if date.past?
+ "ago"
+ else
+ "remaining"
+ end
content = [
title,
"<br />",
date.to_s(:medium),
- "(#{time_ago})"
+ "(#{time_ago} #{state})"
].join(" ")
content.html_safe
diff --git a/app/helpers/milestones_routing_helper.rb b/app/helpers/milestones_routing_helper.rb
index a0b2616f224..a49b561533a 100644
--- a/app/helpers/milestones_routing_helper.rb
+++ b/app/helpers/milestones_routing_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MilestonesRoutingHelper
def milestone_path(milestone, *args)
if milestone.group_milestone?
diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb
index 93ed22513ac..a4025730397 100644
--- a/app/helpers/mirror_helper.rb
+++ b/app/helpers/mirror_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MirrorHelper
def mirrors_form_data_attributes
{ project_mirror_endpoint: project_mirror_path(@project) }
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 30585cb403d..6c65e573307 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
module NamespacesHelper
def namespace_id_from(params)
params.dig(:project, :namespace_id) || params[:namespace_id]
end
+ # rubocop: disable CodeReuse/ActiveRecord
def namespaces_options(selected = :current_user, display_path: false, groups: nil, extra_group: nil, groups_only: false)
groups ||= current_user.manageable_groups
.eager_load(:route)
@@ -40,6 +43,7 @@ module NamespacesHelper
grouped_options_for_select(options, selected_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def namespace_icon(namespace, size = 40)
if namespace.is_a?(Group)
@@ -53,14 +57,16 @@ module NamespacesHelper
# Many importers create a temporary Group, so use the real
# group if one exists by that name to prevent duplicates.
+ # rubocop: disable CodeReuse/ActiveRecord
def dedup_extra_group(extra_group)
unless extra_group.persisted?
- existing_group = Group.find_by(name: extra_group.name)
+ existing_group = Group.find_by(path: extra_group.path)
extra_group = existing_group if existing_group&.persisted?
end
extra_group
end
+ # rubocop: enable CodeReuse/ActiveRecord
def options_for_group(namespaces, display_path:, type:)
group_label = type.pluralize
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index a84a39235d8..761f42f2f0f 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module NavHelper
def header_links
@header_links ||= get_header_links
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 5404ead44f3..e0905584803 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module NotesHelper
def note_target_fields(note)
if note.noteable
@@ -108,7 +110,7 @@ module NotesHelper
end
def noteable_note_url(note)
- Gitlab::UrlBuilder.build(note)
+ Gitlab::UrlBuilder.build(note) if note.id
end
def form_resources
@@ -176,7 +178,7 @@ module NotesHelper
notesPath: notes_url,
totalNotes: issuable.discussions.length,
lastFetchedAt: Time.now.to_i
- }.to_json
+ }
end
def discussion_resolved_intro(discussion)
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index a185f2916d4..5318ab4ddef 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module NotificationsHelper
include IconsHelper
diff --git a/app/helpers/numbers_helper.rb b/app/helpers/numbers_helper.rb
index 45bd3606076..3c0b11c4d32 100644
--- a/app/helpers/numbers_helper.rb
+++ b/app/helpers/numbers_helper.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
module NumbersHelper
+ # rubocop: disable CodeReuse/ActiveRecord
def limited_counter_with_delimiter(resource, **options)
limit = options.fetch(:limit, 1000).to_i
count = resource.limit(limit + 1).count(:all)
@@ -8,4 +11,5 @@ module NumbersHelper
number_with_delimiter(count, options)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 68d892393ef..b33c074d1af 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module PageLayoutHelper
def page_title(*titles)
@page_title ||= []
@@ -65,14 +67,14 @@ module PageLayoutHelper
end
def page_card_meta_tags
- tags = ''
+ tags = []
page_card_attributes.each_with_index do |pair, i|
tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0])
tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1])
end
- tags.html_safe
+ tags.join.html_safe
end
def header_title(title = nil, title_url = nil)
@@ -115,16 +117,16 @@ module PageLayoutHelper
end
def container_class
- css_class = "container-fluid"
+ css_class = ["container-fluid"]
unless fluid_layout
- css_class += " container-limited"
+ css_class << "container-limited"
end
if blank_container
- css_class += " container-blank"
+ css_class << "container-blank"
end
- css_class
+ css_class.join(' ')
end
end
diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb
index 83dd76a01dd..d05153c9d4b 100644
--- a/app/helpers/pagination_helper.rb
+++ b/app/helpers/pagination_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module PaginationHelper
def paginate_collection(collection, remote: nil)
if collection.is_a?(Kaminari::PaginatableWithoutCount)
diff --git a/app/helpers/performance_bar_helper.rb b/app/helpers/performance_bar_helper.rb
index d24efe37f5f..7518cec160c 100644
--- a/app/helpers/performance_bar_helper.rb
+++ b/app/helpers/performance_bar_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module PerformanceBarHelper
# This is a hack since using `alias_method :performance_bar_enabled?, :peek_enabled?`
# in WithPerformanceBar breaks tests (but works in the browser).
diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb
index 4b9f6bd2caf..0e166106b32 100644
--- a/app/helpers/pipeline_schedules_helper.rb
+++ b/app/helpers/pipeline_schedules_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module PipelineSchedulesHelper
def timezone_data
ActiveSupport::TimeZone.all.map do |timezone|
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index fb523cb865b..ff9842d4cd9 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Helper methods for per-User preferences
module PreferencesHelper
def layout_choices
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index e7aa92e6e5c..55674e37a34 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ProfilesHelper
def attribute_provider_label(attribute)
user_synced_attributes_metadata = current_user.user_synced_attributes_metadata
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 18b3badda8d..0016f89db5c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ProjectsHelper
def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
@@ -50,7 +52,7 @@ module ProjectsHelper
return "(deleted)" unless author
- author_html = ""
+ author_html = []
# Build avatar image tag
author_html << link_to_member_avatar(author, opts) if opts[:avatar]
@@ -60,7 +62,7 @@ module ProjectsHelper
author_html << capture(&block) if block
- author_html = author_html.html_safe
+ author_html = author_html.join.html_safe
if opts[:name]
link_to(author_html, user_path(author), class: "author-link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
@@ -80,15 +82,8 @@ module ProjectsHelper
end
project_link = link_to project_path(project) do
- output =
- if project.avatar_url && !Rails.env.test?
- project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15)
- else
- ""
- end
-
- output << content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text")
- output.html_safe
+ icon = project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15) if project.avatar_url && !Rails.env.test?
+ [icon, content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text")].join.html_safe
end
namespace_link = breadcrumb_list_item(namespace_link) unless project.group
@@ -203,6 +198,14 @@ module ProjectsHelper
current_user.require_extra_setup_for_git_auth?
end
+ def show_auto_devops_implicitly_enabled_banner?(project)
+ cookie_key = "hide_auto_devops_implicitly_enabled_banner_#{project.id}"
+
+ project.has_auto_devops_implicitly_enabled? &&
+ cookies[cookie_key.to_sym].blank? &&
+ (project.owner == current_user || project.team.maintainer?(current_user))
+ end
+
def link_to_set_password
if current_user.require_password_creation_for_git?
link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path
@@ -219,6 +222,7 @@ module ProjectsHelper
#
# If no limit is applied we'll just issue a COUNT since the result set could
# be too large to load into memory.
+ # rubocop: disable CodeReuse/ActiveRecord
def any_projects?(projects)
return projects.any? if projects.is_a?(Array)
@@ -228,6 +232,7 @@ module ProjectsHelper
projects.except(:offset).any?
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def show_projects?(projects, params)
!!(params[:personal] || params[:name] || any_projects?(projects))
@@ -252,6 +257,10 @@ module ProjectsHelper
"xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}"
end
+ def legacy_render_context(params)
+ params[:legacy_render] ? { markdown_engine: :redcarpet } : {}
+ end
+
private
def get_project_nav_tabs(project, current_user)
@@ -351,6 +360,10 @@ module ProjectsHelper
end
end
+ def default_clone_label
+ _("Copy %{protocol} clone URL") % { protocol: default_clone_protocol.upcase }
+ end
+
def default_clone_protocol
if allowed_protocols_present?
enabled_protocol
@@ -441,6 +454,7 @@ module ProjectsHelper
buildsAccessLevel: feature.builds_access_level,
wikiAccessLevel: feature.wiki_access_level,
snippetsAccessLevel: feature.snippets_access_level,
+ pagesAccessLevel: feature.pages_access_level,
containerRegistryEnabled: !!project.container_registry_enabled,
lfsEnabled: !!project.lfs_enabled
}
@@ -455,7 +469,10 @@ module ProjectsHelper
registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'),
lfsAvailable: Gitlab.config.lfs.enabled,
- lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs'),
+ pagesAvailable: Gitlab.config.pages.enabled,
+ pagesAccessControlEnabled: Gitlab.config.pages.access_control,
+ pagesHelpPath: help_page_path('user/project/pages/index.md')
}
end
diff --git a/app/helpers/repository_languages_helper.rb b/app/helpers/repository_languages_helper.rb
index 9a842cf5ce0..cf7eee7fff3 100644
--- a/app/helpers/repository_languages_helper.rb
+++ b/app/helpers/repository_languages_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RepositoryLanguagesHelper
def repository_languages_bar(languages)
return if languages.none?
@@ -11,6 +13,7 @@ module RepositoryLanguagesHelper
content_tag :div, nil,
class: "progress-bar has-tooltip",
style: "width: #{lang.share}%; background-color:#{lang.color}",
- title: lang.name
+ data: { html: true },
+ title: "<span class=\"repository-language-bar-tooltip-language\">#{escape_javascript(lang.name)}</span>&nbsp;<span class=\"repository-language-bar-tooltip-share\">#{lang.share.round(1)}%</span>"
end
end
diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb
index 7d4fa83a67a..67c7d244f11 100644
--- a/app/helpers/rss_helper.rb
+++ b/app/helpers/rss_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RssHelper
def rss_url_options
{ format: :atom, feed_token: current_user.try(:feed_token) }
diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb
index 9fb42487a75..cb21f922401 100644
--- a/app/helpers/runners_helper.rb
+++ b/app/helpers/runners_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RunnersHelper
def runner_status_icon(runner)
status = runner.status
diff --git a/app/helpers/safe_params_helper.rb b/app/helpers/safe_params_helper.rb
index b568e8810cc..18bbf3347a8 100644
--- a/app/helpers/safe_params_helper.rb
+++ b/app/helpers/safe_params_helper.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
module SafeParamsHelper
# Rails 5.0 requires to permit `params` if they're used in url helpers.
# Use this helper when generating links with `params.merge(...)`
+ # rubocop: disable CodeReuse/ActiveRecord
def safe_params
if params.respond_to?(:permit!)
params.except(:host, :port, :protocol).permit!
@@ -8,4 +11,5 @@ module SafeParamsHelper
params
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 98074a4c0c5..4f9e1322b56 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SearchHelper
def search_autocomplete_opts(term)
return unless current_user
@@ -99,6 +101,7 @@ module SearchHelper
end
# Autocomplete results for the current user's groups
+ # rubocop: disable CodeReuse/ActiveRecord
def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group|
{
@@ -110,8 +113,10 @@ module SearchHelper
}
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Autocomplete results for the current user's projects
+ # rubocop: disable CodeReuse/ActiveRecord
def projects_autocomplete(term, limit = 5)
current_user.authorized_projects.order_id_desc.search_by_title(term)
.sorted_by_stars.non_archived.limit(limit).map do |p|
@@ -125,6 +130,7 @@ module SearchHelper
}
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def search_result_sanitize(str)
Sanitize.clean(str)
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 6cefcde558a..cf60696ef39 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
module SelectsHelper
def users_select_tag(id, opts = {})
- css_class = "ajax-users-select "
- css_class << "multiselect " if opts[:multiple]
- css_class << "skip_ldap " if opts[:skip_ldap]
+ css_class = ["ajax-users-select"]
+ css_class << "multiselect" if opts[:multiple]
+ css_class << "skip_ldap" if opts[:skip_ldap]
css_class << (opts[:class] || '')
value = opts[:selected] || ''
html = {
- class: css_class,
+ class: css_class.join(' '),
data: users_select_data_attributes(opts)
}
@@ -24,20 +26,21 @@ module SelectsHelper
end
def groups_select_tag(id, opts = {})
- opts[:class] ||= ''
- opts[:class] << ' ajax-groups-select'
+ classes = Array.wrap(opts[:class])
+ classes << 'ajax-groups-select'
+
+ opts[:class] = classes.join(' ')
+
select2_tag(id, opts)
end
def namespace_select_tag(id, opts = {})
- opts[:class] ||= ''
- opts[:class] << ' ajax-namespace-select'
+ opts[:class] = [*opts[:class], 'ajax-namespace-select'].join(' ')
select2_tag(id, opts)
end
def project_select_tag(id, opts = {})
- opts[:class] ||= ''
- opts[:class] << ' ajax-project-select'
+ opts[:class] = [*opts[:class], 'ajax-project-select'].join(' ')
unless opts.delete(:scope) == :all
if @group
@@ -57,7 +60,10 @@ module SelectsHelper
end
def select2_tag(id, opts = {})
- opts[:class] << ' multiselect' if opts[:multiple]
+ klass_opts = [opts[:class]]
+ klass_opts << 'multiselect' if opts[:multiple]
+
+ opts[:class] = klass_opts.join(' ')
value = opts[:selected] || ''
hidden_field_tag(id, value, opts)
diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb
index 3d255df66a0..d53eaef9952 100644
--- a/app/helpers/sentry_helper.rb
+++ b/app/helpers/sentry_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SentryHelper
def sentry_enabled?
Gitlab::Sentry.enabled?
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index f872990122e..d4b50b7ecfb 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ServicesHelper
def service_event_description(event)
case event
@@ -30,7 +32,7 @@ module ServicesHelper
end
def service_save_button(service)
- button_tag(class: 'btn btn-save', type: 'submit', disabled: service.deprecated?) do
+ button_tag(class: 'btn btn-success', type: 'submit', disabled: service.deprecated?) do
icon('spinner spin', class: 'hidden js-btn-spinner') +
content_tag(:span, 'Save changes', class: 'js-btn-label')
end
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
index 50aeb7f4b82..32bf3526571 100644
--- a/app/helpers/sidekiq_helper.rb
+++ b/app/helpers/sidekiq_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SidekiqHelper
SIDEKIQ_PS_REGEXP = %r{\A
(?<pid>\d+)\s+
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index a05640773ad..c7d31f3469d 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SnippetsHelper
def reliable_snippet_path(snippet, opts = nil)
if snippet.project_id?
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 36a311dfa8a..53bd43d4861 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SortingHelper
def sort_options_hash
{
@@ -22,7 +24,8 @@ module SortingHelper
sort_value_recently_updated => sort_title_recently_updated,
sort_value_popularity => sort_title_popularity,
sort_value_priority => sort_title_priority,
- sort_value_upvotes => sort_title_upvotes
+ sort_value_upvotes => sort_title_upvotes,
+ sort_value_contacted_date => sort_title_contacted_date
}
end
@@ -32,7 +35,8 @@ module SortingHelper
sort_value_name => sort_title_name,
sort_value_oldest_activity => sort_title_oldest_activity,
sort_value_oldest_created => sort_title_oldest_created,
- sort_value_recently_created => sort_title_recently_created
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_most_stars => sort_title_most_stars
}
if current_controller?('admin/projects')
@@ -99,6 +103,17 @@ module SortingHelper
}
end
+ def label_sort_options_hash
+ {
+ sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_oldest_updated => sort_title_oldest_updated
+ }
+ end
+
def sortable_item(item, path, sorted_by)
link_to item, path, class: sorted_by == item ? 'is-active' : ''
end
@@ -228,6 +243,14 @@ module SortingHelper
s_('SortOptions|Most popular')
end
+ def sort_title_contacted_date
+ s_('SortOptions|Last Contact')
+ end
+
+ def sort_title_most_stars
+ s_('SortOptions|Most stars')
+ end
+
# Values.
def sort_value_access_level_asc
'access_level_asc'
@@ -348,4 +371,12 @@ module SortingHelper
def sort_value_upvotes
'upvotes_desc'
end
+
+ def sort_value_contacted_date
+ 'contacted_asc'
+ end
+
+ def sort_value_most_stars
+ 'stars_desc'
+ end
end
diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb
index b76c1228220..182e8e6641b 100644
--- a/app/helpers/storage_health_helper.rb
+++ b/app/helpers/storage_health_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module StorageHealthHelper
def failing_storage_health_message(storage_health)
storage_name = content_tag(:strong, h(storage_health.storage_name))
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index e19c67a37ca..be8761db562 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module StorageHelper
def storage_counter(size_in_bytes)
precision = size_in_bytes < 1.megabyte ? 0 : 1
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index ec2cf2b16c0..164c69ca50b 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SubmoduleHelper
extend self
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index 5b4a141dbcf..ac4e8f54260 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SystemNoteHelper
ICON_NAMES_BY_ACTION = {
'commit' => 'commit',
@@ -21,7 +23,8 @@ module SystemNoteHelper
'outdated' => 'pencil-square',
'duplicate' => 'issue-duplicate',
'locked' => 'lock',
- 'unlocked' => 'lock-open'
+ 'unlocked' => 'lock-open',
+ 'due_date' => 'calendar'
}.freeze
def system_note_icon_name(note)
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index ee701076a14..d91f0f78db7 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TabHelper
# Navigation link helper
#
@@ -6,7 +8,7 @@ module TabHelper
# element is the value passed to the block.
#
# options - The options hash used to determine if the element is "active" (default: {})
- # :controller - One or more controller names to check (optional).
+ # :controller - One or more controller names to check, use path notation when namespaced (optional).
# :action - One or more action names to check (optional).
# :path - A shorthand path, such as 'dashboard#index', to check (optional).
# :html_options - Extra options to be passed to the list element (optional).
@@ -40,6 +42,20 @@ module TabHelper
# nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" }
# # => '<li class="home active">Hello</li>'
#
+ # # For namespaced controllers like Admin::AppearancesController#show
+ #
+ # # Controller and namespace matches
+ # nav_link(controller: 'admin/appearances') { "Hello" }
+ # # => '<li class="active">Hello</li>'
+ #
+ # # Controller and namespace matches but action doesn't
+ # nav_link(controller: 'admin/appearances', action: :edit) { "Hello" }
+ # # => '<li>Hello</li>'
+ #
+ # # Shorthand path with namespace
+ # nav_link(path: 'admin/appearances#show') { "Hello"}
+ # # => '<li class="active">Hello</li>'
+ #
# Returns a list item element String
def nav_link(options = {}, &block)
klass = active_nav_link?(options) ? 'active' : ''
@@ -47,9 +63,7 @@ module TabHelper
# Add our custom class into the html_options, which may or may not exist
# and which may or may not already have a :class key
o = options.delete(:html_options) || {}
- o[:class] ||= ''
- o[:class] += ' ' + klass
- o[:class].strip!
+ o[:class] = [*o[:class], klass].join(' ').strip
if block_given?
content_tag(:li, capture(&block), o)
diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb
index d000d6b1c0a..de0b92b6fd7 100644
--- a/app/helpers/tags_helper.rb
+++ b/app/helpers/tags_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TagsHelper
def tag_path(tag)
"/tags/#{tag}"
@@ -14,12 +16,13 @@ module TagsHelper
end
def tag_list(project)
- html = ''
+ html = []
+
project.tag_list.each do |tag|
html << link_to(tag, tag_path(tag))
end
- html.html_safe
+ html.join.html_safe
end
def protected_tag?(project, tag)
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 336385f6798..3e6a301b77d 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TimeHelper
def time_interval_in_words(interval_in_seconds)
interval_in_seconds = interval_in_seconds.to_i
@@ -19,9 +21,17 @@ module TimeHelper
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
- def duration_in_numbers(duration)
- time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S"
+ def duration_in_numbers(duration_in_seconds, allow_overflow = false)
+ if allow_overflow
+ seconds = duration_in_seconds % 1.minute
+ minutes = (duration_in_seconds / 1.minute) % (1.hour / 1.minute)
+ hours = duration_in_seconds / 1.hour
+
+ "%02d:%02d:%02d" % [hours, minutes, seconds]
+ else
+ time_format = duration_in_seconds < 1.hour ? "%M:%S" : "%H:%M:%S"
- Time.at(duration).utc.strftime(time_format)
+ Time.at(duration_in_seconds).utc.strftime(time_format)
+ end
end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 7cd74358168..6bd78336ed3 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TodosHelper
def todos_pending_count
@todos_pending_count ||= current_user.todos_pending_count
@@ -94,9 +96,7 @@ module TodosHelper
end
end
- path = request.path
- path << "?#{options.to_param}"
- path
+ "#{request.path}?#{options.to_param}"
end
def todo_actions_options
@@ -152,10 +152,11 @@ module TodosHelper
''
end
- html = "&middot; ".html_safe
- html << content_tag(:span, class: css_class) do
+ content = content_tag(:span, class: css_class) do
"Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}"
end
+
+ "&middot; #{content}".html_safe
end
private
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index dc42caa70e5..6d2da5699fb 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TreeHelper
FILE_LIMIT = 1_000
@@ -5,10 +7,11 @@ module TreeHelper
# their corresponding partials
#
# tree - A `Tree` object for the current tree
+ # rubocop: disable CodeReuse/ActiveRecord
def render_tree(tree)
# Sort submodules and folders together by name ahead of files
folders, files, submodules = tree.trees, tree.blobs, tree.submodules
- tree = ''
+ tree = []
items = (folders + submodules).sort_by(&:name) + files
if items.size > FILE_LIMIT
@@ -18,8 +21,9 @@ module TreeHelper
end
tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present?
- tree.html_safe
+ tree.join.html_safe
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Return an image icon depending on the file type and mode
#
@@ -122,6 +126,7 @@ module TreeHelper
end
# returns the relative path of the first subdir that doesn't have only one directory descendant
+ # rubocop: disable CodeReuse/ActiveRecord
def flatten_tree(root_path, tree)
return tree.flat_path.sub(%r{\A#{Regexp.escape(root_path)}/}, '') if tree.flat_path.present?
@@ -132,6 +137,7 @@ module TreeHelper
return tree.name
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def selected_branch
@branch_name || tree_edit_branch
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
index ce435ca2241..5cfdc0971f0 100644
--- a/app/helpers/triggers_helper.rb
+++ b/app/helpers/triggers_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TriggersHelper
def builds_trigger_url(project_id, ref: nil)
if ref.nil?
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index da5fe25c07d..bae01d476df 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
+ CLUSTER_SECURITY_WARNING = 'cluster_security_warning'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
@@ -11,6 +14,10 @@ module UserCalloutsHelper
!user_dismissed?(GCP_SIGNUP_OFFER)
end
+ def show_cluster_security_warning?
+ !user_dismissed?(CLUSTER_SECURITY_WARNING)
+ end
+
private
def user_dismissed?(feature_name)
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 2c0c4254a0c..42b533ad772 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module UsersHelper
def user_link(user)
link_to(user.name, user_path(user),
@@ -74,7 +76,7 @@ module UsersHelper
tabs = []
if can?(current_user, :read_user_profile, @user)
- tabs += [:activity, :groups, :contributed, :projects, :snippets]
+ tabs += [:overview, :activity, :groups, :contributed, :projects, :snippets]
end
tabs
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index c20753ece72..ab77b149072 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -1,8 +1,25 @@
+# frozen_string_literal: true
+
module VersionCheckHelper
def version_status_badge
- if Rails.env.production? && Gitlab::CurrentSettings.version_check_enabled
- image_url = VersionCheck.new.url
- image_tag image_url, class: 'js-version-status-badge'
+ return unless Rails.env.production?
+ return unless Gitlab::CurrentSettings.version_check_enabled
+ return if User.single_user&.requires_usage_stats_consent?
+
+ image_url = VersionCheck.new.url
+ image_tag image_url, class: 'js-version-status-badge'
+ end
+
+ def link_to_version
+ if Gitlab.pre_release?
+ commit_link = link_to(Gitlab.revision, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', source_code_project, Gitlab.revision))
+ [Gitlab::VERSION, content_tag(:small, commit_link)].join(' ').html_safe
+ else
+ link_to Gitlab::VERSION, Gitlab::COM_URL + namespace_project_tag_path('gitlab-org', source_code_project, "v#{Gitlab::VERSION}")
end
end
+
+ def source_code_project
+ 'gitlab-ce'
+ end
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index cf2fe5a2019..e690350a0d1 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module VisibilityLevelHelper
def visibility_level_color(level)
case level
@@ -82,7 +84,7 @@ module VisibilityLevelHelper
def disallowed_project_visibility_level_description(level, project)
level_name = Gitlab::VisibilityLevel.level_name(level).downcase
reasons = []
- instructions = ''
+ instructions = []
unless project.visibility_level_allowed_as_fork?(level)
reasons << "the fork source project has lower visibility"
@@ -96,7 +98,7 @@ module VisibilityLevelHelper
end
reasons = reasons.any? ? ' because ' + reasons.to_sentence : ''
- "This project cannot be #{level_name}#{reasons}.#{instructions}".html_safe
+ "This project cannot be #{level_name}#{reasons}.#{instructions.join}".html_safe
end
# Note: these messages closely mirror the form validation strings found in the group
@@ -104,7 +106,7 @@ module VisibilityLevelHelper
def disallowed_group_visibility_level_description(level, group)
level_name = Gitlab::VisibilityLevel.level_name(level).downcase
reasons = []
- instructions = ''
+ instructions = []
unless group.visibility_level_allowed_by_projects?(level)
reasons << "it contains projects with higher visibility"
@@ -122,7 +124,7 @@ module VisibilityLevelHelper
end
reasons = reasons.any? ? ' because ' + reasons.to_sentence : ''
- "This group cannot be #{level_name}#{reasons}.#{instructions}".html_safe
+ "This group cannot be #{level_name}#{reasons}.#{instructions.join}".html_safe
end
def visibility_icon_description(form_model)
@@ -138,7 +140,7 @@ module VisibilityLevelHelper
end
def project_visibility_icon_description(level)
- "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}"
+ "#{project_visibility_level_description(level)}"
end
def visibility_level_label(level)
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index 72f6b397046..345ddcf023a 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module WebpackHelper
def webpack_bundle_tag(bundle)
javascript_include_tag(*webpack_entrypoint_paths(bundle))
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index 41f9eedd4bd..647f34e57ed 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -1,4 +1,8 @@
+# frozen_string_literal: true
+
module WikiHelper
+ include API::Helpers::RelatedResourcesHelpers
+
# Produces a pure text breadcrumb for a given page.
#
# page_slug - The slug of a WikiPage object.
@@ -39,4 +43,8 @@ module WikiHelper
end
end
end
+
+ def wiki_attachment_upload_url
+ expose_url(api_v4_projects_wikis_attachments_path(id: @project.id))
+ end
end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index fd1d78bd9b8..f19445fca1a 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Helpers to send Git blobs, diffs, patches or archives through Workhorse.
# Workhorse will also serve files when using `send_file`.
module WorkhorseHelper
diff --git a/app/mailers/emails/auto_devops.rb b/app/mailers/emails/auto_devops.rb
new file mode 100644
index 00000000000..9705a3052d4
--- /dev/null
+++ b/app/mailers/emails/auto_devops.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Emails
+ module AutoDevops
+ def autodevops_disabled_email(pipeline, recipient)
+ @pipeline = pipeline
+ @project = pipeline.project
+
+ add_project_headers
+
+ mail(to: recipient,
+ subject: auto_devops_disabled_subject(@project.name)) do |format|
+ format.html { render layout: 'mailer' }
+ format.text { render layout: 'mailer' }
+ end
+ end
+
+ private
+
+ def auto_devops_disabled_subject(project_name)
+ subject("Auto DevOps pipeline was disabled for #{project_name}")
+ end
+ end
+end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index c8b1ab5033a..602e5afe26b 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -19,6 +19,7 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end
+ # rubocop: disable CodeReuse/ActiveRecord
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_ids, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
@@ -27,6 +28,7 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end
+ # rubocop: enable CodeReuse/ActiveRecord
def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 70f65d4e58d..be085496731 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -3,13 +3,14 @@
module Emails
module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id, reason = nil)
- setup_merge_request_mail(merge_request_id, recipient_id)
+ setup_merge_request_mail(merge_request_id, recipient_id, present: true)
mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id, reason))
end
def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
- setup_merge_request_mail(merge_request_id, recipient_id)
+ setup_merge_request_mail(merge_request_id, recipient_id, present: true)
+
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
@@ -22,12 +23,14 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
+ # rubocop: disable CodeReuse/ActiveRecord
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
+ # rubocop: enable CodeReuse/ActiveRecord
def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
@@ -73,11 +76,16 @@ module Emails
private
- def setup_merge_request_mail(merge_request_id, recipient_id)
+ def setup_merge_request_mail(merge_request_id, recipient_id, present: false)
@merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project
@target_url = project_merge_request_url(@project, @merge_request)
+ if present
+ recipient = User.find(recipient_id)
+ @mr_presenter = @merge_request.present(current_user: recipient)
+ end
+
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 40d7b9ccd7a..2ea1aea1f51 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -9,6 +9,7 @@ module Emails
mail(to: @user.notification_email, subject: subject("Account was created for you"))
end
+ # rubocop: disable CodeReuse/ActiveRecord
def new_ssh_key_email(key_id)
@key = Key.find_by(id: key_id)
@@ -18,7 +19,9 @@ module Emails
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def new_gpg_key_email(gpg_key_id)
@gpg_key = GpgKey.find_by(id: gpg_key_id)
@@ -28,5 +31,6 @@ module Emails
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("GPG key was added to your account"))
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index f4eeb85270e..f7347ee61b4 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -12,6 +12,7 @@ class Notify < BaseMailer
include Emails::Profile
include Emails::Pipelines
include Emails::Members
+ include Emails::AutoDevops
helper MergeRequestsHelper
helper DiffHelper
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index df470930e9e..2f5b5483e9d 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -9,7 +9,7 @@ class NotifyPreview < ActionMailer::Preview
In this notification email, we expect to see:
- The note contents (that's what you're looking at)
- - A link to view this note on Gitlab
+ - A link to view this note on GitLab
- An explanation for why the user is receiving this notification
MD
@@ -26,7 +26,7 @@ class NotifyPreview < ActionMailer::Preview
- A line saying who started this discussion
- The note contents (that's what you're looking at)
- - A link to view this discussion on Gitlab
+ - A link to view this discussion on GitLab
- An explanation for why the user is receiving this notification
MD
@@ -44,7 +44,7 @@ class NotifyPreview < ActionMailer::Preview
- A line saying who started this discussion and on what file
- The diff
- The note contents (that's what you're looking at)
- - A link to view this discussion on Gitlab
+ - A link to view this discussion on GitLab
- An explanation for why the user is receiving this notification
MD
@@ -125,6 +125,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
end
+ def autodevops_disabled_email
+ Notify.autodevops_disabled_email(pipeline, user.email).message
+ end
+
private
def project
diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb
index 4bcf371cfc0..145169be8a6 100644
--- a/app/mailers/repository_check_mailer.rb
+++ b/app/mailers/repository_check_mailer.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class RepositoryCheckMailer < BaseMailer
+ # rubocop: disable CodeReuse/ActiveRecord
def notify(failed_count)
@message =
if failed_count == 1
@@ -14,4 +15,5 @@ class RepositoryCheckMailer < BaseMailer
subject: "GitLab Admin | #{@message}"
)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index a853106e5bd..1466407d0d1 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -74,7 +74,7 @@ class Ability
end
def policy_for(user, subject = :global)
- cache = RequestStore.active? ? RequestStore : {}
+ cache = Gitlab::SafeRequestStore.active? ? Gitlab::SafeRequestStore : {}
DeclarativePolicy.policy_for(user, subject, cache: cache)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 03bd7fa016e..65a2f760f93 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -26,7 +26,6 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
- serialize :sidekiq_throttling_queues, Array # rubocop:disable Cop/ActiveRecordSerialize
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
@@ -131,15 +130,6 @@ class ApplicationSetting < ActiveRecord::Base
presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
if: :domain_blacklist_enabled?
- validates :sidekiq_throttling_factor,
- numericality: { greater_than: 0, less_than: 1 },
- presence: { message: 'Throttling factor cannot be empty if Sidekiq Throttling is enabled.' },
- if: :sidekiq_throttling_enabled?
-
- validates :sidekiq_throttling_queues,
- presence: { message: 'Queues to throttle cannot be empty if Sidekiq Throttling is enabled.' },
- if: :sidekiq_throttling_enabled?
-
validates :housekeeping_incremental_repack_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -192,6 +182,12 @@ class ApplicationSetting < ActiveRecord::Base
numericality: { less_than_or_equal_to: :gitaly_timeout_default },
if: :gitaly_timeout_default
+ validates :diff_max_patch_bytes,
+ presence: true,
+ numericality: { only_integer: true,
+ greater_than_or_equal_to: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
+ less_than_or_equal_to: Gitlab::Git::Diff::MAX_PATCH_BYTES_UPPER_BOUND }
+
validates :user_default_internal_regex, js_regex: true, allow_nil: true
SUPPORTED_KEY_TYPES.each do |type|
@@ -219,6 +215,7 @@ class ApplicationSetting < ActiveRecord::Base
validate :terms_exist, if: :enforce_terms?
before_validation :ensure_uuid!
+ before_validation :strip_sentry_values
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
@@ -281,7 +278,6 @@ class ApplicationSetting < ActiveRecord::Base
send_user_confirmation_email: false,
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
shared_runners_text: nil,
- sidekiq_throttling_enabled: false,
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0,
@@ -302,7 +298,9 @@ class ApplicationSetting < ActiveRecord::Base
instance_statistics_visibility_private: false,
user_default_external: false,
user_default_internal_regex: nil,
- user_show_add_ssh_key_message: true
+ user_show_add_ssh_key_message: true,
+ usage_stats_set_by_user_id: nil,
+ diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES
}
end
@@ -326,10 +324,6 @@ class ApplicationSetting < ActiveRecord::Base
::Gitlab::Database.cached_column_exists?(:application_settings, :help_page_support_url)
end
- def sidekiq_throttling_column_exists?
- ::Gitlab::Database.cached_column_exists?(:application_settings, :sidekiq_throttling_enabled)
- end
-
def disabled_oauth_sign_in_sources=(sources)
sources = (sources || []).map(&:to_s) & Devise.omniauth_providers.map(&:to_s)
super(sources)
@@ -381,6 +375,11 @@ class ApplicationSetting < ActiveRecord::Base
super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end
+ def strip_sentry_values
+ sentry_dsn.strip! if sentry_dsn.present?
+ clientside_sentry_dsn.strip! if clientside_sentry_dsn.present?
+ end
+
def performance_bar_allowed_group
Group.find_by_id(performance_bar_allowed_group_id)
end
@@ -404,12 +403,6 @@ class ApplicationSetting < ActiveRecord::Base
ensure_health_check_access_token!
end
- def sidekiq_throttling_enabled?
- return false unless sidekiq_throttling_column_exists?
-
- sidekiq_throttling_enabled
- end
-
def usage_ping_can_be_configured?
Settings.gitlab.usage_ping_enabled
end
diff --git a/app/models/badge.rb b/app/models/badge.rb
index 7e3b6b659e4..f016654206b 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Badge < ActiveRecord::Base
+ include FromUnion
+
# This structure sets the placeholders that the urls
# can have. This hash also sets which action to ask when
# the placeholder is found.
diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb
index 1a86f04b1b9..655241c2808 100644
--- a/app/models/blob_viewer/gitlab_ci_yml.rb
+++ b/app/models/blob_viewer/gitlab_ci_yml.rb
@@ -10,16 +10,16 @@ module BlobViewer
self.file_types = %i(gitlab_ci)
self.binary = false
- def validation_message
+ def validation_message(project, sha)
return @validation_message if defined?(@validation_message)
prepare!
- @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data)
+ @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data, { project: project, sha: sha })
end
- def valid?
- validation_message.blank?
+ def valid?(project, sha)
+ validation_message(project, sha).blank?
end
end
end
diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb
index d12dd93ce2e..7cae60a74d6 100644
--- a/app/models/blob_viewer/package_json.rb
+++ b/app/models/blob_viewer/package_json.rb
@@ -33,7 +33,8 @@ module BlobViewer
end
def homepage
- json_data['homepage']
+ url = json_data['homepage']
+ url if Gitlab::UrlSanitizer.valid?(url)
end
def npm_url
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index cd0b31482d2..d87d6a5cb2f 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -4,7 +4,7 @@ module Ci
class ArtifactBlob
include BlobLike
- EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json].freeze
+ EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json .log].freeze
attr_reader :entry
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index faa160ad6ba..cdfe8175a42 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -40,6 +40,7 @@ module Ci
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
+ delegate :trigger_short_token, to: :trigger_request, allow_nil: true
##
# The "environment" field for builds is a String, and is the unexpanded name!
@@ -67,8 +68,12 @@ module Ci
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
+ scope :with_existing_job_artifacts, ->(query) do
+ where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query))
+ end
+
scope :with_archived_trace, ->() do
- where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
+ with_existing_job_artifacts(Ci::JobArtifact.trace)
end
scope :without_archived_trace, ->() do
@@ -76,16 +81,19 @@ module Ci
end
scope :with_test_reports, ->() do
- includes(:job_artifacts_junit) # Prevent N+1 problem when iterating each ci_job_artifact row
- .where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').test_reports)
+ with_existing_job_artifacts(Ci::JobArtifact.test_reports)
+ .eager_load_job_artifacts
end
+ scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
+
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_archived_trace_stored_locally, -> { with_archived_trace.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
- scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
+ scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
+ scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
scope :ref_protected, -> { where(protected: true) }
scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
@@ -139,9 +147,11 @@ module Ci
end
def retry(build, current_user)
+ # rubocop: disable CodeReuse/ServiceClass
Ci::RetryBuildService
.new(build.project, current_user)
.execute(build)
+ # rubocop: enable CodeReuse/ServiceClass
end
end
@@ -150,6 +160,34 @@ module Ci
transition created: :manual
end
+ event :schedule do
+ transition created: :scheduled
+ end
+
+ event :unschedule do
+ transition scheduled: :manual
+ end
+
+ event :enqueue_scheduled do
+ transition scheduled: :pending, if: ->(build) do
+ build.scheduled_at && build.scheduled_at < Time.now
+ end
+ end
+
+ before_transition scheduled: any do |build|
+ build.scheduled_at = nil
+ end
+
+ before_transition created: :scheduled do |build|
+ build.scheduled_at = build.options_scheduled_at
+ end
+
+ after_transition created: :scheduled do |build|
+ build.run_after_commit do
+ Ci::BuildScheduleWorker.perform_at(build.scheduled_at, build.id)
+ end
+ end
+
after_transition any => [:pending] do |build|
build.run_after_commit do
BuildQueueWorker.perform_async(id)
@@ -217,18 +255,29 @@ module Ci
end
def playable?
- action? && (manual? || retryable?)
+ action? && (manual? || scheduled? || retryable?)
+ end
+
+ def schedulable?
+ Feature.enabled?('ci_enable_scheduled_build', default_enabled: true) &&
+ self.when == 'delayed' && options[:start_in].present?
+ end
+
+ def options_scheduled_at
+ ChronicDuration.parse(options[:start_in])&.seconds&.from_now
end
def action?
- self.when == 'manual'
+ %w[manual delayed].include?(self.when)
end
+ # rubocop: disable CodeReuse/ServiceClass
def play(current_user)
Ci::PlayBuildService
.new(project, current_user)
.execute(self)
end
+ # rubocop: enable CodeReuse/ServiceClass
def cancelable?
active? || created?
@@ -385,9 +434,11 @@ module Ci
update(coverage: coverage) if coverage.present?
end
+ # rubocop: disable CodeReuse/ServiceClass
def parse_trace_sections!
ExtractSectionsFromBuildTraceService.new(project, user).execute(self)
end
+ # rubocop: enable CodeReuse/ServiceClass
def trace
Gitlab::Ci::Trace.new(self)
@@ -397,8 +448,8 @@ module Ci
trace.exist?
end
- def has_test_reports?
- job_artifacts.test_reports.any?
+ def has_job_artifacts?
+ job_artifacts.any?
end
def has_old_trace?
@@ -462,28 +513,23 @@ module Ci
end
end
- def erase_artifacts!
- remove_artifacts_file!
- remove_artifacts_metadata!
- save
- end
-
- def erase_test_reports!
- # TODO: Use fast_destroy_all in the context of https://gitlab.com/gitlab-org/gitlab-ce/issues/35240
- job_artifacts_junit&.destroy
+ # and use that for `ExpireBuildInstanceArtifactsWorker`?
+ def erase_erasable_artifacts!
+ job_artifacts.erasable.destroy_all # rubocop: disable DestroyAll
+ erase_old_artifacts!
end
def erase(opts = {})
return false unless erasable?
- erase_artifacts!
- erase_test_reports!
+ job_artifacts.destroy_all # rubocop: disable DestroyAll
+ erase_old_artifacts!
erase_trace!
update_erased!(opts[:erased_by])
end
def erasable?
- complete? && (artifacts? || has_test_reports? || has_trace?)
+ complete? && (artifacts? || has_job_artifacts? || has_trace?)
end
def erased?
@@ -514,6 +560,13 @@ module Ci
self.job_artifacts.update_all(expire_at: nil)
end
+ def artifacts_file_for_type(type)
+ file = job_artifacts.find_by(file_type: Ci::JobArtifact.file_types[type])&.file
+ # TODO: to be removed once legacy artifacts is removed
+ file ||= legacy_artifacts_file if type == :archive
+ file
+ end
+
def coverage_regex
super || project.try(:build_coverage_regex)
end
@@ -641,22 +694,57 @@ module Ci
def collect_test_reports!(test_reports)
test_reports.get_suite(group_name).tap do |test_suite|
- each_test_report do |file_type, blob|
- Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, test_suite)
+ each_report(Ci::JobArtifact::TEST_REPORT_FILE_TYPES) do |file_type, blob|
+ Gitlab::Ci::Parsers::Test.fabricate!(file_type).parse!(blob, test_suite)
end
end
end
+ # Virtual deployment status depending on the environment status.
+ def deployment_status
+ return nil unless starts_environment?
+
+ if success?
+ return successful_deployment_status
+ elsif complete? && !success?
+ return :failed
+ end
+
+ :creating
+ end
+
private
- def each_test_report
- Ci::JobArtifact::TEST_REPORT_FILE_TYPES.each do |file_type|
- public_send("job_artifacts_#{file_type}").each_blob do |blob| # rubocop:disable GitlabSecurity/PublicSend
- yield file_type, blob
+ def erase_old_artifacts!
+ # TODO: To be removed once we get rid of
+ remove_artifacts_file!
+ remove_artifacts_metadata!
+ save
+ end
+
+ def successful_deployment_status
+ if success? && last_deployment&.last?
+ return :last
+ elsif success? && last_deployment.present?
+ return :out_of_date
+ end
+
+ :creating
+ end
+
+ def each_report(report_types)
+ job_artifacts_for_types(report_types).each do |report_artifact|
+ report_artifact.each_blob do |blob|
+ yield report_artifact.file_type, blob
end
end
end
+ def job_artifacts_for_types(report_types)
+ # Use select to leverage cached associations and avoid N+1 queries
+ job_artifacts.select { |artifact| artifact.file_type.in?(report_types) }
+ end
+
def update_artifacts_size
self.artifacts_size = legacy_artifacts_file&.size
end
@@ -700,6 +788,9 @@ module Ci
variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(','))
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
+ variables.append(key: 'CI_SERVER_VERSION_MAJOR', value: gitlab_version_info.major.to_s)
+ variables.append(key: 'CI_SERVER_VERSION_MINOR', value: gitlab_version_info.minor.to_s)
+ variables.append(key: 'CI_SERVER_VERSION_PATCH', value: gitlab_version_info.patch.to_s)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab.revision)
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
@@ -714,6 +805,10 @@ module Ci
end
end
+ def gitlab_version_info
+ @gitlab_version_info ||= Gitlab::VersionInfo.parse(Gitlab::VERSION)
+ end
+
def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_BUILD_REF', value: sha)
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 17b7ee4f07e..cb73fc74bb6 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -9,8 +9,30 @@ module Ci
NotSupportedAdapterError = Class.new(StandardError)
TEST_REPORT_FILE_TYPES = %w[junit].freeze
- DEFAULT_FILE_NAMES = { junit: 'junit.xml' }.freeze
- TYPE_AND_FORMAT_PAIRS = { archive: :zip, metadata: :gzip, trace: :raw, junit: :gzip }.freeze
+ NON_ERASABLE_FILE_TYPES = %w[trace].freeze
+ DEFAULT_FILE_NAMES = {
+ archive: nil,
+ metadata: nil,
+ trace: nil,
+ junit: 'junit.xml',
+ codequality: 'codequality.json',
+ sast: 'gl-sast-report.json',
+ dependency_scanning: 'gl-dependency-scanning-report.json',
+ container_scanning: 'gl-container-scanning-report.json',
+ dast: 'gl-dast-report.json'
+ }.freeze
+
+ TYPE_AND_FORMAT_PAIRS = {
+ archive: :zip,
+ metadata: :gzip,
+ trace: :raw,
+ junit: :gzip,
+ codequality: :gzip,
+ sast: :gzip,
+ dependency_scanning: :gzip,
+ container_scanning: :gzip,
+ dast: :gzip
+ }.freeze
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
@@ -27,8 +49,18 @@ module Ci
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
+ scope :with_file_types, -> (file_types) do
+ types = self.file_types.select { |file_type| file_types.include?(file_type) }.values
+
+ where(file_type: types)
+ end
+
scope :test_reports, -> do
- types = self.file_types.select { |file_type| TEST_REPORT_FILE_TYPES.include?(file_type) }.values
+ with_file_types(TEST_REPORT_FILE_TYPES)
+ end
+
+ scope :erasable, -> do
+ types = self.file_types.reject { |file_type| NON_ERASABLE_FILE_TYPES.include?(file_type) }.values
where(file_type: types)
end
@@ -39,7 +71,12 @@ module Ci
archive: 1,
metadata: 2,
trace: 3,
- junit: 4
+ junit: 4,
+ sast: 5, ## EE-specific
+ dependency_scanning: 6, ## EE-specific
+ container_scanning: 7, ## EE-specific
+ dast: 8, ## EE-specific
+ codequality: 9 ## EE-specific
}
enum file_format: {
@@ -48,6 +85,20 @@ module Ci
gzip: 3
}
+ # `file_location` indicates where actual files are stored.
+ # Ideally, actual files should be stored in the same directory, and use the same
+ # convention to generate its path. However, sometimes we can't do so due to backward-compatibility.
+ #
+ # legacy_path ... The actual file is stored at a path consists of a timestamp
+ # and raw project/model IDs. Those rows were migrated from
+ # `ci_builds.artifacts_file` and `ci_builds.artifacts_metadata`
+ # hashed_path ... The actual file is stored at a path consists of a SHA2 based on the project ID.
+ # This is the default value.
+ enum file_location: {
+ legacy_path: 1,
+ hashed_path: 2
+ }
+
FILE_FORMAT_ADAPTERS = {
gzip: Gitlab::Ci::Build::Artifacts::GzipFileAdapter
}.freeze
@@ -72,6 +123,12 @@ module Ci
[nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
end
+ def hashed_path?
+ return true if trace? # ArchiveLegacyTraces background migration might not have `file_location` column
+
+ super || self.file_location.nil?
+ end
+
def expire_in
expire_at - Time.now if expire_at
end
@@ -108,7 +165,7 @@ module Ci
end
def update_project_statistics_after_destroy
- update_project_statistics(-self.size)
+ update_project_statistics(-self.size.to_i)
end
def update_project_statistics(difference)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 526bf7af99b..17024e8a0af 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -35,6 +35,7 @@ module Ci
has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
+ has_many :scheduled_actions, -> { latest.scheduled_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
@@ -80,7 +81,7 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
- transition [:created, :skipped] => :pending
+ transition [:created, :skipped, :scheduled] => :pending
transition [:success, :failed, :canceled] => :running
end
@@ -108,6 +109,10 @@ module Ci
transition any - [:manual] => :manual
end
+ event :delay do
+ transition any - [:scheduled] => :scheduled
+ end
+
# IMPORTANT
# Do not add any operations to this state_machine
# Create a separate worker for each new operation
@@ -161,6 +166,12 @@ module Ci
PipelineNotificationWorker.perform_async(pipeline.id)
end
end
+
+ after_transition any => [:failed] do |pipeline|
+ next unless pipeline.auto_devops_source?
+
+ pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
+ end
end
scope :internal, -> { where(source: internal_sources) }
@@ -381,10 +392,12 @@ module Ci
end
end
+ # rubocop: disable CodeReuse/ServiceClass
def retry_failed(current_user)
Ci::RetryPipelineService.new(project, current_user)
.execute(self)
end
+ # rubocop: enable CodeReuse/ServiceClass
def mark_as_processable_after_stage(stage_idx)
builds.skipped.after_stage(stage_idx).find_each(&:process)
@@ -458,7 +471,7 @@ module Ci
return @config_processor if defined?(@config_processor)
@config_processor ||= begin
- Gitlab::Ci::YamlProcessor.new(ci_yaml_file)
+ ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha })
rescue Gitlab::Ci::YamlProcessor::ValidationError => e
self.yaml_errors = e.message
nil
@@ -519,9 +532,11 @@ module Ci
project.notes.for_commit_id(sha)
end
+ # rubocop: disable CodeReuse/ServiceClass
def process!
Ci::ProcessPipelineService.new(project, user).execute(self)
end
+ # rubocop: enable CodeReuse/ServiceClass
def update_status
retry_optimistic_lock(self) do
@@ -534,6 +549,7 @@ module Ci
when 'canceled' then cancel
when 'skipped' then skip
when 'manual' then block
+ when 'scheduled' then delay
else
raise HasStatus::UnknownStatusError,
"Unknown status `#{latest_builds_status}`"
@@ -617,6 +633,22 @@ module Ci
end
end
+ def branch_updated?
+ strong_memoize(:branch_updated) do
+ push_details.branch_updated?
+ end
+ end
+
+ def modified_paths
+ strong_memoize(:modified_paths) do
+ push_details.modified_paths
+ end
+ end
+
+ def default_branch?
+ ref == project.default_branch
+ end
+
private
def ci_yaml_from_repo
@@ -640,6 +672,22 @@ module Ci
Gitlab::DataBuilder::Pipeline.build(self)
end
+ def push_details
+ strong_memoize(:push_details) do
+ Gitlab::Git::Push.new(project, before_sha, sha, push_ref)
+ end
+ end
+
+ def push_ref
+ if branch?
+ Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
+ elsif tag?
+ Gitlab::Git::TAG_REF_PREFIX + ref.to_s
+ else
+ raise ArgumentError, 'Invalid pipeline type!'
+ end
+ end
+
def latest_builds_status
return 'failed' unless yaml_errors.blank?
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index 017ec0b145a..08514d6af4e 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -10,5 +10,9 @@ module Ci
alias_attribute :secret_value, :value
validates :key, uniqueness: { scope: :pipeline_id }
+
+ def hook_attrs
+ { key: key, value: value }
+ end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index f41955f43e7..31330d0682e 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -7,11 +7,26 @@ module Ci
include IgnorableColumn
include RedisCacheable
include ChronicDurationAttribute
+ include FromUnion
+
+ enum access_level: {
+ not_protected: 0,
+ ref_protected: 1
+ }
+
+ enum runner_type: {
+ instance_type: 1,
+ group_type: 2,
+ project_type: 3
+ }
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes
- AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
+ AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
+ AVAILABLE_TYPES = runner_types.keys.freeze
+ AVAILABLE_STATUSES = %w[active paused online offline].freeze
+ AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
ignore_column :is_shared
@@ -29,6 +44,13 @@ module Ci
scope :active, -> { where(active: true) }
scope :paused, -> { where(active: false) }
scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
+ # The following query using negation is cheaper than using `contacted_at <= ?`
+ # because there are less runners online than have been created. The
+ # resulting query is quickly finding online ones and then uses the regular
+ # indexed search and rejects the ones that are in the previous set. If we
+ # did `contacted_at <= ?` the query would effectively have to do a seq
+ # scan.
+ scope :offline, -> { where.not(id: online) }
scope :ordered, -> { order(id: :desc) }
# BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb`
@@ -48,21 +70,32 @@ module Ci
}
scope :owned_or_instance_wide, -> (project_id) do
- union = Gitlab::SQL::Union.new(
- [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), instance_type],
+ from_union(
+ [
+ belonging_to_project(project_id),
+ belonging_to_parent_group_of_project(project_id),
+ instance_type
+ ],
remove_duplicates: false
)
- from("(#{union.to_sql}) ci_runners")
end
scope :assignable_for, ->(project) do
# FIXME: That `to_sql` is needed to workaround a weird Rails bug.
# Without that, placeholders would miss one and couldn't match.
+ #
+ # We use "unscoped" here so that any current Ci::Runner filters don't
+ # apply to the inner query, which is not necessary.
+ exclude_runners = unscoped { project.runners.select(:id) }.to_sql
+
where(locked: false)
- .where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})")
+ .where.not("ci_runners.id IN (#{exclude_runners})")
.project_type
end
+ scope :order_contacted_at_asc, -> { order(contacted_at: :asc) }
+ scope :order_created_at_desc, -> { order(created_at: :desc) }
+
validate :tag_constraints
validates :access_level, presence: true
validates :runner_type, presence: true
@@ -76,17 +109,6 @@ module Ci
after_destroy :cleanup_runner_queue
- enum access_level: {
- not_protected: 0,
- ref_protected: 1
- }
-
- enum runner_type: {
- instance_type: 1,
- group_type: 2,
- project_type: 3
- }
-
cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
@@ -115,6 +137,14 @@ module Ci
ONLINE_CONTACT_TIMEOUT.ago
end
+ def self.order_by(order)
+ if order == 'contacted_asc'
+ order_contacted_at_asc
+ else
+ order_created_at_desc
+ end
+ end
+
def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank?
end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 511ded55dc3..58f3fe2460a 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -65,6 +65,10 @@ module Ci
event :block do
transition any - [:manual] => :manual
end
+
+ event :delay do
+ transition any - [:scheduled] => :scheduled
+ end
end
def update_status
@@ -77,6 +81,7 @@ module Ci
when 'failed' then drop
when 'canceled' then cancel
when 'manual' then block
+ when 'scheduled' then delay
when 'skipped', nil then skip
else
raise HasStatus::UnknownStatusError,
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index 913936a0bcb..0b52c690e93 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -8,6 +8,8 @@ module Ci
belongs_to :pipeline, foreign_key: :commit_id
has_many :builds
+ delegate :short_token, to: :trigger, prefix: true, allow_nil: true
+
# We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables.
# Ci::TriggerRequest doesn't save variables anymore.
validates :variables, absence: true
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index 55bbf7cae7e..423071ec024 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -32,7 +32,8 @@ module Clusters
def install_command
Gitlab::Kubernetes::Helm::InitCommand.new(
name: name,
- files: files
+ files: files,
+ rbac: cluster.platform_kubernetes_rbac?
)
end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 93f654e0638..bd0286ee3f9 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -39,6 +39,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files
)
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index ef1c76c03bd..7be6a14f585 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -40,6 +40,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
repository: repository
@@ -72,6 +73,11 @@ module Clusters
"clientSecret" => oauth_application.secret,
"callbackUrl" => callback_url
}
+ },
+ "singleuser" => {
+ "extraEnv" => {
+ "GITLAB_CLUSTER_ID" => cluster.id
+ }
}
}
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 88399dbbb95..46d0388a464 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -48,6 +48,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files
)
@@ -71,7 +72,7 @@ module Clusters
private
def kube_client
- cluster&.kubeclient
+ cluster&.kubeclient&.core_client
end
end
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index bde255723c8..a4a2e2b79a6 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -33,6 +33,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
repository: repository
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 7cf75403ab6..d7011ef447a 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -42,6 +42,7 @@ module Clusters
delegate :on_creation?, to: :provider, allow_nil: true
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
+ delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index d4d3859dfd5..a9df59fc059 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -15,6 +15,9 @@ module Clusters
state :scheduled, value: 1
state :installing, value: 2
state :installed, value: 3
+ state :updating, value: 4
+ state :updated, value: 5
+ state :update_errored, value: 6
event :make_scheduled do
transition [:installable, :errored] => :scheduled
@@ -32,6 +35,18 @@ module Clusters
transition any => :errored
end
+ event :make_updating do
+ transition [:installed, :updated, :update_errored] => :updating
+ end
+
+ event :make_updated do
+ transition [:updating] => :updated
+ end
+
+ event :make_update_errored do
+ transition any => :update_errored
+ end
+
before_transition any => [:scheduled] do |app_status, _|
app_status.status_reason = nil
end
@@ -40,6 +55,15 @@ module Clusters
status_reason = transition.args.first
app_status.status_reason = status_reason if status_reason
end
+
+ before_transition any => [:updating] do |app_status, _|
+ app_status.status_reason = nil
+ end
+
+ before_transition any => [:update_errored] do |app_status, transition|
+ status_reason = transition.args.first
+ app_status.status_reason = status_reason if status_reason
+ end
end
end
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index e6ddca0d5d0..3a335909101 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -5,6 +5,7 @@ module Clusters
class Kubernetes < ActiveRecord::Base
include Gitlab::Kubernetes
include ReactiveCaching
+ include EnumWithNil
self.table_name = 'cluster_platforms_kubernetes'
self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] }
@@ -47,6 +48,12 @@ module Clusters
alias_method :active?, :enabled?
+ enum_with_nil authorization_type: {
+ unknown_authorization: nil,
+ rbac: 1,
+ abac: 2
+ }
+
def actual_namespace
if namespace.present?
namespace
@@ -95,7 +102,7 @@ module Clusters
end
def kubeclient
- @kubeclient ||= build_kubeclient!
+ @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
end
private
@@ -115,15 +122,16 @@ module Clusters
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
- def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ def build_kube_client!(api_groups: ['api'], api_version: 'v1')
raise "Incomplete settings" unless api_url && actual_namespace
unless (username && password) || token
raise "Either username/password or token is required to access API"
end
- ::Kubeclient::Client.new(
- join_api_url(api_path),
+ Gitlab::Kubernetes::KubeClient.new(
+ api_url,
+ api_groups,
api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
@@ -133,7 +141,7 @@ module Clusters
# Returns a hash of all pods in the namespace
def read_pods
- kubeclient = build_kubeclient!
+ kubeclient = build_kube_client!
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue Kubeclient::HttpError => err
@@ -157,15 +165,6 @@ module Clusters
{ bearer_token: token }
end
- def join_api_url(api_path)
- url = URI.parse(api_url)
- prefix = url.path.sub(%r{/+\z}, '')
-
- url.path = [prefix, api_path].join("/")
-
- url.to_s
- end
-
def terminal_auth
{
token: token,
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 594972ad344..a61ed03cf35 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -22,6 +22,7 @@ class Commit
attr_accessor :project, :author
attr_accessor :redacted_description_html
attr_accessor :redacted_title_html
+ attr_accessor :redacted_full_title_html
attr_reader :gpg_commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
@@ -318,7 +319,11 @@ class Commit
def status(ref = nil)
return @statuses[ref] if @statuses.key?(ref)
- @statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id]
+ @statuses[ref] = status_for_project(ref, project)
+ end
+
+ def status_for_project(ref, pipeline_project)
+ pipeline_project.pipelines.latest_status_per_commit(id, ref)[id]
end
def set_status_for_ref(ref, status)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index b65d7672973..06507345fe8 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -49,7 +49,8 @@ class CommitStatus < ActiveRecord::Base
stuck_or_timeout_failure: 3,
runner_system_failure: 4,
missing_dependency_failure: 5,
- runner_unsupported: 6
+ runner_unsupported: 6,
+ stale_schedule: 7
}
##
@@ -58,9 +59,11 @@ class CommitStatus < ActiveRecord::Base
# These are pages deployments and external statuses.
#
before_create unless: :importing? do
+ # rubocop: disable CodeReuse/ServiceClass
Ci::EnsureStageService.new(project, user).execute(self) do |stage|
self.run_after_commit { StageUpdateWorker.perform_async(stage.id) }
end
+ # rubocop: enable CodeReuse/ServiceClass
end
state_machine :status do
@@ -69,7 +72,7 @@ class CommitStatus < ActiveRecord::Base
end
event :enqueue do
- transition [:created, :skipped, :manual] => :pending
+ transition [:created, :skipped, :manual, :scheduled] => :pending
end
event :run do
@@ -81,7 +84,7 @@ class CommitStatus < ActiveRecord::Base
end
event :drop do
- transition [:created, :pending, :running] => :failed
+ transition [:created, :pending, :running, :scheduled] => :failed
end
event :success do
@@ -89,10 +92,10 @@ class CommitStatus < ActiveRecord::Base
end
event :cancel do
- transition [:created, :pending, :running, :manual] => :canceled
+ transition [:created, :pending, :running, :manual, :scheduled] => :canceled
end
- before_transition [:created, :skipped, :manual] => :pending do |commit_status|
+ before_transition [:created, :skipped, :manual, :scheduled] => :pending do |commit_status|
commit_status.queued_at = Time.now
end
@@ -130,10 +133,12 @@ class CommitStatus < ActiveRecord::Base
after_transition any => :failed do |commit_status|
next unless commit_status.project
+ # rubocop: disable CodeReuse/ServiceClass
commit_status.run_after_commit do
MergeRequests::AddTodoWhenBuildFailsService
.new(project, nil).execute(self)
end
+ # rubocop: enable CodeReuse/ServiceClass
end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index c0233661a9b..0d5311a9985 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -9,7 +9,7 @@ module Avatarable
include Gitlab::Utils::StrongMemoize
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
- validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
+ validates :avatar, file_size: { maximum: 200.kilobytes.to_i }, if: :avatar_changed?
mount_uploader :avatar, AvatarUploader
diff --git a/app/models/concerns/bulk_member_access_load.rb b/app/models/concerns/bulk_member_access_load.rb
index c4346d5dd17..041ed3755e0 100644
--- a/app/models/concerns/bulk_member_access_load.rb
+++ b/app/models/concerns/bulk_member_access_load.rb
@@ -16,9 +16,9 @@ module BulkMemberAccessLoad
key = max_member_access_for_resource_key(resource_klass, memoization_index)
access = {}
- if RequestStore.active?
- RequestStore.store[key] ||= {}
- access = RequestStore.store[key]
+ if Gitlab::SafeRequestStore.active?
+ Gitlab::SafeRequestStore[key] ||= {}
+ access = Gitlab::SafeRequestStore[key]
end
# Look up only the IDs we need
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index 62b78c3611c..f8034be8376 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -27,11 +27,7 @@ module CacheableAttributes
end
def cached
- if RequestStore.active?
- RequestStore[:"#{name}_cached_attributes"] ||= retrieve_from_cache
- else
- retrieve_from_cache
- end
+ Gitlab::SafeRequestStore[:"#{name}_cached_attributes"] ||= retrieve_from_cache
end
def retrieve_from_cache
diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb
index 6e80365ee5b..c93b6589ee7 100644
--- a/app/models/concerns/case_sensitivity.rb
+++ b/app/models/concerns/case_sensitivity.rb
@@ -9,23 +9,46 @@ module CaseSensitivity
#
# Unlike other ActiveRecord methods this method only operates on a Hash.
def iwhere(params)
- criteria = self
- cast_lower = Gitlab::Database.postgresql?
+ criteria = self
params.each do |key, value|
- column = ActiveRecord::Base.connection.quote_table_name(key)
+ criteria = case value
+ when Array
+ criteria.where(value_in(key, value))
+ else
+ criteria.where(value_equal(key, value))
+ end
+ end
+
+ criteria
+ end
- condition =
- if cast_lower
- "LOWER(#{column}) = LOWER(:value)"
- else
- "#{column} = :value"
- end
+ private
+
+ def value_equal(column, value)
+ lower_value = lower_value(value)
+
+ lower_column(arel_table[column]).eq(lower_value).to_sql
+ end
- criteria = criteria.where(condition, value: value)
+ def value_in(column, values)
+ lower_values = values.map do |value|
+ lower_value(value)
end
- criteria
+ lower_column(arel_table[column]).in(lower_values).to_sql
+ end
+
+ def lower_value(value)
+ return value if Gitlab::Database.mysql?
+
+ Arel::Nodes::NamedFunction.new('LOWER', [Arel::Nodes.build_quoted(value)])
+ end
+
+ def lower_column(column)
+ return column if Gitlab::Database.mysql?
+
+ column.lower
end
end
end
diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb
new file mode 100644
index 00000000000..b61bf29e6ad
--- /dev/null
+++ b/app/models/concerns/diff_positionable_note.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+module DiffPositionableNote
+ extend ActiveSupport::Concern
+
+ included do
+ before_validation :set_original_position, on: :create
+ before_validation :update_position, on: :create, if: :on_text?
+
+ serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
+ end
+
+ %i(original_position position change_position).each do |meth|
+ define_method "#{meth}=" do |new_position|
+ if new_position.is_a?(String)
+ new_position = JSON.parse(new_position) rescue nil
+ end
+
+ if new_position.is_a?(Hash)
+ new_position = new_position.with_indifferent_access
+ new_position = Gitlab::Diff::Position.new(new_position)
+ end
+
+ return if new_position == read_attribute(meth)
+
+ super(new_position)
+ end
+ end
+
+ def on_text?
+ position&.position_type == "text"
+ end
+
+ def on_image?
+ position&.position_type == "image"
+ end
+
+ def supported?
+ for_commit? || self.noteable.has_complete_diff_refs?
+ end
+
+ def active?(diff_refs = nil)
+ return false unless supported?
+ return true if for_commit?
+
+ diff_refs ||= noteable.diff_refs
+
+ self.position.diff_refs == diff_refs
+ end
+
+ def set_original_position
+ return unless position
+
+ self.original_position = self.position.dup unless self.original_position&.complete?
+ end
+
+ def update_position
+ return unless supported?
+ return if for_commit?
+
+ return if active?
+ return unless position
+
+ tracer = Gitlab::Diff::PositionTracer.new(
+ project: self.project,
+ old_diff_refs: self.position.diff_refs,
+ new_diff_refs: self.noteable.diff_refs,
+ paths: self.position.paths
+ )
+
+ result = tracer.trace(self.position)
+ return unless result
+
+ if result[:outdated]
+ self.change_position = result[:position]
+ else
+ self.position = result[:position]
+ end
+ end
+end
diff --git a/app/models/concerns/from_union.rb b/app/models/concerns/from_union.rb
new file mode 100644
index 00000000000..9b8595b1211
--- /dev/null
+++ b/app/models/concerns/from_union.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module FromUnion
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Produces a query that uses a FROM to select data using a UNION.
+ #
+ # Using a FROM for a UNION has in the past lead to better query plans. As
+ # such, we generally recommend this pattern instead of using a WHERE IN.
+ #
+ # Example:
+ # users = User.from_union([User.where(id: 1), User.where(id: 2)])
+ #
+ # This would produce the following SQL query:
+ #
+ # SELECT *
+ # FROM (
+ # SELECT *
+ # FROM users
+ # WHERE id = 1
+ #
+ # UNION
+ #
+ # SELECT *
+ # FROM users
+ # WHERE id = 2
+ # ) users;
+ #
+ # members - An Array of ActiveRecord::Relation objects to use in the UNION.
+ #
+ # remove_duplicates - A boolean indicating if duplicate entries should be
+ # removed. Defaults to true.
+ #
+ # alias_as - The alias to use for the sub query. Defaults to the name of the
+ # table of the current model.
+ # rubocop: disable Gitlab/Union
+ def from_union(members, remove_duplicates: true, alias_as: table_name)
+ union = Gitlab::SQL::Union
+ .new(members, remove_duplicates: remove_duplicates)
+ .to_sql
+
+ # This pattern is necessary as a bug in Rails 4 can cause the use of
+ # `from("string here").includes(:foo)` to break ActiveRecord. This is
+ # fixed in https://github.com/rails/rails/pull/25374, which is released as
+ # part of Rails 5.
+ from([Arel.sql("(#{union}) #{alias_as}")])
+ end
+ # rubocop: enable Gitlab/Union
+ end
+end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index b3960cbad1a..b92643f87f8 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -4,14 +4,15 @@ module HasStatus
extend ActiveSupport::Concern
DEFAULT_STATUS = 'created'.freeze
- BLOCKED_STATUS = 'manual'.freeze
- AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual].freeze
- STARTED_STATUSES = %w[running success failed skipped manual].freeze
+ BLOCKED_STATUS = %w[manual scheduled].freeze
+ AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual scheduled].freeze
+ STARTED_STATUSES = %w[running success failed skipped manual scheduled].freeze
ACTIVE_STATUSES = %w[pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
- ORDERED_STATUSES = %w[failed pending running manual canceled success skipped created].freeze
+ ORDERED_STATUSES = %w[failed pending running manual scheduled canceled success skipped created].freeze
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
- failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
+ failed: 4, canceled: 5, skipped: 6, manual: 7,
+ scheduled: 8 }.freeze
UnknownStatusError = Class.new(StandardError)
@@ -24,6 +25,7 @@ module HasStatus
created = scope_relevant.created.select('count(*)').to_sql
success = scope_relevant.success.select('count(*)').to_sql
manual = scope_relevant.manual.select('count(*)').to_sql
+ scheduled = scope_relevant.scheduled.select('count(*)').to_sql
pending = scope_relevant.pending.select('count(*)').to_sql
running = scope_relevant.running.select('count(*)').to_sql
skipped = scope_relevant.skipped.select('count(*)').to_sql
@@ -40,6 +42,7 @@ module HasStatus
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})>0 THEN 'running'
WHEN (#{manual})>0 THEN 'manual'
+ WHEN (#{scheduled})>0 THEN 'scheduled'
WHEN (#{created})>0 THEN 'running'
ELSE 'failed'
END)"
@@ -74,6 +77,7 @@ module HasStatus
state :canceled, value: 'canceled'
state :skipped, value: 'skipped'
state :manual, value: 'manual'
+ state :scheduled, value: 'scheduled'
end
scope :created, -> { where(status: 'created') }
@@ -85,6 +89,7 @@ module HasStatus
scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') }
+ scope :scheduled, -> { where(status: 'scheduled') }
scope :alive, -> { where(status: [:created, :pending, :running]) }
scope :created_or_pending, -> { where(status: [:created, :pending]) }
scope :running_or_pending, -> { where(status: [:running, :pending]) }
@@ -92,7 +97,7 @@ module HasStatus
scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) }
scope :cancelable, -> do
- where(status: [:running, :pending, :created])
+ where(status: [:running, :pending, :created, :scheduled])
end
end
@@ -109,7 +114,7 @@ module HasStatus
end
def blocked?
- BLOCKED_STATUS == status
+ BLOCKED_STATUS.include?(status)
end
private
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index f881ce2321c..2aa52bbaeea 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -49,7 +49,7 @@ module Issuable
end
end
- has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :label_links, as: :target, dependent: :destroy, inverse_of: :target # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -76,6 +76,7 @@ module Issuable
scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
+ scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :opened, -> { with_state(:opened) }
scope :only_opened, -> { with_state(:opened) }
@@ -109,10 +110,6 @@ module Issuable
false
end
- def etag_caching_enabled?
- false
- end
-
def has_multiple_assignees?
assignees.count > 1
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 393607e82c4..298d0d42d90 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -61,7 +61,7 @@ module Mentionable
cache_key: [self, attr],
author: author,
skip_project_check: skip_project_check?
- )
+ ).merge(mentionable_params)
extractor.analyze(text, options)
end
@@ -86,12 +86,11 @@ module Mentionable
return [] unless matches_cross_reference_regex?
refs = all_references(current_user)
- refs = (refs.issues + refs.merge_requests + refs.commits)
# We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the
# case for otherwise identical Commit objects.
- refs.reject { |ref| ref == local_reference }
+ extracted_mentionables(refs).reject { |ref| ref == local_reference }
end
# Uses regex to quickly determine if mentionables might be referenced
@@ -134,6 +133,10 @@ module Mentionable
private
+ def extracted_mentionables(refs)
+ refs.issues + refs.merge_requests + refs.commits
+ end
+
# Returns a Hash of changed mentionable fields
#
# Preference is given to the `changes` Hash, but falls back to
@@ -161,4 +164,8 @@ module Mentionable
def skip_project_check?
false
end
+
+ def mentionable_params
+ {}
+ end
end
diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb
index f6fd28bac33..fe8fbb71184 100644
--- a/app/models/concerns/mentionable/reference_regexes.rb
+++ b/app/models/concerns/mentionable/reference_regexes.rb
@@ -5,13 +5,19 @@ module Mentionable
def self.reference_pattern(link_patterns, issue_pattern)
Regexp.union(link_patterns,
issue_pattern,
- Commit.reference_pattern,
- MergeRequest.reference_pattern)
+ *other_patterns)
+ end
+
+ def self.other_patterns
+ [
+ Commit.reference_pattern,
+ MergeRequest.reference_pattern
+ ]
end
DEFAULT_PATTERN = begin
issue_pattern = Issue.reference_pattern
- link_patterns = Regexp.union([Issue, Commit, MergeRequest].map(&:link_reference_pattern))
+ link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic].map(&:link_reference_pattern).compact)
reference_pattern(link_patterns, issue_pattern)
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index ce778eae271..098eed137ba 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -82,4 +82,23 @@ module Noteable
def lockable?
[MergeRequest, Issue].include?(self.class)
end
+
+ def etag_caching_enabled?
+ false
+ end
+
+ def expire_note_etag_cache
+ return unless discussions_rendered_on_frontend?
+ return unless etag_caching_enabled?
+
+ Gitlab::EtagCaching::Store.new.touch(note_etag_key)
+ end
+
+ def note_etag_key
+ Gitlab::Routing.url_helpers.project_noteable_notes_path(
+ project,
+ target_type: self.class.name.underscore,
+ target_id: id
+ )
+ end
end
diff --git a/app/models/concerns/project_services_loggable.rb b/app/models/concerns/project_services_loggable.rb
new file mode 100644
index 00000000000..fecd77cdc98
--- /dev/null
+++ b/app/models/concerns/project_services_loggable.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module ProjectServicesLoggable
+ def log_info(message, params = {})
+ message = build_message(message, params)
+
+ logger.info(message)
+ end
+
+ def log_error(message, params = {})
+ message = build_message(message, params)
+
+ logger.error(message)
+ end
+
+ def build_message(message, params = {})
+ {
+ service_class: self.class.name,
+ project_id: project.id,
+ project_path: project.full_path,
+ message: message
+ }.merge(params)
+ end
+
+ def logger
+ Gitlab::ProjectServiceLogger
+ end
+end
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 744f7f48dc8..58761fce952 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -2,18 +2,17 @@
module ProtectedBranchAccess
extend ActiveSupport::Concern
+ include ProtectedRefAccess
included do
- include ProtectedRefAccess
-
belongs_to :protected_branch
delegate :project, to: :protected_branch
+ end
- def check_access(user)
- return false if access_level == Gitlab::Access::NO_ACCESS
+ def check_access(user)
+ return false if access_level == Gitlab::Access::NO_ACCESS
- super
- end
+ super
end
end
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index e62e680af6e..af387c99f3d 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -50,14 +50,20 @@ module ProtectedRef
.map(&:"#{action}_access_levels").flatten
end
+ # Returns all protected refs that match the given ref name.
+ # This checks all records from the scope built up so far, and does
+ # _not_ return a relation.
+ #
+ # This method optionally takes in a list of `protected_refs` to search
+ # through, to avoid calling out to the database.
def matching(ref_name, protected_refs: nil)
- ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs)
+ (protected_refs || self.all).select { |protected_ref| protected_ref.matches?(ref_name) }
end
end
private
def ref_matcher
- @ref_matcher ||= ProtectedRefMatcher.new(self)
+ @ref_matcher ||= RefMatcher.new(self.name)
end
end
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index efa666fb3f2..583751ea6ac 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -3,18 +3,22 @@
module ProtectedRefAccess
extend ActiveSupport::Concern
- ALLOWED_ACCESS_LEVELS = [
- Gitlab::Access::MAINTAINER,
- Gitlab::Access::DEVELOPER,
- Gitlab::Access::NO_ACCESS
- ].freeze
-
HUMAN_ACCESS_LEVELS = {
Gitlab::Access::MAINTAINER => "Maintainers".freeze,
Gitlab::Access::DEVELOPER => "Developers + Maintainers".freeze,
Gitlab::Access::NO_ACCESS => "No one".freeze
}.freeze
+ class_methods do
+ def allowed_access_levels
+ [
+ Gitlab::Access::MAINTAINER,
+ Gitlab::Access::DEVELOPER,
+ Gitlab::Access::NO_ACCESS
+ ]
+ end
+ end
+
included do
scope :master, -> { maintainer } # @deprecated
scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) }
@@ -26,7 +30,7 @@ module ProtectedRefAccess
scope :for_group, -> { where.not(group_id: nil) }
validates :access_level, presence: true, if: :role?, inclusion: {
- in: ALLOWED_ACCESS_LEVELS
+ in: self.allowed_access_levels
}
end
diff --git a/app/models/concerns/protected_tag_access.rb b/app/models/concerns/protected_tag_access.rb
index 04bd54d6b1c..3f5696c0749 100644
--- a/app/models/concerns/protected_tag_access.rb
+++ b/app/models/concerns/protected_tag_access.rb
@@ -2,10 +2,9 @@
module ProtectedTagAccess
extend ActiveSupport::Concern
+ include ProtectedRefAccess
included do
- include ProtectedRefAccess
-
belongs_to :protected_tag
delegate :project, to: :protected_tag
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 3b745657a9e..7723c07279d 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -5,8 +5,10 @@ module Storage
extend ActiveSupport::Concern
def move_dir
- if any_project_has_container_registry_tags?
- raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
+ proj_with_tags = first_project_with_container_registry_tags
+
+ if proj_with_tags
+ raise Gitlab::UpdatePathError.new("Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry")
end
parent_was = if parent_changed? && parent_id_was.present?
@@ -25,8 +27,6 @@ module Storage
Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path)
end
- remove_exports!
-
# If repositories moved successfully we need to
# send update instructions to users.
# However we cannot allow rollback since we moved namespace dir
@@ -101,8 +101,6 @@ module Storage
end
end
end
-
- remove_exports!
end
def remove_legacy_exports!
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 1d0a61364b0..92a5c1112af 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -31,9 +31,11 @@ module Subscribable
end
def subscribers(project)
- subscriptions_available(project)
- .where(subscribed: true)
- .map(&:user)
+ relation = subscriptions_available(project)
+ .where(subscribed: true)
+ .select(:user_id)
+
+ User.where(id: relation)
end
def toggle_subscription(user, project = nil)
diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb
index f55ab2fcaf3..c52baa0524c 100644
--- a/app/models/concerns/triggerable_hooks.rb
+++ b/app/models/concerns/triggerable_hooks.rb
@@ -6,6 +6,7 @@ module TriggerableHooks
push_hooks: :push_events,
tag_push_hooks: :tag_push_events,
issue_hooks: :issues_events,
+ confidential_note_hooks: :confidential_note_events,
confidential_issue_hooks: :confidential_issues_events,
note_hooks: :note_events,
merge_request_hooks: :merge_requests_events,
@@ -28,6 +29,12 @@ module TriggerableHooks
public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend
end
+ def select_active(hooks_scope, data)
+ select do |hook|
+ ActiveHookFilter.new(hook).matches?(hooks_scope, data)
+ end
+ end
+
private
def triggerable_hooks(hooks)
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 41413854d5c..2c08a8e1acf 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -8,8 +8,7 @@ class ContainerRepository < ActiveRecord::Base
delegate :client, to: :registry
- before_destroy :delete_tags!
-
+ # rubocop: disable CodeReuse/ServiceClass
def registry
@registry ||= begin
token = Auth::ContainerRegistryAuthenticationService.full_access_token(path)
@@ -20,6 +19,7 @@ class ContainerRepository < ActiveRecord::Base
ContainerRegistry::Registry.new(url, token: token, path: host_port)
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def path
@path ||= [project.full_path, name]
diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb
index 13807d43265..32e8104125c 100644
--- a/app/models/dashboard_group_milestone.rb
+++ b/app/models/dashboard_group_milestone.rb
@@ -13,7 +13,11 @@ class DashboardGroupMilestone < GlobalMilestone
end
def self.build_collection(groups)
- MilestonesFinder.new(group_ids: groups.pluck(:id)).execute.map { |m| new(m) }
+ Milestone.of_groups(groups.select(:id))
+ .reorder_by_due_date_asc
+ .order_by_name_asc
+ .active
+ .map { |m| new(m) }
end
override :group_milestone?
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index fd5d7726fb6..db501b4b506 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -18,7 +18,7 @@ class DeployKey < Key
end
def orphaned?
- self.deploy_keys_projects.length == 0
+ self.deploy_keys_projects.empty?
end
def almost_orphaned?
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 716cf6574d3..95694377fe3 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -5,14 +5,11 @@
# A note of this type can be resolvable.
class DiffNote < Note
include NoteOnDiff
+ include DiffPositionableNote
include Gitlab::Utils::StrongMemoize
NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
- serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
- serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
- serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
-
validates :original_position, presence: true
validates :position, presence: true
validates :line_code, presence: true, line_code: true, if: :on_text?
@@ -21,8 +18,6 @@ class DiffNote < Note
validate :verify_supported
validate :diff_refs_match_commit, if: :for_commit?
- before_validation :set_original_position, on: :create
- before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code, if: :on_text?
after_save :keep_around_commits
after_commit :create_diff_file, on: :create
@@ -31,31 +26,6 @@ class DiffNote < Note
DiffDiscussion
end
- %i(original_position position change_position).each do |meth|
- define_method "#{meth}=" do |new_position|
- if new_position.is_a?(String)
- new_position = JSON.parse(new_position) rescue nil
- end
-
- if new_position.is_a?(Hash)
- new_position = new_position.with_indifferent_access
- new_position = Gitlab::Diff::Position.new(new_position)
- end
-
- return if new_position == read_attribute(meth)
-
- super(new_position)
- end
- end
-
- def on_text?
- position.position_type == "text"
- end
-
- def on_image?
- position.position_type == "image"
- end
-
def create_diff_file
return unless should_create_diff_file?
@@ -87,15 +57,6 @@ class DiffNote < Note
self.diff_file.line_code(self.diff_line)
end
- def active?(diff_refs = nil)
- return false unless supported?
- return true if for_commit?
-
- diff_refs ||= noteable.diff_refs
-
- self.position.diff_refs == diff_refs
- end
-
def created_at_diff?(diff_refs)
return false unless supported?
return true if for_commit?
@@ -131,7 +92,7 @@ class DiffNote < Note
# As an extra benefit, the returned `diff_file` already
# has `highlighted_diff_lines` data set from Redis on
# `Diff::FileCollection::MergeRequestDiff`.
- noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
+ noteable.diffs(original_position.diff_options).diff_files.first
else
original_position.diff_file(self.project.repository)
end
@@ -141,37 +102,10 @@ class DiffNote < Note
for_commit? || self.noteable.has_complete_diff_refs?
end
- def set_original_position
- self.original_position = self.position.dup unless self.original_position&.complete?
- end
-
def set_line_code
self.line_code = self.position.line_code(self.project.repository)
end
- def update_position
- return unless supported?
- return if for_commit?
-
- return if active?
-
- tracer = Gitlab::Diff::PositionTracer.new(
- project: self.project,
- old_diff_refs: self.position.diff_refs,
- new_diff_refs: self.noteable.diff_refs,
- paths: self.position.paths
- )
-
- result = tracer.trace(self.position)
- return unless result
-
- if result[:outdated]
- self.change_position = result[:position]
- else
- self.position = result[:position]
- end
- end
-
def verify_supported
return if supported?
diff --git a/app/models/environment.rb b/app/models/environment.rb
index c8d1d378ae0..309bd4f37c9 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -158,9 +158,11 @@ class Environment < ActiveRecord::Base
prometheus_adapter.query(:additional_metrics_environment, self) if has_metrics?
end
+ # rubocop: disable CodeReuse/ServiceClass
def prometheus_adapter
@prometheus_adapter ||= Prometheus::AdapterService.new(project, deployment_platform).prometheus_adapter
end
+ # rubocop: enable CodeReuse/ServiceClass
def slug
super.presence || generate_slug
diff --git a/app/models/epic.rb b/app/models/epic.rb
index f027993376c..ccd10593434 100644
--- a/app/models/epic.rb
+++ b/app/models/epic.rb
@@ -3,6 +3,10 @@
# Placeholder class for model that is implemented in EE
# It reserves '&' as a reference prefix, but the table does not exists in CE
class Epic < ActiveRecord::Base
+ def self.link_reference_pattern
+ nil
+ end
+
def self.reference_prefix
'&'
end
diff --git a/app/models/event.rb b/app/models/event.rb
index ba28866e8e6..2e690f8c013 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -3,6 +3,7 @@
class Event < ActiveRecord::Base
include Sortable
include IgnorableColumn
+ include FromUnion
default_scope { reorder(nil) }
CREATED = 1
@@ -147,21 +148,31 @@ class Event < ActiveRecord::Base
end
end
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
def visible_to_user?(user = nil)
if push? || commit_note?
Ability.allowed?(user, :download_code, project)
elsif membership_changed?
- true
+ Ability.allowed?(user, :read_project, project)
elsif created_project?
- true
+ Ability.allowed?(user, :read_project, project)
elsif issue? || issue_note?
Ability.allowed?(user, :read_issue, note? ? note_target : target)
elsif merge_request? || merge_request_note?
Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
+ elsif personal_snippet_note?
+ Ability.allowed?(user, :read_personal_snippet, note_target)
+ elsif project_snippet_note?
+ Ability.allowed?(user, :read_project_snippet, note_target)
+ elsif milestone?
+ Ability.allowed?(user, :read_milestone, project)
else
- milestone?
+ false # No other event types are visible
end
end
+ # rubocop:enable Metrics/PerceivedComplexity
+ # rubocop:enable Metrics/CyclomaticComplexity
def project_name
if project
@@ -303,6 +314,10 @@ class Event < ActiveRecord::Base
note? && target && target.for_snippet?
end
+ def personal_snippet_note?
+ note? && target && target.for_personal_snippet?
+ end
+
def note_target
target.noteable
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 6e23e811b0e..a6cebabe089 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -17,7 +17,7 @@ class GlobalMilestone
params =
{ project_ids: projects.map(&:id), state: params[:state] }
- child_milestones = MilestonesFinder.new(params).execute
+ child_milestones = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped|
milestones_relation = Milestone.where(id: grouped.map(&:id))
@@ -48,7 +48,7 @@ class GlobalMilestone
params = { group_ids: [group.id], state: 'all' }
- relation = MilestonesFinder.new(params).execute
+ relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
grouped_by_state = relation.reorder(nil).group(:state).count
{
@@ -64,7 +64,7 @@ class GlobalMilestone
params = { project_ids: projects.map(&:id), state: 'all' }
- relation = MilestonesFinder.new(params).execute
+ relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count
opened = count_by_state(project_milestones_by_state_and_title, 'active')
diff --git a/app/models/group.rb b/app/models/group.rb
index 106a1f4a94c..612c546ca57 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -82,8 +82,17 @@ class Group < Namespace
User.reference_pattern
end
- def visible_to_user(user)
- where(id: user.authorized_groups.select(:id).reorder(nil))
+ # WARNING: This method should never be used on its own
+ # please do make sure the number of rows you are filtering is small
+ # enough for this query
+ def public_or_visible_to_user(user)
+ return public_to_user unless user
+
+ public_for_user = public_to_user_arel(user)
+ visible_for_user = visible_to_user_arel(user)
+ public_or_visible = public_for_user.or(visible_for_user)
+
+ where(public_or_visible)
end
def select_for_project_authorization
@@ -95,6 +104,23 @@ class Group < Namespace
super
end
end
+
+ private
+
+ def public_to_user_arel(user)
+ self.arel_table[:visibility_level]
+ .in(Gitlab::VisibilityLevel.levels_for_user(user))
+ end
+
+ def visible_to_user_arel(user)
+ groups_table = self.arel_table
+ authorized_groups = user.authorized_groups.as('authorized')
+
+ groups_table.project(1)
+ .from(authorized_groups)
+ .where(authorized_groups[:id].eq(groups_table[:id]))
+ .exists
+ end
end
# Overrides notification_settings has_many association
@@ -236,14 +262,18 @@ class Group < Namespace
system_hook_service.execute_hooks_for(self, :destroy)
end
+ # rubocop: disable CodeReuse/ServiceClass
def system_hook_service
SystemHooksService.new
end
+ # rubocop: enable CodeReuse/ServiceClass
+ # rubocop: disable CodeReuse/ServiceClass
def refresh_members_authorized_projects(blocking: true)
UserProjectAccessChangedService.new(user_ids_for_project_authorizations)
.execute(blocking: blocking)
end
+ # rubocop: enable CodeReuse/ServiceClass
def user_ids_for_project_authorizations
members_with_parents.pluck(:user_id)
@@ -300,14 +330,12 @@ class Group < Namespace
# 3. They belong to a sub-group or project in such sub-group
# 4. They belong to an ancestor group
def direct_and_indirect_users
- union = Gitlab::SQL::Union.new([
+ User.from_union([
User
.where(id: direct_and_indirect_members.select(:user_id))
.reorder(nil),
project_users_with_descendants
])
-
- User.from("(#{union.to_sql}) #{User.table_name}")
end
# Returns all users that are members of projects
diff --git a/app/models/hooks/active_hook_filter.rb b/app/models/hooks/active_hook_filter.rb
new file mode 100644
index 00000000000..283e2d680f4
--- /dev/null
+++ b/app/models/hooks/active_hook_filter.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class ActiveHookFilter
+ def initialize(hook)
+ @hook = hook
+ @push_events_filter_matcher = RefMatcher.new(@hook.push_events_branch_filter)
+ end
+
+ def matches?(hooks_scope, data)
+ return true if hooks_scope != :push_hooks
+ return true if @hook.push_events_branch_filter.blank?
+
+ branch_name = Gitlab::Git.branch_name(data[:ref])
+ @push_events_filter_matcher.matches?(branch_name)
+ end
+end
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index bda82a116a1..7d9f6d89d44 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -4,7 +4,9 @@ class ServiceHook < WebHook
belongs_to :service
validates :service, presence: true
+ # rubocop: disable CodeReuse/ServiceClass
def execute(data)
WebHookService.new(self, data, 'service_hook').execute
end
+ # rubocop: enable CodeReuse/ServiceClass
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index f18aadefa5c..68ba4b213b2 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -3,23 +3,72 @@
class WebHook < ActiveRecord::Base
include Sortable
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm',
+ key: Settings.attr_encrypted_db_key_base_truncated
+
+ attr_encrypted :url,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm',
+ key: Settings.attr_encrypted_db_key_base_truncated
+
has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :url, presence: true, public_url: { allow_localhost: lambda(&:allow_local_requests?),
allow_local_network: lambda(&:allow_local_requests?) }
validates :token, format: { without: /\n/ }
+ validates :push_events_branch_filter, branch_filter: true
+ # rubocop: disable CodeReuse/ServiceClass
def execute(data, hook_name)
WebHookService.new(self, data, hook_name).execute
end
+ # rubocop: enable CodeReuse/ServiceClass
+ # rubocop: disable CodeReuse/ServiceClass
def async_execute(data, hook_name)
WebHookService.new(self, data, hook_name).async_execute
end
+ # rubocop: enable CodeReuse/ServiceClass
# Allow urls pointing localhost and the local network
def allow_local_requests?
false
end
+
+ # In 11.4, the web_hooks table has both `token` and `encrypted_token` fields.
+ # Ensure that the encrypted version always takes precedence if present.
+ alias_method :attr_encrypted_token, :token
+ def token
+ attr_encrypted_token.presence || read_attribute(:token)
+ end
+
+ # In 11.4, the web_hooks table has both `token` and `encrypted_token` fields.
+ # Pending a background migration to encrypt all fields, we should just clear
+ # the unencrypted value whenever the new value is set.
+ alias_method :'attr_encrypted_token=', :'token='
+ def token=(value)
+ self.attr_encrypted_token = value
+
+ write_attribute(:token, nil)
+ end
+
+ # In 11.4, the web_hooks table has both `url` and `encrypted_url` fields.
+ # Ensure that the encrypted version always takes precedence if present.
+ alias_method :attr_encrypted_url, :url
+ def url
+ attr_encrypted_url.presence || read_attribute(:url)
+ end
+
+ # In 11.4, the web_hooks table has both `url` and `encrypted_url` fields.
+ # Pending a background migration to encrypt all fields, we should just clear
+ # the unencrypted value whenever the new value is set.
+ alias_method :'attr_encrypted_url=', :'url='
+ def url=(value)
+ self.attr_encrypted_url = value
+
+ write_attribute(:url, nil)
+ end
end
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
index 7d8ce0bbd05..11289887e00 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -64,10 +64,10 @@ class InstanceConfiguration
end
def ssh_algorithm_md5(ssh_file_content)
- OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':')
+ Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint
end
def ssh_algorithm_sha256(ssh_file_content)
- OpenSSL::Digest::SHA256.hexdigest(ssh_file_content)
+ Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint('SHA256')
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d0cd7461daa..4ace5d3ab97 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -170,22 +170,6 @@ class Issue < ActiveRecord::Base
"#{project.to_reference(from, full: full)}#{reference}"
end
- # All branches containing the current issue's ID, except for
- # those with a merge request open referencing the current issue.
- def related_branches(current_user)
- branches_with_iid = project.repository.branch_names.select do |branch|
- branch =~ /\A#{iid}-(?!\d+-stable)/i
- end
-
- branches_with_merge_request =
- Issues::ReferencedMergeRequestsService
- .new(project, current_user)
- .referenced_merge_requests(self)
- .map(&:source_branch)
-
- branches_with_iid - branches_with_merge_request
- end
-
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
@@ -278,9 +262,11 @@ class Issue < ActiveRecord::Base
true
end
+ # rubocop: disable CodeReuse/ServiceClass
def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache
end
+ # rubocop: enable CodeReuse/ServiceClass
private
diff --git a/app/models/key.rb b/app/models/key.rb
index 3bb0d2f6f9c..bdb83e12793 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -55,9 +55,11 @@ class Key < ActiveRecord::Base
"key-#{id}"
end
+ # rubocop: disable CodeReuse/ServiceClass
def update_last_used_at
Keys::LastUsedService.new(self).execute
end
+ # rubocop: enable CodeReuse/ServiceClass
def add_to_shell
GitlabShellWorker.perform_async(
@@ -67,9 +69,11 @@ class Key < ActiveRecord::Base
)
end
+ # rubocop: disable CodeReuse/ServiceClass
def post_create_hook
SystemHooksService.new.execute_hooks_for(self, :create)
end
+ # rubocop: enable CodeReuse/ServiceClass
def remove_from_shell
GitlabShellWorker.perform_async(
@@ -79,15 +83,19 @@ class Key < ActiveRecord::Base
)
end
+ # rubocop: disable CodeReuse/ServiceClass
def refresh_user_cache
return unless user
Users::KeysCountService.new(user).refresh_cache
end
+ # rubocop: enable CodeReuse/ServiceClass
+ # rubocop: disable CodeReuse/ServiceClass
def post_destroy_hook
SystemHooksService.new.execute_hooks_for(self, :destroy)
end
+ # rubocop: enable CodeReuse/ServiceClass
def public_key
@public_key ||= Gitlab::SSHPublicKey.new(key)
diff --git a/app/models/label.rb b/app/models/label.rb
index 96c1515b41a..43b49445765 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -5,6 +5,9 @@ class Label < ActiveRecord::Base
include Referable
include Subscribable
include Gitlab::SQL::Pattern
+ include OptionallySearch
+ include Sortable
+ include FromUnion
# Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned.
@@ -40,6 +43,9 @@ class Label < ActiveRecord::Base
scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) }
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
+ scope :order_name_asc, -> { reorder(title: :asc) }
+ scope :order_name_desc, -> { reorder(title: :desc) }
+ scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) }
def self.prioritized(project)
joins(:priorities)
@@ -69,6 +75,14 @@ class Label < ActiveRecord::Base
joins(label_priorities)
end
+ def self.optionally_subscribed_by(user_id)
+ if user_id
+ subscribed_by(user_id)
+ else
+ all
+ end
+ end
+
alias_attribute :name, :title
def self.reference_prefix
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index 779657b25d5..1d93a55e8e9 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -3,7 +3,7 @@
class LabelLink < ActiveRecord::Base
include Importable
- belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :target, polymorphic: true, inverse_of: :label_links # rubocop:disable Cop/PolymorphicAssociations
belongs_to :label
validates :target, presence: true, unless: :importing?
diff --git a/app/models/label_note.rb b/app/models/label_note.rb
new file mode 100644
index 00000000000..680952cf421
--- /dev/null
+++ b/app/models/label_note.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+class LabelNote < Note
+ attr_accessor :resource_parent
+ attr_reader :events
+
+ def self.from_events(events, resource: nil, resource_parent: nil)
+ resource ||= events.first.issuable
+
+ attrs = {
+ system: true,
+ author: events.first.user,
+ created_at: events.first.created_at,
+ discussion_id: events.first.discussion_id,
+ noteable: resource,
+ system_note_metadata: SystemNoteMetadata.new(action: 'label'),
+ events: events,
+ resource_parent: resource_parent
+ }
+
+ if resource_parent.is_a?(Project)
+ attrs[:project_id] = resource_parent.id
+ end
+
+ LabelNote.new(attrs)
+ end
+
+ def events=(events)
+ @events = events
+
+ update_outdated_markdown
+ end
+
+ def cached_html_up_to_date?(markdown_field)
+ true
+ end
+
+ def note
+ @note ||= note_text
+ end
+
+ def note_html
+ @note_html ||= "<p dir=\"auto\">#{note_text(html: true)}</p>"
+ end
+
+ def project
+ resource_parent if resource_parent.is_a?(Project)
+ end
+
+ def group
+ resource_parent if resource_parent.is_a?(Group)
+ end
+
+ private
+
+ def update_outdated_markdown
+ events.each do |event|
+ if event.outdated_markdown?
+ event.refresh_invalid_reference
+ end
+ end
+ end
+
+ def note_text(html: false)
+ added = labels_str('added', label_refs_by_action('add', html))
+ removed = labels_str('removed', label_refs_by_action('remove', html))
+
+ [added, removed].compact.join(' and ')
+ end
+
+ # returns string containing added/removed labels including
+ # count of deleted labels:
+ #
+ # added ~1 ~2 + 1 deleted label
+ # added 3 deleted labels
+ # added ~1 ~2 labels
+ def labels_str(prefix, label_refs)
+ existing_refs = label_refs.select { |ref| ref.present? }.sort
+ refs_str = existing_refs.empty? ? nil : existing_refs.join(' ')
+
+ deleted = label_refs.count - existing_refs.count
+ deleted_str = deleted == 0 ? nil : "#{deleted} deleted"
+
+ return nil unless refs_str || deleted_str
+
+ label_list_str = [refs_str, deleted_str].compact.join(' + ')
+ suffix = 'label'.pluralize(deleted > 0 ? deleted : existing_refs.count)
+
+ "#{prefix} #{label_list_str} #{suffix}"
+ end
+
+ def label_refs_by_action(action, html)
+ field = html ? :reference_html : :reference
+
+ events.select { |e| e.action == action }.map(&field)
+ end
+end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 20f9b18e4ca..00dec6bb92b 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -20,11 +20,7 @@ class LegacyDiffNote < Note
end
def project_repository
- if RequestStore.active?
- RequestStore.fetch("project:#{project_id}:repository") { self.project.repository }
- else
- self.project.repository
- end
+ Gitlab::SafeRequestStore.fetch("project:#{project_id}:repository") { self.project.repository }
end
def diff_file_hash
diff --git a/app/models/license_template.rb b/app/models/license_template.rb
index 0ad75b27827..73e403f98b4 100644
--- a/app/models/license_template.rb
+++ b/app/models/license_template.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LicenseTemplate
PROJECT_TEMPLATE_REGEX =
%r{[\<\{\[]
@@ -10,12 +12,10 @@ class LicenseTemplate
(fullname|name\sof\s(author|copyright\sowner))
[\>\}\]]}xi.freeze
- attr_reader :id, :name, :category, :nickname, :url, :meta
-
- alias_method :key, :id
+ attr_reader :key, :name, :category, :nickname, :url, :meta
- def initialize(id:, name:, category:, content:, nickname: nil, url: nil, meta: {})
- @id = id
+ def initialize(key:, name:, category:, content:, nickname: nil, url: nil, meta: {})
+ @key = key
@name = name
@category = category
@content = content
diff --git a/app/models/member.rb b/app/models/member.rb
index d9b4e8d2ac6..0696ea46c8b 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -145,6 +145,7 @@ class Member < ActiveRecord::Base
end
def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
+ # rubocop: disable CodeReuse/ServiceClass
# `user` can be either a User object, User ID or an email to be invited
member = retrieve_member(source, user, existing_members)
access_level = retrieve_access_level(access_level)
@@ -171,6 +172,7 @@ class Member < ActiveRecord::Base
end
member
+ # rubocop: enable CodeReuse/ServiceClass
end
def add_users(source, users, access_level, current_user: nil, expires_at: nil)
@@ -339,12 +341,14 @@ class Member < ActiveRecord::Base
@notification_setting ||= user&.notification_settings_for(source)
end
+ # rubocop: disable CodeReuse/ServiceClass
def notifiable?(type, opts = {})
# always notify when there isn't a user yet
return true if user.blank?
NotificationRecipientService.notifiable?(user, type, notifiable_options.merge(opts))
end
+ # rubocop: enable CodeReuse/ServiceClass
private
@@ -374,6 +378,7 @@ class Member < ActiveRecord::Base
# in a transaction. Doing so can lead to the job running before the
# transaction has been committed, resulting in the job either throwing an
# error or not doing any meaningful work.
+ # rubocop: disable CodeReuse/ServiceClass
def refresh_member_authorized_projects
# If user/source is being destroyed, project access are going to be
# destroyed eventually because of DB foreign keys, so we shouldn't bother
@@ -382,6 +387,7 @@ class Member < ActiveRecord::Base
UserProjectAccessChangedService.new(user_id).execute
end
+ # rubocop: enable CodeReuse/ServiceClass
def after_accept_invite
post_create_hook
@@ -395,13 +401,17 @@ class Member < ActiveRecord::Base
post_create_hook
end
+ # rubocop: disable CodeReuse/ServiceClass
def system_hook_service
SystemHooksService.new
end
+ # rubocop: enable CodeReuse/ServiceClass
+ # rubocop: disable CodeReuse/ServiceClass
def notification_service
NotificationService.new
end
+ # rubocop: enable CodeReuse/ServiceClass
def notifiable_options
{}
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 0154fe5aeba..537f2a3a231 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -138,7 +138,9 @@ class ProjectMember < Member
super
end
+ # rubocop: disable CodeReuse/ServiceClass
def event_service
EventCreateService.new
end
+ # rubocop: enable CodeReuse/ServiceClass
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 396647a14ae..6559f94a696 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -6,6 +6,7 @@ class MergeRequest < ActiveRecord::Base
include Issuable
include Noteable
include Referable
+ include Presentable
include IgnorableColumn
include TimeTrackable
include ManualInverseAssociation
@@ -14,6 +15,7 @@ class MergeRequest < ActiveRecord::Base
include Gitlab::Utils::StrongMemoize
include LabelEventable
include ReactiveCaching
+ include FromUnion
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes
@@ -137,12 +139,14 @@ class MergeRequest < ActiveRecord::Base
Gitlab::Timeless.timeless(merge_request, &block)
end
+ # rubocop: disable CodeReuse/ServiceClass
after_transition unchecked: :cannot_be_merged do |merge_request, transition|
if merge_request.notify_conflict?
NotificationService.new.merge_request_unmergeable(merge_request)
TodoService.new.merge_request_became_unmergeable(merge_request)
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def check_state?(merge_status)
[:unchecked, :cannot_be_merged_recheck].include?(merge_status.to_sym)
@@ -235,11 +239,10 @@ class MergeRequest < ActiveRecord::Base
def self.in_projects(relation)
# unscoping unnecessary conditions that'll be applied
# when executing `where("merge_requests.id IN (#{union.to_sql})")`
- source = unscoped.where(source_project_id: relation).select(:id)
- target = unscoped.where(target_project_id: relation).select(:id)
- union = Gitlab::SQL::Union.new([source, target])
+ source = unscoped.where(source_project_id: relation)
+ target = unscoped.where(target_project_id: relation)
- where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ from_union([source, target])
end
# This is used after project import, to reset the IDs to the correct
@@ -258,7 +261,7 @@ class MergeRequest < ActiveRecord::Base
end
end
- WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
+ WIP_REGEX = /\A*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def self.work_in_progress?(title)
!!(title =~ WIP_REGEX)
@@ -623,11 +626,13 @@ class MergeRequest < ActiveRecord::Base
end
end
+ # rubocop: disable CodeReuse/ServiceClass
def reload_diff(current_user = nil)
return unless open?
MergeRequests::ReloadDiffsService.new(self, current_user).execute
end
+ # rubocop: enable CodeReuse/ServiceClass
def check_if_can_be_merged
return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
@@ -736,11 +741,8 @@ class MergeRequest < ActiveRecord::Base
# compared to using OR statements. We're using UNION ALL since the queries
# used won't produce any duplicates (e.g. a note for a commit can't also be
# a note for an MR).
- union = Gitlab::SQL::Union
- .new([notes, commit_notes], remove_duplicates: false)
- .to_sql
-
- Note.from("(#{union}) #{Note.table_name}")
+ Note
+ .from_union([notes, commit_notes], remove_duplicates: false)
.includes(:noteable)
end
@@ -1036,6 +1038,7 @@ class MergeRequest < ActiveRecord::Base
actual_head_pipeline&.has_test_reports?
end
+ # rubocop: disable CodeReuse/ServiceClass
def compare_test_reports
unless has_test_reports?
return { status: :error, status_reason: 'This merge request does not have test reports' }
@@ -1050,7 +1053,9 @@ class MergeRequest < ActiveRecord::Base
data
end || { status: :parsing }
end
+ # rubocop: enable CodeReuse/ServiceClass
+ # rubocop: disable CodeReuse/ServiceClass
def calculate_reactive_cache(identifier, *args)
case identifier.to_sym
when :compare_test_results
@@ -1060,6 +1065,7 @@ class MergeRequest < ActiveRecord::Base
raise NotImplementedError, "Unknown identifier: #{identifier}"
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def all_commits
# MySQL doesn't support LIMIT in a subquery.
@@ -1125,6 +1131,7 @@ class MergeRequest < ActiveRecord::Base
diff_refs && diff_refs.complete?
end
+ # rubocop: disable CodeReuse/ServiceClass
def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
return unless has_complete_diff_refs?
return if new_diff_refs == old_diff_refs
@@ -1154,6 +1161,7 @@ class MergeRequest < ActiveRecord::Base
.execute(self)
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def keep_around_commit
project.repository.keep_around(self.merge_commit_sha)
@@ -1189,9 +1197,11 @@ class MergeRequest < ActiveRecord::Base
true
end
+ # rubocop: disable CodeReuse/ServiceClass
def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
+ # rubocop: enable CodeReuse/ServiceClass
def first_contribution?
return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index bbe4f6f7969..02c6b650f33 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -219,12 +219,14 @@ class MergeRequestDiff < ActiveRecord::Base
self.id == merge_request.latest_merge_request_diff_id
end
+ # rubocop: disable CodeReuse/ServiceClass
def compare_with(sha)
# When compare merge request versions we want diff A..B instead of A...B
# so we handle cases when user does squash and rebase of the commits between versions.
# For this reason we set straight to true by default.
CompareService.new(project, head_commit_sha).execute(project, sha, straight: true)
end
+ # rubocop: enable CodeReuse/ServiceClass
private
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index cb1def1b422..892a680f221 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -46,6 +46,9 @@ class Milestone < ActiveRecord::Base
where(conditions.reduce(:or))
end
+ scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
+ scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
+
validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group
@@ -149,7 +152,7 @@ class Milestone < ActiveRecord::Base
sorted =
case method.to_s
when 'due_date_asc'
- reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC'))
+ reorder_by_due_date_asc
when 'due_date_desc'
reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC'))
when 'name_asc'
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 0deb44d7916..599bedde27d 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -11,6 +11,7 @@ class Namespace < ActiveRecord::Base
include Gitlab::SQL::Pattern
include IgnorableColumn
include FeatureGate
+ include FromUnion
ignore_column :deleted_at
@@ -134,6 +135,10 @@ class Namespace < ActiveRecord::Base
all_projects.any?(&:has_container_registry_tags?)
end
+ def first_project_with_container_registry_tags
+ all_projects.find(&:has_container_registry_tags?)
+ end
+
def send_update_instructions
projects.each do |project|
project.send_move_instructions("#{full_path_was}/#{project.path}")
@@ -147,8 +152,8 @@ class Namespace < ActiveRecord::Base
def find_fork_of(project)
return nil unless project.fork_network
- if RequestStore.active?
- forks_in_namespace = RequestStore.fetch("namespaces:#{id}:forked_projects") do
+ if Gitlab::SafeRequestStore.active?
+ forks_in_namespace = Gitlab::SafeRequestStore.fetch("namespaces:#{id}:forked_projects") do
Hash.new do |found_forks, project|
found_forks[project] = project.fork_network.find_forks_in(projects).first
end
@@ -253,18 +258,6 @@ class Namespace < ActiveRecord::Base
end
end
- # Exports belonging to projects with legacy storage are placed in a common
- # subdirectory of the namespace, so a simple `rm -rf` is sufficient to remove
- # them.
- #
- # Exports of projects using hashed storage are placed in a location defined
- # only by the project ID, so each must be removed individually.
- def remove_exports!
- remove_legacy_exports!
-
- all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
- end
-
def refresh_project_authorizations
owner.refresh_authorized_projects
end
diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb
index 6c5a4c56377..1b2369aab18 100644
--- a/app/models/network/commit.rb
+++ b/app/models/network/commit.rb
@@ -18,7 +18,7 @@ module Network
end
def space
- if @spaces.size > 0
+ if @spaces.present?
@spaces.first
else
0
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 1431dfefc55..6da3bb7bfb7 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -81,7 +81,7 @@ module Network
skip = 0
while offset == -1
tmp_commits = find_commits(skip)
- if tmp_commits.size > 0
+ if tmp_commits.present?
index = tmp_commits.index do |c|
c.id == @commit.id
end
@@ -218,7 +218,7 @@ module Network
def get_space_base(leaves)
space_base = 1
parents = leaves.last.parents(@map)
- if parents.size > 0
+ if parents.present?
if parents.first.space > 0
space_base = parents.first.space
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 2e343b8f9f8..1b595ef60b4 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -17,6 +17,7 @@ class Note < ActiveRecord::Base
include Editable
include Gitlab::SQL::Pattern
include ThrottledTouch
+ include FromUnion
module SpecialRole
FIRST_TIME_CONTRIBUTOR = :first_time_contributor
@@ -37,10 +38,12 @@ class Note < ActiveRecord::Base
alias_attribute :last_edited_at, :updated_at
alias_attribute :last_edited_by, :updated_by
- # Attribute containing rendered and redacted Markdown as generated by
- # Banzai::ObjectRenderer.
+ # Number of user visible references as generated by Banzai::ObjectRenderer
attr_accessor :redacted_note_html
+ # Total of all references as generated by Banzai::ObjectRenderer
+ attr_accessor :total_reference_count
+
# An Array containing the number of visible references as generated by
# Banzai::ObjectRenderer
attr_accessor :user_visible_reference_count
@@ -181,6 +184,7 @@ class Note < ActiveRecord::Base
end
end
+ # rubocop: disable CodeReuse/ServiceClass
def cross_reference?
return unless system?
@@ -190,6 +194,7 @@ class Note < ActiveRecord::Base
SystemNoteService.cross_reference?(note)
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def diff_note?
false
@@ -285,15 +290,7 @@ class Note < ActiveRecord::Base
end
def cross_reference_not_visible_for?(user)
- cross_reference? && !has_referenced_mentionables?(user)
- end
-
- def has_referenced_mentionables?(user)
- if user_visible_reference_count.present?
- user_visible_reference_count > 0
- else
- referenced_mentionables(user).any?
- end
+ cross_reference? && !all_referenced_mentionables_allowed?(user)
end
def award_emoji?
@@ -389,18 +386,7 @@ class Note < ActiveRecord::Base
end
def expire_etag_cache
- return unless noteable&.discussions_rendered_on_frontend?
- return unless noteable&.etag_caching_enabled?
-
- Gitlab::EtagCaching::Store.new.touch(etag_key)
- end
-
- def etag_key
- Gitlab::Routing.url_helpers.project_noteable_notes_path(
- project,
- target_type: noteable_type.underscore,
- target_id: noteable_id
- )
+ noteable&.expire_note_etag_cache
end
def touch(*args)
@@ -474,9 +460,18 @@ class Note < ActiveRecord::Base
self.discussion_id ||= discussion_class.discussion_id(self)
end
+ def all_referenced_mentionables_allowed?(user)
+ if user_visible_reference_count.present? && total_reference_count.present?
+ # if they are not equal, then there are private/confidential references as well
+ user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count
+ else
+ referenced_mentionables(user).any?
+ end
+ end
+
def force_cross_reference_regex_check?
return unless system?
- SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.include?(system_note_metadata&.action)
+ system_note_metadata&.cross_reference_types&.include?(system_note_metadata&.action)
end
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 7739a3894d3..7a33ade826b 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -140,9 +140,11 @@ class PagesDomain < ActiveRecord::Base
self.verification_code = SecureRandom.hex(16)
end
+ # rubocop: disable CodeReuse/ServiceClass
def update_daemon
::Projects::UpdatePagesConfigurationService.new(project).execute
end
+ # rubocop: enable CodeReuse/ServiceClass
def pages_config_changed?
project_id_changed? ||
diff --git a/app/models/project.rb b/app/models/project.rb
index 67593c9b2fe..0cdd876dc20 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -29,6 +29,7 @@ class Project < ActiveRecord::Base
include BatchDestroyDependentAssociations
include FeatureGate
include OptionallySearch
+ include FromUnion
extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
@@ -54,8 +55,8 @@ class Project < ActiveRecord::Base
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
- :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
- allow_nil: true
+ :merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?,
+ to: :project_feature, allow_nil: true
delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
@@ -85,7 +86,7 @@ class Project < ActiveRecord::Base
after_create :create_project_feature, unless: :project_feature
after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) }
- before_destroy :untrack_site_statistics
+ before_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) }
after_create :create_ci_cd_settings,
unless: :ci_cd_settings,
@@ -110,7 +111,7 @@ class Project < ActiveRecord::Base
after_create :ensure_storage_path_exists
after_save :ensure_storage_path_exists, if: :namespace_id_changed?
- acts_as_taggable
+ acts_as_ordered_taggable
attr_accessor :old_path_with_namespace
attr_accessor :template_name
@@ -232,6 +233,8 @@ class Project < ActiveRecord::Base
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
+ has_many :prometheus_metrics
+
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
# here.
@@ -328,7 +331,7 @@ class Project < ActiveRecord::Base
# last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push
scope :sorted_by_activity, -> { reorder("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC") }
- scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
+ scope :sorted_by_stars, -> { reorder(star_count: :desc) }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
@@ -353,7 +356,7 @@ class Project < ActiveRecord::Base
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
access_level_attribute = ProjectFeature.access_level_attribute(feature)
- with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] })
+ with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] })
}
# Picks a feature where the level is exactly that given.
@@ -415,15 +418,15 @@ class Project < ActiveRecord::Base
end
end
- # project features may be "disabled", "internal" or "enabled". If "internal",
+ # project features may be "disabled", "internal", "enabled" or "public". If "internal",
# they are only available to team members. This scope returns projects where
- # the feature is either enabled, or internal with permission for the user.
+ # the feature is either public, enabled, or internal with permission for the user.
#
# This method uses an optimised version of `with_feature_access_level` for
# logged in users to more efficiently get private projects with the given
# feature.
def self.with_feature_available_for_user(feature, user)
- visible = [nil, ProjectFeature::ENABLED]
+ visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
if user&.admin?
with_feature_enabled(feature)
@@ -478,6 +481,8 @@ class Project < ActiveRecord::Base
reorder(last_activity_at: :desc)
when 'latest_activity_asc'
reorder(last_activity_at: :asc)
+ when 'stars_desc'
+ sorted_by_stars
else
order_by(method)
end
@@ -567,7 +572,6 @@ class Project < ActiveRecord::Base
end
def cleanup
- @repository&.cleanup
@repository = nil
end
@@ -1078,31 +1082,13 @@ class Project < ActiveRecord::Base
end
def find_or_initialize_services(exceptions: [])
- services_templates = Service.where(template: true)
-
available_services_names = Service.available_services_names - exceptions
available_services = available_services_names.map do |service_name|
- service = find_service(services, service_name)
-
- if service
- service
- else
- # We should check if template for the service exists
- template = find_service(services_templates, service_name)
-
- if template.nil?
- # If no template, we should create an instance. Ex `build_gitlab_ci_service`
- public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend
- else
- Service.build_from_template(id, template)
- end
- end
+ find_or_initialize_service(service_name)
end
- available_services.reject do |service|
- disabled_services.include?(service.to_param)
- end
+ available_services.compact
end
def disabled_services
@@ -1110,15 +1096,30 @@ class Project < ActiveRecord::Base
end
def find_or_initialize_service(name)
- find_or_initialize_services.find { |service| service.to_param == name }
+ return if disabled_services.include?(name)
+
+ service = find_service(services, name)
+ return service if service
+
+ # We should check if template for the service exists
+ template = find_service(services_templates, name)
+
+ if template
+ Service.build_from_template(id, template)
+ else
+ # If no template, we should create an instance. Ex `build_gitlab_ci_service`
+ public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
+ end
end
+ # rubocop: disable CodeReuse/ServiceClass
def create_labels
Label.templates.each do |label|
params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def find_service(list, name)
list.find { |service| service.to_param == name }
@@ -1166,6 +1167,7 @@ class Project < ActiveRecord::Base
end
end
+ # rubocop: disable CodeReuse/ServiceClass
def send_move_instructions(old_path_with_namespace)
# New project path needs to be committed to the DB or notification will
# retrieve stale information
@@ -1173,6 +1175,7 @@ class Project < ActiveRecord::Base
NotificationService.new.project_was_moved(self, old_path_with_namespace)
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def owner
if group
@@ -1182,15 +1185,16 @@ class Project < ActiveRecord::Base
end
end
+ # rubocop: disable CodeReuse/ServiceClass
def execute_hooks(data, hooks_scope = :push_hooks)
run_after_commit_or_now do
- hooks.hooks_for(hooks_scope).each do |hook|
+ hooks.hooks_for(hooks_scope).select_active(hooks_scope, data).each do |hook|
hook.async_execute(data, hooks_scope.to_s)
end
-
SystemHooksService.new.execute_hooks(data, hooks_scope)
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def execute_services(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
@@ -1356,6 +1360,18 @@ class Project < ActiveRecord::Base
end
end
+ # Filters `users` to return only authorized users of the project
+ def members_among(users)
+ if users.is_a?(ActiveRecord::Relation) && !users.loaded?
+ authorized_users.merge(users)
+ else
+ return [] if users.empty?
+
+ user_ids = authorized_users.where(users: { id: users.map(&:id) }).pluck(:id)
+ users.select { |user| user_ids.include?(user.id) }
+ end
+ end
+
def default_branch
@default_branch ||= repository.root_ref if repository.exists?
end
@@ -1487,8 +1503,7 @@ class Project < ActiveRecord::Base
end
def all_runners
- union = Gitlab::SQL::Union.new([runners, group_runners, shared_runners])
- Ci::Runner.from("(#{union.to_sql}) ci_runners")
+ Ci::Runner.from_union([runners, group_runners, shared_runners])
end
def active_runners
@@ -1505,13 +1520,17 @@ class Project < ActiveRecord::Base
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
+ # rubocop: disable CodeReuse/ServiceClass
def open_issues_count(current_user = nil)
Projects::OpenIssuesCountService.new(self, current_user).count
end
+ # rubocop: enable CodeReuse/ServiceClass
+ # rubocop: disable CodeReuse/ServiceClass
def open_merge_requests_count
Projects::OpenMergeRequestsCountService.new(self).count
end
+ # rubocop: enable CodeReuse/ServiceClass
def visibility_level_allowed_as_fork?(level = self.visibility_level)
return true unless forked?
@@ -1592,6 +1611,7 @@ class Project < ActiveRecord::Base
end
# TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal?
+ # rubocop: disable CodeReuse/ServiceClass
def remove_pages
# Projects with a missing namespace cannot have their pages removed
return unless namespace
@@ -1607,6 +1627,7 @@ class Project < ActiveRecord::Base
PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path)
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def rename_repo
path_before = previous_changes['path'].first
@@ -1667,6 +1688,7 @@ class Project < ActiveRecord::Base
end
end
+ # rubocop: disable CodeReuse/ServiceClass
def after_create_default_branch
return unless default_branch
@@ -1687,6 +1709,7 @@ class Project < ActiveRecord::Base
ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true)
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def remove_import_jid
return unless import_jid
@@ -1734,16 +1757,12 @@ class Project < ActiveRecord::Base
import_export_shared.archive_path
end
- def export_project_path
- Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) }
- end
-
def export_status
if export_in_progress?
:started
elsif after_export_in_progress?
:after_export_action
- elsif export_project_path || export_project_object_exists?
+ elsif export_file_exists?
:finished
else
:none
@@ -1758,21 +1777,19 @@ class Project < ActiveRecord::Base
import_export_shared.after_export_in_progress?
end
- def remove_exports(path = export_path)
- if path.present?
- FileUtils.rm_rf(path)
- elsif export_project_object_exists?
- import_export_upload.remove_export_file!
- import_export_upload.save
- end
+ def remove_exports
+ return unless export_file_exists?
+
+ import_export_upload.remove_export_file!
+ import_export_upload.save
end
- def remove_exported_project_file
- remove_exports(export_project_path)
+ def export_file_exists?
+ export_file&.file
end
- def export_project_object_exists?
- Gitlab::ImportExport.object_storage? && import_export_upload&.export_file&.file
+ def export_file
+ import_export_upload&.export_file
end
def full_path_slug
@@ -1923,9 +1940,11 @@ class Project < ActiveRecord::Base
# @deprecated cannot remove yet because it has an index with its name in elasticsearch
alias_method :path_with_namespace, :full_path
+ # rubocop: disable CodeReuse/ServiceClass
def forks_count
Projects::ForksCountService.new(self).count
end
+ # rubocop: enable CodeReuse/ServiceClass
def legacy_storage?
[nil, 0].include?(self.storage_version)
@@ -2012,12 +2031,10 @@ class Project < ActiveRecord::Base
def badges
return project_badges unless group
- group_badges_rel = GroupBadge.where(group: group.self_and_ancestors)
-
- union = Gitlab::SQL::Union.new([project_badges.select(:id),
- group_badges_rel.select(:id)])
-
- Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ Badge.from_union([
+ project_badges,
+ GroupBadge.where(group: group.self_and_ancestors)
+ ])
end
def merge_requests_allowing_push_to_user(user)
@@ -2070,6 +2087,7 @@ class Project < ActiveRecord::Base
private
+ # rubocop: disable CodeReuse/ServiceClass
def rename_or_migrate_repository!
if Gitlab::CurrentSettings.hashed_storage_enabled? &&
storage_upgradable? &&
@@ -2079,6 +2097,7 @@ class Project < ActiveRecord::Base
storage.rename_repo
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def storage_upgradable?
storage_version != LATEST_STORAGE_VERSION
@@ -2098,11 +2117,7 @@ class Project < ActiveRecord::Base
Gitlab::PagesTransfer.new.rename_project(path_before, self.path, namespace.full_path)
end
- def untrack_site_statistics
- SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
- self.project_feature.untrack_statistics_for_deletion!
- end
-
+ # rubocop: disable CodeReuse/ServiceClass
def execute_rename_repository_hooks!(full_path_before)
# When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions.
@@ -2113,6 +2128,7 @@ class Project < ActiveRecord::Base
reload_repository!
end
+ # rubocop: enable CodeReuse/ServiceClass
def storage
@storage ||=
@@ -2252,12 +2268,12 @@ class Project < ActiveRecord::Base
end
end
- if RequestStore.active?
- RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
- check_access.call
- end
- else
+ Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
check_access.call
end
end
+
+ def services_templates
+ @services_templates ||= Service.where(template: true)
+ end
end
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 746bb4584c9..2c590008db2 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ProjectAuthorization < ActiveRecord::Base
+ include FromUnion
+
belongs_to :user
belongs_to :project
@@ -8,9 +10,9 @@ class ProjectAuthorization < ActiveRecord::Base
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
- def self.select_from_union(union)
- select(['project_id', 'MAX(access_level) AS access_level'])
- .from("(#{union.to_sql}) #{ProjectAuthorization.table_name}")
+ def self.select_from_union(relations)
+ from_union(relations)
+ .select(['project_id', 'MAX(access_level) AS access_level'])
.group(:project_id)
end
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index dc6736dd9cd..2253ad7b543 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -5,7 +5,8 @@ class ProjectAutoDevops < ActiveRecord::Base
enum deploy_strategy: {
continuous: 0,
- manual: 1
+ manual: 1,
+ timed_incremental: 2
}
scope :enabled, -> { where(enabled: true) }
@@ -30,10 +31,7 @@ class ProjectAutoDevops < ActiveRecord::Base
value: domain.presence || instance_domain)
end
- if manual?
- variables.append(key: 'STAGING_ENABLED', value: '1')
- variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1')
- end
+ variables.concat(deployment_strategy_default_variables)
end
end
@@ -51,4 +49,16 @@ class ProjectAutoDevops < ActiveRecord::Base
!project.public? &&
!project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present?
end
+
+ def deployment_strategy_default_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ if manual?
+ variables.append(key: 'STAGING_ENABLED', value: '1')
+ variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1') # deprecated
+ variables.append(key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual')
+ elsif timed_incremental?
+ variables.append(key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed')
+ end
+ end
+ end
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index d74cb2506ba..39f2b8fe0de 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -13,15 +13,16 @@ class ProjectFeature < ActiveRecord::Base
# Disabled: not enabled for anyone
# Private: enabled only for team members
# Enabled: enabled for everyone able to access the project
+ # Public: enabled for everyone (only allowed for pages)
#
# Permission levels
DISABLED = 0
PRIVATE = 10
ENABLED = 20
+ PUBLIC = 30
- FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze
- STATISTICS_ATTRIBUTE = 'wikis_count'.freeze
+ FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze
class << self
def access_level_attribute(feature)
@@ -47,6 +48,7 @@ class ProjectFeature < ActiveRecord::Base
validates :project, presence: true
validate :repository_children_level
+ validate :allowed_access_levels
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_access_level, value: ENABLED, allows_nil: false
@@ -55,10 +57,10 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
- after_create ->(model) { SiteStatistic.track(STATISTICS_ATTRIBUTE) if model.wiki_enabled? }
- after_update :update_site_statistics
-
def feature_available?(feature, user)
+ # This feature might not be behind a feature flag at all, so default to true
+ return false unless ::Feature.enabled?(feature, user, default_enabled: true)
+
get_permission(user, access_level(feature))
end
@@ -82,30 +84,18 @@ class ProjectFeature < ActiveRecord::Base
issues_access_level > DISABLED
end
- # This is a workaround for the removal hooks not been triggered when removing a Project.
- #
- # ProjectFeature is removed using database cascade index rule.
- # This method is called by Project model when deletion starts.
- def untrack_statistics_for_deletion!
- return unless wiki_enabled?
-
- SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
+ def pages_enabled?
+ pages_access_level > DISABLED
end
- private
-
- def update_site_statistics
- return unless wiki_access_level_changed?
+ def public_pages?
+ return true unless Gitlab.config.pages.access_control
- if self.wiki_access_level_was == DISABLED
- # possible new states are PRIVATE / ENABLED, both should be tracked
- SiteStatistic.track(STATISTICS_ATTRIBUTE)
- elsif self.wiki_access_level == DISABLED
- # old state was either PRIVATE / ENABLED, only untrack if new state is DISABLED
- SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
- end
+ pages_access_level == PUBLIC || pages_access_level == ENABLED && project.public?
end
+ private
+
# Validates builds and merge requests access level
# which cannot be higher than repository access level
def repository_children_level
@@ -118,6 +108,17 @@ class ProjectFeature < ActiveRecord::Base
%i(merge_requests_access_level builds_access_level).each(&validator)
end
+ # Validates access level for other than pages cannot be PUBLIC
+ def allowed_access_levels
+ validator = lambda do |field|
+ level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend
+ not_allowed = level > ProjectFeature::ENABLED
+ self.errors.add(field, "cannot have public visibility level") if not_allowed
+ end
+
+ (FEATURES - %i(pages)).each {|f| validator.call("#{f}_access_level")}
+ end
+
def get_permission(user, level)
case level
when DISABLED
@@ -126,6 +127,8 @@ class ProjectFeature < ActiveRecord::Base
user && (project.team.member?(user) || user.full_private_access?)
when ENABLED
true
+ when PUBLIC
+ true
else
true
end
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index 89ed09af96a..d59cb43dea4 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -48,9 +48,11 @@ class ProjectImportState < ActiveRecord::Base
project.reset_cache_and_import_attrs
if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
+ # rubocop: disable CodeReuse/ServiceClass
state.run_after_commit do
Projects::AfterImportService.new(project).execute
end
+ # rubocop: enable CodeReuse/ServiceClass
end
end
end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 35c19049c04..cc5f1207653 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -65,7 +65,7 @@ http://app.asana.com/-/account_api'
# check the branch restriction is poplulated and branch is not included
branch = Gitlab::Git.ref_name(data[:ref])
branch_restriction = restrict_to_branch.to_s
- if branch_restriction.length > 0 && branch_restriction.index(branch).nil?
+ if branch_restriction.present? && branch_restriction.index(branch).nil?
return
end
@@ -101,7 +101,7 @@ http://app.asana.com/-/account_api'
task.update(completed: true)
end
rescue => e
- Rails.logger.error(e.message)
+ log_error(e.message)
next
end
end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index 58631e09538..6b7a35aaa75 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -26,7 +26,7 @@ module ChatMessage
def activity
{
- title: "Merge Request #{state} by #{user_combined_name}",
+ title: "Merge Request #{state_or_action_text} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: merge_request_link,
image: user_avatar
@@ -48,7 +48,7 @@ module ChatMessage
end
def merge_request_message
- "#{user_combined_name} #{state} #{merge_request_link} in #{project_link}: #{title}"
+ "#{user_combined_name} #{state_or_action_text} #{merge_request_link} in #{project_link}"
end
def merge_request_link
@@ -62,5 +62,10 @@ module ChatMessage
def merge_request_url
"#{project_url}/merge_requests/#{merge_request_iid}"
end
+
+ # overridden in EE
+ def state_or_action_text
+ state
+ end
end
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 66012f0da99..a69b7b4c4b6 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -149,7 +149,7 @@ class HipchatService < Service
context.merge!(options)
- html = Banzai.post_process(Banzai.render(text, context), context)
+ html = Banzai.render_and_post_process(text, context)
sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt])
sanitized_html.truncate(200, separator: ' ', omission: '...')
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index a783a314071..a15780c14f9 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -104,7 +104,7 @@ class IrkerService < Service
new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
uri = consider_uri(URI.parse(new_recipient))
rescue
- Rails.logger.error("Unable to create a valid URL from #{default_irc_uri} and #{recipient}")
+ log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient)
end
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index c7520d766a8..e1d342be188 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -88,7 +88,7 @@ class IssueTrackerService < Service
rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error
message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}"
end
- Rails.logger.info(message)
+ log_info(message)
result
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index cc98b3f5a41..ba7fcb0cf93 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -205,7 +205,7 @@ class JiraService < IssueTrackerService
begin
issue.transitions.build.save!(transition: { id: transition_id })
rescue => error
- Rails.logger.info "#{self.class.name} Issue Transition failed message ERROR: #{client_url} - #{error.message}"
+ log_error("Issue transition failed", error: error.message, client_url: client_url)
return false
end
end
@@ -257,9 +257,8 @@ class JiraService < IssueTrackerService
new_remote_link.save!(remote_link_props)
end
- result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}."
- Rails.logger.info(result_message)
- result_message
+ log_info("Successfully posted", client_url: client_url)
+ "SUCCESS: Successfully posted to http://jira.example.net."
end
end
@@ -317,7 +316,7 @@ class JiraService < IssueTrackerService
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e
@error = e.message
- Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{@error}"
+ log_error("Error sending message", client_url: client_url, error: @error)
nil
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index bda1f67b8ff..f119555f16b 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -96,10 +96,10 @@ class KubernetesService < DeploymentService
# Check we can connect to the Kubernetes API
def test(*args)
- kubeclient = build_kubeclient!
+ kubeclient = build_kube_client!
- kubeclient.discover
- { success: kubeclient.discovered, result: "Checked API discovery endpoint" }
+ kubeclient.core_client.discover
+ { success: kubeclient.core_client.discovered, result: "Checked API discovery endpoint" }
rescue => err
{ success: false, result: err }
end
@@ -144,7 +144,7 @@ class KubernetesService < DeploymentService
end
def kubeclient
- @kubeclient ||= build_kubeclient!
+ @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
end
def deprecated?
@@ -182,11 +182,12 @@ class KubernetesService < DeploymentService
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
- def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ def build_kube_client!(api_groups: ['api'], api_version: 'v1')
raise "Incomplete settings" unless api_url && actual_namespace && token
- ::Kubeclient::Client.new(
- join_api_url(api_path),
+ Gitlab::Kubernetes::KubeClient.new(
+ api_url,
+ api_groups,
api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
@@ -196,7 +197,7 @@ class KubernetesService < DeploymentService
# Returns a hash of all pods in the namespace
def read_pods
- kubeclient = build_kubeclient!
+ kubeclient = build_kube_client!
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue Kubeclient::HttpError => err
@@ -220,15 +221,6 @@ class KubernetesService < DeploymentService
{ bearer_token: token }
end
- def join_api_url(api_path)
- url = URI.parse(api_url)
- prefix = url.path.sub(%r{/+\z}, '')
-
- url.path = [prefix, api_path].join("/")
-
- url.to_s
- end
-
def terminal_auth
{
token: token,
diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb
index e3ab60adefd..bfabc6d262c 100644
--- a/app/models/project_services/slash_commands_service.rb
+++ b/app/models/project_services/slash_commands_service.rb
@@ -44,11 +44,15 @@ class SlashCommandsService < Service
private
+ # rubocop: disable CodeReuse/ServiceClass
def find_chat_user(params)
ChatNames::FindUserService.new(self, params).execute
end
+ # rubocop: enable CodeReuse/ServiceClass
+ # rubocop: disable CodeReuse/ServiceClass
def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute
end
+ # rubocop: enable CodeReuse/ServiceClass
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index f4b3421f04b..559e4f99294 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -80,7 +80,7 @@ class ProjectWiki
pages(limit: 1).empty?
end
- # Returns an Array of Gitlab WikiPage instances or an
+ # Returns an Array of GitLab WikiPage instances or an
# empty Array if this Wiki has no pages.
def pages(limit: 0)
wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) }
@@ -184,11 +184,12 @@ class ProjectWiki
def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title)
+ git_user = Gitlab::Git::User.from_gitlab(@user)
Gitlab::Git::Wiki::CommitDetails.new(@user.id,
- @user.username,
- @user.name,
- @user.email,
+ git_user.username,
+ git_user.name,
+ git_user.email,
commit_message)
end
diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb
new file mode 100644
index 00000000000..ce2db9cb44c
--- /dev/null
+++ b/app/models/prometheus_metric.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+class PrometheusMetric < ActiveRecord::Base
+ belongs_to :project, validate: true, inverse_of: :prometheus_metrics
+
+ enum group: {
+ # built-in groups
+ nginx_ingress: -1,
+ ha_proxy: -2,
+ aws_elb: -3,
+ nginx: -4,
+ kubernetes: -5,
+
+ # custom/user groups
+ business: 0,
+ response: 1,
+ system: 2
+ }
+
+ validates :title, presence: true
+ validates :query, presence: true
+ validates :group, presence: true
+ validates :y_label, presence: true
+ validates :unit, presence: true
+
+ validates :project, presence: true, unless: :common?
+ validates :project, absence: true, if: :common?
+
+ scope :common, -> { where(common: true) }
+
+ GROUP_TITLES = {
+ # built-in groups
+ nginx_ingress: _('Response metrics (NGINX Ingress)'),
+ ha_proxy: _('Response metrics (HA Proxy)'),
+ aws_elb: _('Response metrics (AWS ELB)'),
+ nginx: _('Response metrics (NGINX)'),
+ kubernetes: _('System metrics (Kubernetes)'),
+
+ # custom/user groups
+ business: _('Business metrics (Custom)'),
+ response: _('Response metrics (Custom)'),
+ system: _('System metrics (Custom)')
+ }.freeze
+
+ REQUIRED_METRICS = {
+ nginx_ingress: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
+ ha_proxy: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
+ aws_elb: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
+ nginx: %w(nginx_server_requests nginx_server_requestMsec),
+ kubernetes: %w(container_memory_usage_bytes container_cpu_usage_seconds_total)
+ }.freeze
+
+ def group_title
+ GROUP_TITLES[group.to_sym]
+ end
+
+ def required_metrics
+ REQUIRED_METRICS[group.to_sym].to_a.map(&:to_s)
+ end
+
+ def to_query_metric
+ Gitlab::Prometheus::Metric.new(id: id, title: title, required_metrics: required_metrics, weight: 0, y_label: y_label, queries: queries)
+ end
+
+ def queries
+ [
+ {
+ query_range: query,
+ unit: unit,
+ label: legend,
+ series: query_series
+ }.compact
+ ]
+ end
+
+ def query_series
+ case legend
+ when 'Status Code'
+ [{
+ label: 'status_code',
+ when: [
+ { value: '2xx', color: 'green' },
+ { value: '4xx', color: 'orange' },
+ { value: '5xx', color: 'red' }
+ ]
+ }]
+ end
+ end
+end
diff --git a/app/models/protected_ref_matcher.rb b/app/models/protected_ref_matcher.rb
deleted file mode 100644
index bfa9180ac93..00000000000
--- a/app/models/protected_ref_matcher.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-class ProtectedRefMatcher
- def initialize(protected_ref)
- @protected_ref = protected_ref
- end
-
- # Returns all protected refs that match the given ref name.
- # This checks all records from the scope built up so far, and does
- # _not_ return a relation.
- #
- # This method optionally takes in a list of `protected_refs` to search
- # through, to avoid calling out to the database.
- def self.matching(type, ref_name, protected_refs: nil)
- (protected_refs || type.all).select { |protected_ref| protected_ref.matches?(ref_name) }
- end
-
- # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`])
- # that match the current protected ref.
- def matching(refs)
- refs.select { |ref| @protected_ref.matches?(ref.name) }
- end
-
- # Checks if the protected ref matches the given ref name.
- def matches?(ref_name)
- return false if @protected_ref.name.blank?
-
- exact_match?(ref_name) || wildcard_match?(ref_name)
- end
-
- # Checks if this protected ref contains a wildcard
- def wildcard?
- @protected_ref.name && @protected_ref.name.include?('*')
- end
-
- protected
-
- def exact_match?(ref_name)
- @protected_ref.name == ref_name
- end
-
- def wildcard_match?(ref_name)
- return false unless wildcard?
-
- wildcard_regex === ref_name
- end
-
- def wildcard_regex
- @wildcard_regex ||= begin
- name = @protected_ref.name.gsub('*', 'STAR_DONT_ESCAPE')
- quoted_name = Regexp.quote(name)
- regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
- /\A#{regex_string}\z/
- end
- end
-end
diff --git a/app/models/ref_matcher.rb b/app/models/ref_matcher.rb
new file mode 100644
index 00000000000..fa7d2c0f06c
--- /dev/null
+++ b/app/models/ref_matcher.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+class RefMatcher
+ def initialize(ref_name_or_pattern)
+ @ref_name_or_pattern = ref_name_or_pattern
+ end
+
+ # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`])
+ # that match the current protected ref.
+ def matching(refs)
+ refs.select { |ref| matches?(ref.name) }
+ end
+
+ # Checks if the protected ref matches the given ref name.
+ def matches?(ref_name)
+ return false if @ref_name_or_pattern.blank?
+
+ exact_match?(ref_name) || wildcard_match?(ref_name)
+ end
+
+ # Checks if this protected ref contains a wildcard
+ def wildcard?
+ @ref_name_or_pattern && @ref_name_or_pattern.include?('*')
+ end
+
+ protected
+
+ def exact_match?(ref_name)
+ @ref_name_or_pattern == ref_name
+ end
+
+ def wildcard_match?(ref_name)
+ return false unless wildcard?
+
+ wildcard_regex === ref_name
+ end
+
+ def wildcard_regex
+ @wildcard_regex ||= begin
+ name = @ref_name_or_pattern.gsub('*', 'STAR_DONT_ESCAPE')
+ quoted_name = Regexp.quote(name)
+ regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
+ /\A#{regex_string}\z/
+ end
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index cf255c8951f..a3a3ce179fc 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -81,10 +81,6 @@ class Repository
alias_method :raw, :raw_repository
- def cleanup
- @raw_repository&.cleanup
- end
-
# Don't use this! It's going away. Use Gitaly to read or write from repos.
def path_to_repo
@path_to_repo ||=
@@ -514,7 +510,7 @@ class Repository
raw_repository.exists?
end
- cache_method :exists?
+ cache_method_asymmetrically :exists?
# We don't need to cache the output of this method because both exists? and
# has_visible_content? are already memoized and cached. There's no guarantee
@@ -580,7 +576,12 @@ class Repository
end
def rendered_readme
- MarkupHelper.markup_unsafe(readme.name, readme.data, project: project, markdown_engine: :redcarpet) if readme
+ return unless readme
+
+ context = { project: project }
+ context[:markdown_engine] = :redcarpet unless MarkupHelper.commonmark_for_repositories_enabled?
+
+ MarkupHelper.markup_unsafe(readme.name, readme.data, context)
end
cache_method :rendered_readme
@@ -611,7 +612,7 @@ class Repository
Licensee::License.new(license_key)
end
- cache_method :license, memoize_only: true
+ memoize_method :license
def gitignore
file_on_head(:gitignore)
@@ -667,6 +668,14 @@ class Repository
end
end
+ def list_last_commits_for_tree(sha, path, offset: 0, limit: 25)
+ commits = raw_repository.list_last_commits_for_tree(sha, path, offset: offset, limit: limit)
+
+ commits.each do |path, commit|
+ commits[path] = ::Commit.new(commit, @project)
+ end
+ end
+
def last_commit_for_path(sha, path)
commit = raw_repository.last_commit_for_path(sha, path)
::Commit.new(commit, @project) if commit
@@ -994,14 +1003,6 @@ class Repository
remote_branch: merge_request.target_branch)
end
- def blob_data_at(sha, path)
- blob = blob_at(sha, path)
- return unless blob
-
- blob.load_all_data!
- blob.data
- end
-
def squash(user, merge_request)
raw.squash(user, merge_request.id, branch: merge_request.target_branch,
start_sha: merge_request.diff_start_sha,
@@ -1010,6 +1011,14 @@ class Repository
message: merge_request.title)
end
+ def blob_data_at(sha, path)
+ blob = blob_at(sha, path)
+ return unless blob
+
+ blob.load_all_data!
+ blob.data
+ end
+
private
# TODO Generice finder, later split this on finders by Ref or Oid
@@ -1028,6 +1037,10 @@ class Repository
@cache ||= Gitlab::RepositoryCache.new(self)
end
+ def request_store_cache
+ @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
+ end
+
def tags_sorted_by_committed_date
tags.sort_by do |tag|
# Annotated tags can point to any object (e.g. a blob), but generally
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index 42c255fcd1e..3fd96b9dc18 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -3,33 +3,122 @@
# This model is not used yet, it will be used for:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483
class ResourceLabelEvent < ActiveRecord::Base
+ include Importable
+ include Gitlab::Utils::StrongMemoize
+ include CacheMarkdownField
+
+ cache_markdown_field :reference
+
belongs_to :user
belongs_to :issue
belongs_to :merge_request
belongs_to :label
- validates :user, presence: true, on: :create
- validates :label, presence: true, on: :create
+ scope :created_after, ->(time) { where('created_at > ?', time) }
+
+ validates :user, presence: { unless: :importing? }, on: :create
+ validates :label, presence: { unless: :importing? }, on: :create
validate :exactly_one_issuable
+ after_save :expire_etag_cache
+ after_destroy :expire_etag_cache
+
enum action: {
add: 1,
remove: 2
}
- def self.issuable_columns
- %i(issue_id merge_request_id).freeze
+ def self.issuable_attrs
+ %i(issue merge_request).freeze
end
def issuable
issue || merge_request
end
+ # create same discussion id for all actions with the same user and time
+ def discussion_id(resource = nil)
+ strong_memoize(:discussion_id) do
+ Digest::SHA1.hexdigest([self.class.name, created_at, user_id].join("-"))
+ end
+ end
+
+ def project
+ issuable.project
+ end
+
+ def group
+ issuable.group if issuable.respond_to?(:group)
+ end
+
+ def outdated_markdown?
+ return true if label_id.nil? && reference.present?
+
+ reference.nil? || latest_cached_markdown_version != cached_markdown_version
+ end
+
+ def banzai_render_context(field)
+ super.merge(pipeline: 'label', only_path: true)
+ end
+
+ def refresh_invalid_reference
+ # label_id could be nullified on label delete
+ self.reference = '' if label_id.nil?
+
+ # reference is not set for events which were not rendered yet
+ self.reference ||= label_reference
+
+ if changed?
+ save
+ elsif invalidated_markdown_cache?
+ refresh_markdown_cache!
+ end
+ end
+
private
+ def label_reference
+ if local_label?
+ label.to_reference(format: :id)
+ elsif label.is_a?(GroupLabel)
+ label.to_reference(label.group, target_project: resource_parent, format: :id)
+ else
+ label.to_reference(resource_parent, format: :id)
+ end
+ end
+
def exactly_one_issuable
- if self.class.issuable_columns.count { |attr| self[attr] } != 1
- errors.add(:base, "Exactly one of #{self.class.issuable_columns.join(', ')} is required")
+ issuable_count = self.class.issuable_attrs.count { |attr| self["#{attr}_id"] }
+
+ return true if issuable_count == 1
+
+ # if none of issuable IDs is set, check explicitly if nested issuable
+ # object is set, this is used during project import
+ if issuable_count == 0 && importing?
+ issuable_count = self.class.issuable_attrs.count { |attr| self.public_send(attr) } # rubocop:disable GitlabSecurity/PublicSend
+
+ return true if issuable_count == 1
end
+
+ errors.add(:base, "Exactly one of #{self.class.issuable_attrs.join(', ')} is required")
+ end
+
+ def expire_etag_cache
+ issuable.expire_note_etag_cache
+ end
+
+ def local_label?
+ params = { include_ancestor_groups: true }
+ if resource_parent.is_a?(Project)
+ params[:project_id] = resource_parent.id
+ else
+ params[:group_id] = resource_parent.id
+ end
+
+ LabelsFinder.new(nil, params).execute(skip_authorization: true).where(id: label.id).any?
+ end
+
+ def resource_parent
+ issuable.project || issuable.group
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 140058771ee..4dbda7acab6 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -5,6 +5,7 @@
class Service < ActiveRecord::Base
include Sortable
include Importable
+ include ProjectServicesLoggable
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
diff --git a/app/models/site_statistic.rb b/app/models/site_statistic.rb
index 48324570f0b..3a7912ed53a 100644
--- a/app/models/site_statistic.rb
+++ b/app/models/site_statistic.rb
@@ -4,7 +4,7 @@ class SiteStatistic < ActiveRecord::Base
# prevents the creation of multiple rows
default_value_for :id, 1
- COUNTER_ATTRIBUTES = %w(repositories_count wikis_count).freeze
+ COUNTER_ATTRIBUTES = %w(repositories_count).freeze
REQUIRED_SCHEMA_VERSION = 20180629153018
# Tracks specific attribute
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 5b394e3fa79..e9533ee7c77 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -12,6 +12,7 @@ class Snippet < ActiveRecord::Base
include Spammable
include Editable
include Gitlab::SQL::Pattern
+ include FromUnion
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 376ef673ca8..d555ebe5322 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -9,13 +9,14 @@ class SystemNoteMetadata < ActiveRecord::Base
TYPES_WITH_CROSS_REFERENCES = %w[
commit cross_reference
close duplicate
+ moved
].freeze
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved
opened closed merged duplicate locked unlocked
- outdated tag
+ outdated tag due_date
].freeze
validates :note, presence: true
@@ -26,4 +27,8 @@ class SystemNoteMetadata < ActiveRecord::Base
def icon_types
ICON_TYPES
end
+
+ def cross_reference_types
+ TYPES_WITH_CROSS_REFERENCES
+ end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 48d92ad04b3..265fb932f7c 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -2,6 +2,7 @@
class Todo < ActiveRecord::Base
include Sortable
+ include FromUnion
ASSIGNED = 1
MENTIONED = 2
diff --git a/app/models/user.rb b/app/models/user.rb
index f21ca1c569f..cd3b1c95b7e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -20,6 +20,7 @@ class User < ActiveRecord::Base
include BlocksJsonSerialization
include WithUploads
include OptionallySearch
+ include FromUnion
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -61,6 +62,7 @@ class User < ActiveRecord::Base
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
+ # rubocop: disable CodeReuse/ServiceClass
def update_tracked_fields!(request)
return if Gitlab::Database.read_only?
@@ -71,6 +73,7 @@ class User < ActiveRecord::Base
Users::UpdateService.new(self, user: self).execute(validate: false)
end
+ # rubocop: enable CodeReuse/ServiceClass
attr_accessor :force_random_password
@@ -159,6 +162,7 @@ class User < ActiveRecord::Base
validates :notification_email, presence: true
validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
+ validates :commit_email, email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email }
validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit,
presence: true,
@@ -171,12 +175,15 @@ class User < ActiveRecord::Base
validate :unique_email, if: :email_changed?
validate :owns_notification_email, if: :notification_email_changed?
validate :owns_public_email, if: :public_email_changed?
+ validate :owns_commit_email, if: :commit_email_changed?
validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :new_record?
before_validation :set_public_email, if: :public_email_changed?
+ before_validation :set_commit_email, if: :commit_email_changed?
before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
+ before_save :set_commit_email, if: :commit_email_changed? # in case validation is skipped
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
@@ -257,6 +264,7 @@ class User < ActiveRecord::Base
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
+ scope :by_username, -> (usernames) { iwhere(username: usernames) }
# Limits the users to those that have TODOs, optionally in the given state.
#
@@ -279,11 +287,9 @@ class User < ActiveRecord::Base
# user_id - The ID of the user to include.
def self.union_with_user(user_id = nil)
if user_id.present?
- union = Gitlab::SQL::Union.new([all, User.unscoped.where(id: user_id)])
-
# We use "unscoped" here so that any inner conditions are not repeated for
# the outer query, which would be redundant.
- User.unscoped.from("(#{union.to_sql}) #{User.table_name}")
+ User.unscoped.from_union([all, User.unscoped.where(id: user_id)])
else
all
end
@@ -347,9 +353,8 @@ class User < ActiveRecord::Base
emails = joins(:emails).where(emails: { email: email })
emails = emails.confirmed if confirmed
- union = Gitlab::SQL::Union.new([users, emails])
- from("(#{union.to_sql}) #{table_name}")
+ from_union([users, emails])
end
def filter(filter_name)
@@ -444,17 +449,17 @@ class User < ActiveRecord::Base
end
def find_by_username(username)
- iwhere(username: username).take
+ by_username(username).take
end
def find_by_username!(username)
- iwhere(username: username).take!
+ by_username(username).take!
end
def find_by_personal_access_token(token_string)
return unless token_string
- PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
+ PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user # rubocop: disable CodeReuse/Finder
end
# Returns a user for the given SSH key.
@@ -489,6 +494,16 @@ class User < ActiveRecord::Base
u.name = 'Ghost User'
end
end
+
+ # Return true if there is only single non-internal user in the deployment,
+ # ghost user is ignored.
+ def single_user?
+ User.non_internal.limit(2).count == 1
+ end
+
+ def single_user
+ User.non_internal.first if single_user?
+ end
end
def full_path
@@ -606,6 +621,32 @@ class User < ActiveRecord::Base
errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end
+ def owns_commit_email
+ return if read_attribute(:commit_email).blank?
+
+ errors.add(:commit_email, "is not an email you own") unless verified_emails.include?(commit_email)
+ end
+
+ # Define commit_email-related attribute methods explicitly instead of relying
+ # on ActiveRecord to provide them. Some of the specs use the current state of
+ # the model code but an older database schema, so we need to guard against the
+ # possibility of the commit_email column not existing.
+
+ def commit_email
+ return self.email unless has_attribute?(:commit_email)
+
+ # The commit email is the same as the primary email if undefined
+ super.presence || self.email
+ end
+
+ def commit_email=(email)
+ super if has_attribute?(:commit_email)
+ end
+
+ def commit_email_changed?
+ has_attribute?(:commit_email) && super
+ end
+
# see if the new email is already a verified secondary email
def check_for_verified_email
skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
@@ -616,6 +657,7 @@ class User < ActiveRecord::Base
# hash and `_was` variables getting munged.
# By using an `after_commit` instead of `after_update`, we avoid the recursive callback
# scenario, though it then requires us to use the `previous_changes` hash
+ # rubocop: disable CodeReuse/ServiceClass
def update_emails_with_primary_email(previous_email)
primary_email_record = emails.find_by(email: email)
Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
@@ -624,6 +666,7 @@ class User < ActiveRecord::Base
# have access to the original confirmation values at this point, so just set confirmed_at
Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
end
+ # rubocop: enable CodeReuse/ServiceClass
def update_invalid_gpg_signatures
gpg_keys.each(&:update_invalid_gpg_signatures)
@@ -631,10 +674,12 @@ class User < ActiveRecord::Base
# Returns the groups a user has access to, either through a membership or a project authorization
def authorized_groups
- union = Gitlab::SQL::Union
- .new([groups.select(:id), authorized_projects.select(:namespace_id)])
-
- Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ Group.unscoped do
+ Group.from_union([
+ groups,
+ authorized_projects.joins(:namespace).select('namespaces.*')
+ ])
+ end
end
# Returns the groups a user is a member of, either directly or through a parent group
@@ -652,9 +697,11 @@ class User < ActiveRecord::Base
all_expanded_groups.where(require_two_factor_authentication: true)
end
+ # rubocop: disable CodeReuse/ServiceClass
def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute
end
+ # rubocop: enable CodeReuse/ServiceClass
def remove_project_authorizations(project_ids)
project_authorizations.where(project_id: project_ids).delete_all
@@ -697,7 +744,15 @@ class User < ActiveRecord::Base
end
def owned_projects
- @owned_projects ||= Project.from("(#{owned_projects_union.to_sql}) AS projects")
+ @owned_projects ||= Project.from_union(
+ [
+ Project.where(namespace: namespace),
+ Project.joins(:project_authorizations)
+ .where("projects.namespace_id <> ?", namespace.id)
+ .where(project_authorizations: { user_id: id, access_level: Gitlab::Access::OWNER })
+ ],
+ remove_duplicates: false
+ )
end
# Returns projects which user can admin issues on (for example to move an issue to that project).
@@ -707,11 +762,13 @@ class User < ActiveRecord::Base
authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
end
+ # rubocop: disable CodeReuse/ServiceClass
def require_ssh_key?
count = Users::KeysCountService.new(self).count
count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
end
+ # rubocop: enable CodeReuse/ServiceClass
def require_password_creation_for_web?
allow_password_authentication_for_web? && password_automatically_set?
@@ -775,6 +832,7 @@ class User < ActiveRecord::Base
projects_limit - personal_projects_count
end
+ # rubocop: disable CodeReuse/ServiceClass
def recent_push(project = nil)
service = Users::LastPushEventService.new(self)
@@ -784,6 +842,7 @@ class User < ActiveRecord::Base
service.last_event_for_user
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def several_namespaces?
owned_groups.any? || maintainers_groups.any?
@@ -852,10 +911,17 @@ class User < ActiveRecord::Base
end
end
+ def set_commit_email
+ if commit_email.blank? || verified_emails.exclude?(commit_email)
+ self.commit_email = nil
+ end
+ end
+
def update_secondary_emails!
set_notification_email
set_public_email
- save if notification_email_changed? || public_email_changed?
+ set_commit_email
+ save if notification_email_changed? || public_email_changed? || commit_email_changed?
end
def set_projects_limit
@@ -921,9 +987,11 @@ class User < ActiveRecord::Base
email.start_with?('temp-email-for-oauth')
end
+ # rubocop: disable CodeReuse/ServiceClass
def avatar_url(size: nil, scale: 2, **args)
GravatarService.new.execute(email, size, scale, username: username)
end
+ # rubocop: enable CodeReuse/ServiceClass
def primary_email_verified?
confirmed? && !temp_oauth_email?
@@ -989,26 +1057,32 @@ class User < ActiveRecord::Base
system_hook_service.execute_hooks_for(self, :destroy)
end
+ # rubocop: disable CodeReuse/ServiceClass
def remove_key_cache
Users::KeysCountService.new(self).delete_cache
end
+ # rubocop: enable CodeReuse/ServiceClass
def delete_async(deleted_by:, params: {})
block if params[:hard_delete]
DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
end
+ # rubocop: disable CodeReuse/ServiceClass
def notification_service
NotificationService.new
end
+ # rubocop: enable CodeReuse/ServiceClass
def log_info(message)
Gitlab::AppLogger.info message
end
+ # rubocop: disable CodeReuse/ServiceClass
def system_hook_service
SystemHooksService.new
end
+ # rubocop: enable CodeReuse/ServiceClass
def starred?(project)
starred_projects.exists?(project.id)
@@ -1072,17 +1146,17 @@ class User < ActiveRecord::Base
def ci_owned_runners
@ci_owned_runners ||= begin
- project_runner_ids = Ci::RunnerProject
+ project_runners = Ci::RunnerProject
.where(project: authorized_projects(Gitlab::Access::MAINTAINER))
- .select(:runner_id)
+ .joins(:runner)
+ .select('ci_runners.*')
- group_runner_ids = Ci::RunnerNamespace
+ group_runners = Ci::RunnerNamespace
.where(namespace_id: owned_or_maintainers_groups.select(:id))
- .select(:runner_id)
-
- union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids])
+ .joins(:runner)
+ .select('ci_runners.*')
- Ci::Runner.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ Ci::Runner.from_union([project_runners, group_runners])
end
end
@@ -1110,13 +1184,13 @@ class User < ActiveRecord::Base
def assigned_open_merge_requests_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
- MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
+ MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
end
end
def assigned_open_issues_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
- IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
+ IssuesFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
end
end
@@ -1177,6 +1251,7 @@ class User < ActiveRecord::Base
# See:
# <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
#
+ # rubocop: disable CodeReuse/ServiceClass
def increment_failed_attempts!
return if ::Gitlab::Database.read_only?
@@ -1189,6 +1264,7 @@ class User < ActiveRecord::Base
Users::UpdateService.new(self, user: self).execute(validate: false)
end
end
+ # rubocop: enable CodeReuse/ServiceClass
def access_level
if admin?
@@ -1286,6 +1362,10 @@ class User < ActiveRecord::Base
!terms_accepted?
end
+ def requires_usage_stats_consent?
+ !consented_usage_stats? && 7.days.ago > self.created_at && !has_current_license? && User.single_user?
+ end
+
# @deprecated
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
@@ -1300,13 +1380,12 @@ class User < ActiveRecord::Base
private
- def owned_projects_union
- Gitlab::SQL::Union.new([
- Project.where(namespace: namespace),
- Project.joins(:project_authorizations)
- .where("projects.namespace_id <> ?", namespace.id)
- .where(project_authorizations: { user_id: id, access_level: Gitlab::Access::OWNER })
- ], remove_duplicates: false)
+ def has_current_license?
+ false
+ end
+
+ def consented_usage_stats?
+ Gitlab::CurrentSettings.usage_stats_set_by_user_id == self.id
end
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
@@ -1417,7 +1496,7 @@ class User < ActiveRecord::Base
&creation_block
)
- Users::UpdateService.new(user, user: user).execute(validate: false)
+ Users::UpdateService.new(user, user: user).execute(validate: false) # rubocop: disable CodeReuse/ServiceClass
user
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 97e955ace36..2c0e8659fc1 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -5,7 +5,9 @@ class UserCallout < ActiveRecord::Base
enum feature_name: {
gke_cluster_integration: 1,
- gcp_signup_offer: 2
+ gcp_signup_offer: 2,
+ cluster_security_warning: 3,
+ gold_trial: 4
}
validates :user, presence: true
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 33790afc35e..42fd213d03b 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -51,14 +51,14 @@ class WikiPage
validates :title, presence: true
validates :content, presence: true
- # The Gitlab ProjectWiki instance.
+ # The GitLab ProjectWiki instance.
attr_reader :wiki
# The raw Gitlab::Git::WikiPage instance.
attr_reader :page
# The attributes Hash used for storing and validating
- # new Page values before writing to the Gollum repository.
+ # new Page values before writing to the raw repository.
attr_accessor :attributes
def hook_attrs
@@ -111,10 +111,7 @@ class WikiPage
# The processed/formatted content of this page.
def formatted_content
- # Assuming @page exists, nil formatted_data means we didn't load it
- # before hand (i.e. page was fetched by Gitaly), so we fetch it separately.
- # If the page was fetched by Gollum, formatted_data would've been a String.
- @attributes[:formatted_content] ||= @page&.formatted_data || @wiki.page_formatted_data(@page)
+ @attributes[:formatted_content] ||= @wiki.page_formatted_data(@page)
end
# The markup format for the page.
@@ -127,7 +124,7 @@ class WikiPage
version.try(:message)
end
- # The Gitlab Commit instance for this page.
+ # The GitLab Commit instance for this page.
def version
return nil unless persisted?
diff --git a/app/policies/application_setting/term_policy.rb b/app/policies/application_setting/term_policy.rb
index 17f00f33d35..c0d2ceaa349 100644
--- a/app/policies/application_setting/term_policy.rb
+++ b/app/policies/application_setting/term_policy.rb
@@ -19,6 +19,7 @@ class ApplicationSetting
rule { terms_accepted }.prevent :accept_terms
+ # rubocop: disable CodeReuse/ActiveRecord
def agreement
strong_memoize(:agreement) do
next nil if @user.nil? || @subject.nil?
@@ -26,5 +27,6 @@ class ApplicationSetting
@user.term_agreements.find_by(term: @subject)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb
index c44f22b6ad3..de76b7b2b5b 100644
--- a/app/policies/ci/runner_policy.rb
+++ b/app/policies/ci/runner_policy.rb
@@ -5,7 +5,9 @@ module Ci
with_options scope: :subject, score: 0
condition(:locked, scope: :subject) { @subject.locked? }
+ # rubocop: disable CodeReuse/ActiveRecord
condition(:owned_runner) { @user.ci_owned_runners.exists?(@subject.id) }
+ # rubocop: enable CodeReuse/ActiveRecord
rule { anonymous }.prevent_all
diff --git a/app/policies/deploy_key_policy.rb b/app/policies/deploy_key_policy.rb
index 204c54a5b20..7f0ec011e79 100644
--- a/app/policies/deploy_key_policy.rb
+++ b/app/policies/deploy_key_policy.rb
@@ -4,7 +4,9 @@ class DeployKeyPolicy < BasePolicy
with_options scope: :subject, score: 0
condition(:private_deploy_key) { @subject.private? }
+ # rubocop: disable CodeReuse/ActiveRecord
condition(:has_deploy_key) { @user.project_deploy_keys.exists?(id: @subject.id) }
+ # rubocop: enable CodeReuse/ActiveRecord
rule { anonymous }.prevent_all
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index 198bb168d85..6d8b575102e 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -14,6 +14,7 @@ class IssuablePolicy < BasePolicy
rule { assignee_or_author }.policy do
enable :read_issue
enable :update_issue
+ enable :reopen_issue
enable :read_merge_request
enable :update_merge_request
end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 94b5f37c682..a0706eaa46c 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -19,4 +19,8 @@ class IssuePolicy < IssuablePolicy
prevent :update_issue
prevent :admin_issue
end
+
+ rule { locked }.policy do
+ prevent :reopen_issue
+ end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index fd6cc504a3b..a76a083bceb 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -110,6 +110,7 @@ class ProjectPolicy < BasePolicy
snippets
wiki
builds
+ pages
]
features.each do |f|
@@ -167,6 +168,7 @@ class ProjectPolicy < BasePolicy
enable :upload_file
enable :read_cycle_analytics
enable :award_emoji
+ enable :read_pages_content
end
# These abilities are not allowed to admins that are not members of the project,
@@ -180,6 +182,7 @@ class ProjectPolicy < BasePolicy
enable :fork_project
enable :create_project_snippet
enable :update_issue
+ enable :reopen_issue
enable :admin_issue
enable :admin_label
enable :admin_list
@@ -285,6 +288,8 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:merge_request))
end
+ rule { pages_disabled }.prevent :read_pages_content
+
rule { issues_disabled & merge_requests_disabled }.policy do
prevent(*create_read_update_admin_destroy(:label))
prevent(*create_read_update_admin_destroy(:milestone))
@@ -344,6 +349,7 @@ class ProjectPolicy < BasePolicy
enable :download_code
enable :download_wiki_code
enable :read_cycle_analytics
+ enable :read_pages_content
# NOTE: may be overridden by IssuePolicy
enable :read_issue
@@ -389,7 +395,11 @@ class ProjectPolicy < BasePolicy
greedy_load_subject ||= !@user.persisted?
if greedy_load_subject
- project.team.members.include?(user)
+ # We want to load all the members with one query. Calling #include? on
+ # project.team.members will perform a separate query for each user, unless
+ # project.team.members was loaded before somewhere else. Calling #to_a
+ # ensures it's always loaded before checking for membership.
+ project.team.members.to_a.include?(user)
else
# otherwise we just make a specific query for
# this particular user.
@@ -397,6 +407,7 @@ class ProjectPolicy < BasePolicy
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def project_group_member?
return false if @user.nil?
@@ -406,6 +417,7 @@ class ProjectPolicy < BasePolicy
project.group.requesters.exists?(user_id: @user.id)
)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def team_access_level
return -1 if @user.nil?
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 5331cdf632b..33056a809b7 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -35,6 +35,10 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
+ def execute_in
+ scheduled? && scheduled_at && [0, scheduled_at - Time.now].max
+ end
+
private
def tooltip_for_badge
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index a08f34e2335..29eaad759bb 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -8,13 +8,20 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure',
- runner_unsupported: 'Your runner is outdated, please upgrade your runner'
+ runner_unsupported: 'Your runner is outdated, please upgrade your runner',
+ stale_schedule: 'Delayed job could not be executed by some reason, please try again'
}.freeze
+ private_constant :CALLOUT_FAILURE_MESSAGES
+
presents :build
+ def self.callout_failure_messages
+ CALLOUT_FAILURE_MESSAGES
+ end
+
def callout_failure_message
- CALLOUT_FAILURE_MESSAGES.fetch(failure_reason.to_sym)
+ self.class.callout_failure_messages.fetch(failure_reason.to_sym)
end
def recoverable?
diff --git a/app/presenters/conversational_development_index/metric_presenter.rb b/app/presenters/conversational_development_index/metric_presenter.rb
index e0312c6f431..9639b84cf56 100644
--- a/app/presenters/conversational_development_index/metric_presenter.rb
+++ b/app/presenters/conversational_development_index/metric_presenter.rb
@@ -139,8 +139,10 @@ module ConversationalDevelopmentIndex
]
end
+ # rubocop: disable CodeReuse/ActiveRecord
def average_percentage_score
cards.sum(&:percentage_score) / cards.size.to_f
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 8c4eac3c31d..3f565b826dd 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -142,6 +142,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def assign_to_closing_issues_link
+ # rubocop: disable CodeReuse/ServiceClass
issues = MergeRequests::AssignIssuesService.new(project,
current_user,
merge_request: merge_request,
@@ -152,6 +153,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue"
link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post
end
+ # rubocop: enable CodeReuse/ServiceClass
end
def can_revert_on_current_merge_request?
@@ -202,7 +204,9 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def conflicts
+ # rubocop: disable CodeReuse/ServiceClass
@conflicts ||= MergeRequests::Conflicts::ListService.new(merge_request)
+ # rubocop: enable CodeReuse/ServiceClass
end
def closing_issues
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 4c2f33213d6..d2434d96fd7 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -11,16 +11,18 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
presents :project
+ AnchorData = Struct.new(:enabled, :label, :link, :class_modifier)
+ MAX_TAGS_TO_SHOW = 3
+
def statistics_anchors(show_auto_devops_callout:)
[
+ readme_anchor_data,
+ changelog_anchor_data,
+ contribution_guide_anchor_data,
files_anchor_data,
commits_anchor_data,
branches_anchor_data,
tags_anchor_data,
- readme_anchor_data,
- changelog_anchor_data,
- license_anchor_data,
- contribution_guide_anchor_data,
gitlab_ci_anchor_data,
autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout),
kubernetes_cluster_anchor_data
@@ -31,7 +33,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
[
readme_anchor_data,
changelog_anchor_data,
- license_anchor_data,
contribution_guide_anchor_data,
autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout),
kubernetes_cluster_anchor_data,
@@ -42,6 +43,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def empty_repo_statistics_anchors
[
+ files_anchor_data,
+ commits_anchor_data,
+ branches_anchor_data,
+ tags_anchor_data,
autodevops_anchor_data,
kubernetes_cluster_anchor_data
].compact.select { |item| item.enabled }
@@ -51,7 +56,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
[
new_file_anchor_data,
readme_anchor_data,
- license_anchor_data,
autodevops_anchor_data,
kubernetes_cluster_anchor_data
].compact.reject { |item| item.enabled }
@@ -182,95 +186,101 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def files_anchor_data
- OpenStruct.new(enabled: true,
- label: _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) },
- link: project_tree_path(project))
+ AnchorData.new(true,
+ _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) },
+ empty_repo? ? nil : project_tree_path(project))
end
def commits_anchor_data
- OpenStruct.new(enabled: true,
- label: n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) },
- link: project_commits_path(project, repository.root_ref))
+ AnchorData.new(true,
+ n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) },
+ empty_repo? ? nil : project_commits_path(project, repository.root_ref))
end
def branches_anchor_data
- OpenStruct.new(enabled: true,
- label: n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) },
- link: project_branches_path(project))
+ AnchorData.new(true,
+ n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) },
+ empty_repo? ? nil : project_branches_path(project))
end
def tags_anchor_data
- OpenStruct.new(enabled: true,
- label: n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) },
- link: project_tags_path(project))
+ AnchorData.new(true,
+ n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) },
+ empty_repo? ? nil : project_tags_path(project))
end
def new_file_anchor_data
if current_user && can_current_user_push_to_default_branch?
- OpenStruct.new(enabled: false,
- label: _('New file'),
- link: project_new_blob_path(project, default_branch || 'master'),
- class_modifier: 'new')
+ AnchorData.new(false,
+ _('New file'),
+ project_new_blob_path(project, default_branch || 'master'),
+ 'new')
end
end
def readme_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.readme.nil?
- OpenStruct.new(enabled: false,
- label: _('Add Readme'),
- link: add_readme_path)
+ AnchorData.new(false,
+ _('Add Readme'),
+ add_readme_path)
elsif repository.readme
- OpenStruct.new(enabled: true,
- label: _('Readme'),
- link: default_view != 'readme' ? readme_path : '#readme')
+ AnchorData.new(true,
+ _('Readme'),
+ default_view != 'readme' ? readme_path : '#readme')
end
end
def changelog_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
- OpenStruct.new(enabled: false,
- label: _('Add Changelog'),
- link: add_changelog_path)
+ AnchorData.new(false,
+ _('Add Changelog'),
+ add_changelog_path)
elsif repository.changelog.present?
- OpenStruct.new(enabled: true,
- label: _('Changelog'),
- link: changelog_path)
+ AnchorData.new(true,
+ _('Changelog'),
+ changelog_path)
end
end
def license_anchor_data
- if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank?
- OpenStruct.new(enabled: false,
- label: _('Add License'),
- link: add_license_path)
- elsif repository.license_blob.present?
- OpenStruct.new(enabled: true,
- label: license_short_name,
- link: license_path)
+ if repository.license_blob.present?
+ AnchorData.new(true,
+ license_short_name,
+ license_path)
+ else
+ if current_user && can_current_user_push_to_default_branch?
+ AnchorData.new(false,
+ _('Add license'),
+ add_license_path)
+ else
+ AnchorData.new(false,
+ _('No license. All rights reserved'),
+ nil)
+ end
end
end
def contribution_guide_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
- OpenStruct.new(enabled: false,
- label: _('Add Contribution guide'),
- link: add_contribution_guide_path)
+ AnchorData.new(false,
+ _('Add Contribution guide'),
+ add_contribution_guide_path)
elsif repository.contribution_guide.present?
- OpenStruct.new(enabled: true,
- label: _('Contribution guide'),
- link: contribution_guide_path)
+ AnchorData.new(true,
+ _('Contribution guide'),
+ contribution_guide_path)
end
end
def autodevops_anchor_data(show_auto_devops_callout: false)
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
- OpenStruct.new(enabled: auto_devops_enabled?,
- label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
- link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
+ AnchorData.new(auto_devops_enabled?,
+ auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
+ project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
elsif auto_devops_enabled?
- OpenStruct.new(enabled: true,
- label: _('Auto DevOps enabled'),
- link: nil)
+ AnchorData.new(true,
+ _('Auto DevOps enabled'),
+ nil)
end
end
@@ -282,32 +292,48 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
cluster_link = new_project_cluster_path(project)
end
- OpenStruct.new(enabled: !clusters.empty?,
- label: clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'),
- link: cluster_link)
+ AnchorData.new(!clusters.empty?,
+ clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'),
+ cluster_link)
end
end
def gitlab_ci_anchor_data
if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled?
- OpenStruct.new(enabled: false,
- label: _('Set up CI/CD'),
- link: add_ci_yml_path)
+ AnchorData.new(false,
+ _('Set up CI/CD'),
+ add_ci_yml_path)
elsif repository.gitlab_ci_yml.present?
- OpenStruct.new(enabled: true,
- label: _('CI/CD configuration'),
- link: ci_configuration_path)
+ AnchorData.new(true,
+ _('CI/CD configuration'),
+ ci_configuration_path)
end
end
def koding_anchor_data
if current_user && can_current_user_push_code? && koding_enabled? && repository.koding_yml.blank?
- OpenStruct.new(enabled: false,
- label: _('Set up Koding'),
- link: add_koding_stack_path)
+ AnchorData.new(false,
+ _('Set up Koding'),
+ add_koding_stack_path)
end
end
+ def tags_to_show
+ project.tag_list.take(MAX_TAGS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def count_of_extra_tags_not_shown
+ if project.tag_list.count > MAX_TAGS_TO_SHOW
+ project.tag_list.count - MAX_TAGS_TO_SHOW
+ else
+ 0
+ end
+ end
+
+ def has_extra_tags?
+ count_of_extra_tags_not_shown > 0
+ end
+
private
def filename_path(filename)
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
index 28eaef00a12..85518c9a3a4 100644
--- a/app/presenters/projects/settings/deploy_keys_presenter.rb
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -12,9 +12,11 @@ module Projects
@key ||= DeployKey.new.tap { |dk| dk.deploy_keys_projects.build }
end
+ # rubocop: disable CodeReuse/ActiveRecord
def enabled_keys
@enabled_keys ||= project.deploy_keys.includes(:projects)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def any_keys_enabled?
enabled_keys.any?
@@ -24,14 +26,17 @@ module Projects
@available_keys ||= current_user.accessible_deploy_keys - enabled_keys
end
+ # rubocop: disable CodeReuse/ActiveRecord
def available_project_keys
@available_project_keys ||= current_user.project_deploy_keys.includes(:projects) - enabled_keys
end
+ # rubocop: enable CodeReuse/ActiveRecord
def key_available?(deploy_key)
available_keys.include?(deploy_key)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def available_public_keys
return @available_public_keys if defined?(@available_public_keys)
@@ -41,9 +46,10 @@ module Projects
# in @available_project_keys.
@available_public_keys -= available_project_keys
end
+ # rubocop: enable CodeReuse/ActiveRecord
def as_json
- serializer = DeployKeySerializer.new
+ serializer = DeployKeySerializer.new # rubocop: disable CodeReuse/Serializer
opts = { user: current_user }
{
diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb
index f9da3f63911..0db7875aa87 100644
--- a/app/serializers/build_action_entity.rb
+++ b/app/serializers/build_action_entity.rb
@@ -12,6 +12,11 @@ class BuildActionEntity < Grape::Entity
end
expose :playable?, as: :playable
+ expose :scheduled_at, if: -> (build) { build.scheduled? }
+
+ expose :unschedule_path, if: -> (build) { build.scheduled? } do |build|
+ unschedule_project_job_path(build.project, build)
+ end
private
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index b887b99d31c..3d508a9a407 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -3,17 +3,50 @@
class BuildDetailsEntity < JobEntity
expose :coverage, :erased_at, :duration
expose :tag_list, as: :tags
+ expose :has_trace?, as: :has_trace
expose :user, using: UserEntity
expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity
+ expose :deployment_status, if: -> (*) { build.has_environment? } do
+ expose :deployment_status, as: :status
+
+ expose :persisted_environment, as: :environment, with: EnvironmentEntity
+ end
+
expose :metadata, using: BuildMetadataEntity
+ expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do
+ expose :download_path, if: -> (*) { build.artifacts? } do |build|
+ download_project_job_artifacts_path(project, build)
+ end
+
+ expose :browse_path, if: -> (*) { build.browsable_artifacts? } do |build|
+ browse_project_job_artifacts_path(project, build)
+ end
+
+ expose :keep_path, if: -> (*) { build.has_expiring_artifacts? && can?(current_user, :update_build, build) } do |build|
+ keep_project_job_artifacts_path(project, build)
+ end
+
+ expose :expire_at, if: -> (*) { build.artifacts_expire_at.present? } do |build|
+ build.artifacts_expire_at
+ end
+
+ expose :expired, if: -> (*) { build.artifacts_expire_at.present? } do |build|
+ build.artifacts_expired?
+ end
+ end
+
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
end
+ expose :terminal_path, if: -> (*) { can_create_build_terminal? } do |build|
+ terminal_project_job_path(project, build)
+ end
+
expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do
expose :iid do |build|
build.merge_request.iid
@@ -33,6 +66,26 @@ class BuildDetailsEntity < JobEntity
raw_project_job_path(project, build)
end
+ expose :trigger, if: -> (*) { build.trigger_request } do
+ expose :trigger_short_token, as: :short_token
+
+ expose :trigger_variables, as: :variables, using: TriggerVariableEntity
+ end
+
+ expose :runners do
+ expose :online do |build|
+ build.any_runners_online?
+ end
+
+ expose :available do |build|
+ project.any_runners?
+ end
+
+ expose :settings_path, if: -> (*) { can_admin_build? } do |build|
+ project_runners_path(project)
+ end
+ end
+
private
def build_failed_issue_options
@@ -47,4 +100,12 @@ class BuildDetailsEntity < JobEntity
def project
build.project
end
+
+ def can_create_build_terminal?
+ can?(current_user, :create_build_terminal, build) && build.has_terminal?
+ end
+
+ def can_admin_build?
+ can?(request.current_user, :admin_build, project)
+ end
end
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
index b3287c66554..a94e32478ce 100644
--- a/app/serializers/commit_entity.rb
+++ b/app/serializers/commit_entity.rb
@@ -1,19 +1,49 @@
# frozen_string_literal: true
class CommitEntity < API::Entities::Commit
+ include MarkupHelper
include RequestAwareEntity
expose :author, using: UserEntity
expose :author_gravatar_url do |commit|
- GravatarService.new.execute(commit.author_email)
+ GravatarService.new.execute(commit.author_email) # rubocop: disable CodeReuse/ServiceClass
end
- expose :commit_url do |commit|
- project_commit_url(request.project, commit)
+ expose :commit_url do |commit, options|
+ project_commit_url(request.project, commit, params: options.fetch(:commit_url_params, {}))
end
- expose :commit_path do |commit|
- project_commit_path(request.project, commit)
+ expose :commit_path do |commit, options|
+ project_commit_path(request.project, commit, params: options.fetch(:commit_url_params, {}))
+ end
+
+ expose :description_html, if: { type: :full } do |commit|
+ markdown_field(commit, :description)
+ end
+
+ expose :title_html, if: { type: :full } do |commit|
+ markdown_field(commit, :title)
+ end
+
+ expose :signature_html, if: { type: :full } do |commit|
+ render('projects/commit/_signature', signature: commit.signature) if commit.has_signature?
+ end
+
+ expose :pipeline_status_path, if: { type: :full } do |commit, options|
+ pipeline_ref = options[:pipeline_ref]
+ pipeline_project = options[:pipeline_project] || commit.project
+ next unless pipeline_ref && pipeline_project
+
+ status = commit.status_for_project(pipeline_ref, pipeline_project)
+ next unless status
+
+ pipelines_project_commit_path(pipeline_project, commit.id, ref: pipeline_ref)
+ end
+
+ def render(*args)
+ return unless request.respond_to?(:render) && request.render.respond_to?(:call)
+
+ request.render.call(*args)
end
end
diff --git a/app/serializers/status_entity.rb b/app/serializers/detailed_status_entity.rb
index 306c30f0323..da994d78286 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/detailed_status_entity.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class StatusEntity < Grape::Entity
+class DetailedStatusEntity < Grape::Entity
include RequestAwareEntity
expose :icon, :text, :label, :group
@@ -8,6 +8,19 @@ class StatusEntity < Grape::Entity
expose :has_details?, as: :has_details
expose :details_path
+ expose :illustration do |status|
+ begin
+ illustration = {
+ image: ActionController::Base.helpers.image_path(status.illustration[:image])
+ }
+ illustration = status.illustration.merge(illustration)
+
+ illustration
+ rescue NotImplementedError
+ # ignored
+ end
+ end
+
expose :favicon do |status|
Gitlab::Favicon.status_overlay(status.favicon)
end
@@ -17,5 +30,6 @@ class StatusEntity < Grape::Entity
expose :action_title, as: :title
expose :action_path, as: :path
expose :action_method, as: :method
+ expose :action_button_title, as: :button_title
end
end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index d49d4895d89..63ea8e8f95f 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -84,7 +84,7 @@ class DiffFileEntity < Grape::Entity
end
expose :old_path_html do |diff_file|
- old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ old_path, _ = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
old_path
end
@@ -116,6 +116,10 @@ class DiffFileEntity < Grape::Entity
project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
end
+ expose :viewer, using: DiffViewerEntity do |diff_file|
+ diff_file.rich_viewer || diff_file.simple_viewer
+ end
+
expose :replaced_view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
@@ -135,12 +139,12 @@ class DiffFileEntity < Grape::Entity
end
# Used for inline diffs
- expose :highlighted_diff_lines, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ expose :highlighted_diff_lines, using: DiffLineEntity, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
diff_file.diff_lines_for_serializer
end
# Used for parallel diffs
- expose :parallel_diff_lines, if: -> (diff_file, _) { diff_file.text? }
+ expose :parallel_diff_lines, using: DiffLineParallelEntity, if: -> (diff_file, _) { diff_file.text? }
def current_user
request.current_user
diff --git a/app/serializers/diff_line_entity.rb b/app/serializers/diff_line_entity.rb
new file mode 100644
index 00000000000..942714b7787
--- /dev/null
+++ b/app/serializers/diff_line_entity.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class DiffLineEntity < Grape::Entity
+ expose :line_code
+ expose :type
+ expose :old_line
+ expose :new_line
+ expose :text
+ expose :meta_positions, as: :meta_data
+
+ expose :rich_text do |line|
+ ERB::Util.html_escape(line.rich_text || line.text)
+ end
+end
diff --git a/app/serializers/diff_line_parallel_entity.rb b/app/serializers/diff_line_parallel_entity.rb
new file mode 100644
index 00000000000..0438a67d51b
--- /dev/null
+++ b/app/serializers/diff_line_parallel_entity.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class DiffLineParallelEntity < Grape::Entity
+ expose :left, using: DiffLineEntity
+ expose :right, using: DiffLineEntity
+end
diff --git a/app/serializers/diff_line_serializer.rb b/app/serializers/diff_line_serializer.rb
new file mode 100644
index 00000000000..7f1f2d9aa7c
--- /dev/null
+++ b/app/serializers/diff_line_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class DiffLineSerializer < BaseSerializer
+ entity DiffLineEntity
+end
diff --git a/app/serializers/diff_viewer_entity.rb b/app/serializers/diff_viewer_entity.rb
new file mode 100644
index 00000000000..27fba03cb3f
--- /dev/null
+++ b/app/serializers/diff_viewer_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class DiffViewerEntity < Grape::Entity
+ # Partial name refers directly to a Rails feature, let's avoid
+ # using this on the frontend.
+ expose :partial_name, as: :name
+end
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
index f75ace14d9c..b51e4a7e6d0 100644
--- a/app/serializers/diffs_entity.rb
+++ b/app/serializers/diffs_entity.rb
@@ -15,8 +15,13 @@ class DiffsEntity < Grape::Entity
merge_request&.target_branch
end
- expose :commit do |diffs|
- options[:commit]
+ expose :commit do |diffs, options|
+ CommitEntity.represent options[:commit], options.merge(
+ type: :full,
+ commit_url_params: { merge_request_iid: merge_request&.iid },
+ pipeline_ref: merge_request&.source_branch,
+ pipeline_project: merge_request&.source_project
+ )
end
expose :merge_request_diff, using: MergeRequestDiffEntity do |diffs|
@@ -35,13 +40,17 @@ class DiffsEntity < Grape::Entity
diffs_project_merge_request_path(merge_request&.project, merge_request)
end
+ # rubocop: disable CodeReuse/ActiveRecord
expose :added_lines do |diffs|
diffs.diff_files.sum(&:added_lines)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
expose :removed_lines do |diffs|
diffs.diff_files.sum(&:removed_lines)
end
+ # rubocop: enable CodeReuse/ActiveRecord
expose :render_overflow_warning do |diffs|
render_overflow_warning?(diffs.diff_files)
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index b8321037fa5..b6786a0d597 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -6,6 +6,7 @@ class DiscussionEntity < Grape::Entity
expose :id, :reply_id
expose :position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? }
+ expose :original_position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? }
expose :line_code, if: -> (d, _) { d.diff_discussion? }
expose :expanded?, as: :expanded
expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? }
@@ -26,7 +27,7 @@ class DiscussionEntity < Grape::Entity
expose :resolved?, as: :resolved
expose :resolved_by_push?, as: :resolved_by_push
- expose :resolved_by
+ expose :resolved_by, using: NoteUserEntity
expose :resolved_at
expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
@@ -43,7 +44,7 @@ class DiscussionEntity < Grape::Entity
project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion)
end
- expose :truncated_diff_lines, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) }
+ expose :truncated_diff_lines, using: DiffLineEntity, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) }
expose :image_diff_html, if: -> (d, _) { d.diff_discussion? && d.on_image? } do |discussion|
diff_file = discussion.diff_file
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index dc1686c30c4..598ce5f9e4f 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -29,6 +29,7 @@ class EnvironmentSerializer < BaseSerializer
private
+ # rubocop: disable CodeReuse/ActiveRecord
def itemize(resource)
items = resource.order('folder ASC')
.group('COALESCE(environment_type, name)')
@@ -46,4 +47,5 @@ class EnvironmentSerializer < BaseSerializer
Item.new(item.folder, item.size, environments[item.last_id])
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb
index f6804fe7f6a..20d7032c970 100644
--- a/app/serializers/group_child_entity.rb
+++ b/app/serializers/group_child_entity.rb
@@ -66,11 +66,13 @@ class GroupChildEntity < Grape::Entity
private
+ # rubocop: disable CodeReuse/ActiveRecord
def membership
return unless request.current_user
@membership ||= request.current_user.members.find_by(source: object)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def project?
object.is_a?(Project)
diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb
index c46c342ee5d..0e1bc9a6b3d 100644
--- a/app/serializers/group_entity.rb
+++ b/app/serializers/group_entity.rb
@@ -17,9 +17,11 @@ class GroupEntity < Grape::Entity
end
expose :permissions do
+ # rubocop: disable CodeReuse/ActiveRecord
expose :human_group_access do |group, options|
group.group_members.find_by(user_id: request.current_user)&.human_access
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
expose :edit_path do |group|
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index 7bc1d87dea5..0b19cb16955 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -24,10 +24,15 @@ class JobEntity < Grape::Entity
path_to(:play_namespace_project_job, build)
end
+ expose :unschedule_path, if: -> (*) { scheduled? } do |build|
+ path_to(:unschedule_namespace_project_job, build)
+ end
+
expose :playable?, as: :playable
+ expose :scheduled_at, if: -> (*) { scheduled? }
expose :created_at
expose :updated_at
- expose :detailed_status, as: :status, with: StatusEntity
+ expose :detailed_status, as: :status, with: DetailedStatusEntity
expose :callout_message, if: -> (*) { failed? && !build.script_failure? }
expose :recoverable, if: -> (*) { failed? }
@@ -47,6 +52,10 @@ class JobEntity < Grape::Entity
build.playable? && can?(request.current_user, :update_build, build)
end
+ def scheduled?
+ build.scheduled?
+ end
+
def detailed_status
build.detailed_status(request.current_user)
end
diff --git a/app/serializers/job_group_entity.rb b/app/serializers/job_group_entity.rb
index 0941a9d36be..0db7624b3f7 100644
--- a/app/serializers/job_group_entity.rb
+++ b/app/serializers/job_group_entity.rb
@@ -5,7 +5,7 @@ class JobGroupEntity < Grape::Entity
expose :name
expose :size
- expose :detailed_status, as: :status, with: StatusEntity
+ expose :detailed_status, as: :status, with: DetailedStatusEntity
expose :jobs, with: JobEntity
private
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index f55d448235a..380e8804f51 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -243,7 +243,7 @@ class MergeRequestWidgetEntity < IssuableEntity
def presenter(merge_request)
@presenters ||= {}
- @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user)
+ @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter
end
# Once SchedulePopulateMergeRequestMetricsWithEventsData fully runs,
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index daa5c24d0f5..c6d27817411 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -4,6 +4,12 @@ class NoteEntity < API::Entities::Note
include RequestAwareEntity
include NotesHelper
+ expose :id do |note|
+ # resource events are represented as notes too, but don't
+ # have ID, discussion ID is used for them instead
+ note.id ? note.id.to_s : note.discussion_id
+ end
+
expose :type
expose :author, using: NoteUserEntity
@@ -46,8 +52,8 @@ class NoteEntity < API::Entities::Note
expose :emoji_awardable?, as: :emoji_awardable
expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity
- expose :report_abuse_path do |note|
- new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note))
+ expose :report_abuse_path, if: -> (note, _) { note.author_id } do |note|
+ new_abuse_report_path(user_id: note.author_id, ref_url: Gitlab::UrlBuilder.build(note))
end
expose :noteable_note_url do |note|
diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb
index 3b56767f774..d78ad4af4dc 100644
--- a/app/serializers/pipeline_details_entity.rb
+++ b/app/serializers/pipeline_details_entity.rb
@@ -5,5 +5,6 @@ class PipelineDetailsEntity < PipelineEntity
expose :ordered_stages, as: :stages, using: StageEntity
expose :artifacts, using: BuildArtifactEntity
expose :manual_actions, using: BuildActionEntity
+ expose :scheduled_actions, using: BuildActionEntity
end
end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 6cf1925adda..aef838409e0 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -30,7 +30,7 @@ class PipelineEntity < Grape::Entity
end
expose :details do
- expose :detailed_status, as: :status, with: StatusEntity
+ expose :detailed_status, as: :status, with: DetailedStatusEntity
expose :duration
expose :finished_at
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 3205578b83e..7451433a841 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -4,6 +4,7 @@ class PipelineSerializer < BaseSerializer
include WithPagination
entity PipelineDetailsEntity
+ # rubocop: disable CodeReuse/ActiveRecord
def represent(resource, opts = {})
if resource.is_a?(ActiveRecord::Relation)
resource = resource.preload([
@@ -12,6 +13,7 @@ class PipelineSerializer < BaseSerializer
:cancelable_statuses,
:trigger_requests,
:manual_actions,
+ :scheduled_actions,
:artifacts,
{
pending_builds: :project,
@@ -33,6 +35,7 @@ class PipelineSerializer < BaseSerializer
super(resource, opts)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def represent_status(resource)
return {} unless resource.present?
diff --git a/app/serializers/project_note_entity.rb b/app/serializers/project_note_entity.rb
index d7c4d0aacc6..f6cdea1d8b5 100644
--- a/app/serializers/project_note_entity.rb
+++ b/app/serializers/project_note_entity.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class ProjectNoteEntity < NoteEntity
- expose :human_access do |note|
+ expose :human_access, if: -> (note, _) { note.project.present? } do |note|
note.project.team.human_max_access(note.author_id)
end
@@ -9,7 +9,7 @@ class ProjectNoteEntity < NoteEntity
toggle_award_emoji_project_note_path(note.project, note.id)
end
- expose :path do |note|
+ expose :path, if: -> (note, _) { note.id } do |note|
project_note_path(note.project, note)
end
diff --git a/app/serializers/runner_entity.rb b/app/serializers/runner_entity.rb
index 04ec80e0efa..97e5b336a35 100644
--- a/app/serializers/runner_entity.rb
+++ b/app/serializers/runner_entity.rb
@@ -5,8 +5,7 @@ class RunnerEntity < Grape::Entity
expose :id, :description
- expose :edit_path,
- if: -> (*) { can?(request.current_user, :admin_build, project) && runner.project_type? } do |runner|
+ expose :edit_path, if: -> (*) { can_edit_runner? } do |runner|
edit_project_runner_path(project, runner)
end
@@ -17,4 +16,8 @@ class RunnerEntity < Grape::Entity
def project
request.project
end
+
+ def can_edit_runner?
+ can?(request.current_user, :update_runner, runner) && runner.project_type?
+ end
end
diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb
index 00e6d32ee3a..029dd3d0684 100644
--- a/app/serializers/stage_entity.rb
+++ b/app/serializers/stage_entity.rb
@@ -19,7 +19,13 @@ class StageEntity < Grape::Entity
latest_statuses
end
- expose :detailed_status, as: :status, with: StatusEntity
+ expose :retried,
+ if: -> (_, opts) { opts[:retried] },
+ with: JobEntity do |stage|
+ retried_statuses
+ end
+
+ expose :detailed_status, as: :status, with: DetailedStatusEntity
expose :path do |stage|
project_pipeline_path(
@@ -48,9 +54,19 @@ class StageEntity < Grape::Entity
@grouped_statuses ||= stage.statuses.latest_ordered.group_by(&:status)
end
+ def grouped_retried_statuses
+ @grouped_retried_statuses ||= stage.statuses.retried_ordered.group_by(&:status)
+ end
+
def latest_statuses
HasStatus::ORDERED_STATUSES.map do |ordered_status|
grouped_statuses.fetch(ordered_status, [])
end.flatten
end
+
+ def retried_statuses
+ HasStatus::ORDERED_STATUSES.map do |ordered_status|
+ grouped_retried_statuses.fetch(ordered_status, [])
+ end.flatten
+ end
end
diff --git a/app/serializers/trigger_variable_entity.rb b/app/serializers/trigger_variable_entity.rb
new file mode 100644
index 00000000000..56203113631
--- /dev/null
+++ b/app/serializers/trigger_variable_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class TriggerVariableEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :key, :value, :public
+end
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 19cf34e2ac4..2e4643ed668 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -11,11 +11,19 @@ module ApplicationSettings
params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id
end
+ if usage_stats_updated? && !params.delete(:skip_usage_stats_user)
+ params[:usage_stats_set_by_user_id] = current_user.id
+ end
+
@application_setting.update(@params)
end
private
+ def usage_stats_updated?
+ params.key?(:usage_ping_enabled) || params.key?(:version_check_enabled)
+ end
+
def update_terms(terms)
return unless terms.present?
diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb
index 7db90c0b3c6..3d88c4f064e 100644
--- a/app/services/applications/create_service.rb
+++ b/app/services/applications/create_service.rb
@@ -2,10 +2,12 @@
module Applications
class CreateService
+ # rubocop: disable CodeReuse/ActiveRecord
def initialize(current_user, params)
@current_user = current_user
@params = params.except(:ip_address)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def execute(request)
Doorkeeper::Application.create(@params)
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
index 4caf5ffa3cb..1b796cef3e2 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -9,7 +9,7 @@ module Boards
private
def can_create_board?
- parent.boards.size == 0
+ parent.boards.empty?
end
def create_board!
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 0db1418b37a..0b69661bbd0 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -9,6 +9,7 @@ module Boards
fetch_issues.order_by_position_and_priority
end
+ # rubocop: disable CodeReuse/ActiveRecord
def metadata
keys = metadata_fields.keys
columns = metadata_fields.values_at(*keys).join(', ')
@@ -16,6 +17,7 @@ module Boards
Hash[keys.zip(results.flatten)]
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
@@ -24,6 +26,7 @@ module Boards
end
# We memoize the query here since the finder methods we use are quite complex. This does not memoize the result of the query.
+ # rubocop: disable CodeReuse/ActiveRecord
def fetch_issues
strong_memoize(:fetch_issues) do
issues = IssuesFinder.new(current_user, filter_params).execute
@@ -31,6 +34,7 @@ module Boards
filter(issues).reorder(nil)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def filter(issues)
issues = without_board_labels(issues) unless list&.movable? || list&.closed?
@@ -52,6 +56,7 @@ module Boards
set_parent
set_state
set_scope
+ set_non_archived
params
end
@@ -72,24 +77,36 @@ module Boards
params[:include_subgroups] = board.group_board?
end
+ def set_non_archived
+ params[:non_archived] = parent.is_a?(Group)
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
def board_label_ids
@board_label_ids ||= board.lists.movable.pluck(:label_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def without_board_labels(issues)
return issues unless board_label_ids.any?
issues.where.not('EXISTS (?)', issues_label_links.limit(1))
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def issues_label_links
LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id").where(label_id: board_label_ids)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def with_list_label(issues)
issues.where('EXISTS (?)', LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
.where("label_links.label_id = ?", list.label_id).limit(1))
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 6fd8a23b2a1..7dd87034410 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -21,13 +21,17 @@ module Boards
moving_from_list != moving_to_list
end
+ # rubocop: disable CodeReuse/ActiveRecord
def moving_from_list
@moving_from_list ||= board.lists.find_by(id: params[:from_list_id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def moving_to_list
@moving_to_list ||= board.lists.find_by(id: params[:to_list_id])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def update(issue)
::Issues::UpdateService.new(issue.project, current_user, issue_params(issue)).execute(issue)
@@ -61,6 +65,7 @@ module Boards
[moving_to_list.label_id].compact
end
+ # rubocop: disable CodeReuse/ActiveRecord
def remove_label_ids
label_ids =
if moving_to_list.movable?
@@ -73,6 +78,7 @@ module Boards
Array(label_ids).compact
end
+ # rubocop: enable CodeReuse/ActiveRecord
def move_between_ids
return unless params[:move_after_id] || params[:move_before_id]
diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb
index e12d4f46e19..609c430caed 100644
--- a/app/services/boards/lists/destroy_service.rb
+++ b/app/services/boards/lists/destroy_service.rb
@@ -18,10 +18,12 @@ module Boards
attr_reader :board
+ # rubocop: disable CodeReuse/ActiveRecord
def decrement_higher_lists(list)
board.lists.movable.where('position > ?', list.position)
.update_all('position = position - 1')
end
+ # rubocop: enable CodeReuse/ActiveRecord
def remove_list(list)
list.destroy
diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb
index 27a36051662..93f81837d1a 100644
--- a/app/services/boards/lists/move_service.rb
+++ b/app/services/boards/lists/move_service.rb
@@ -34,17 +34,21 @@ module Boards
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def decrement_intermediate_lists
board.lists.movable.where('position > ?', old_position)
.where('position <= ?', new_position)
.update_all('position = position - 1')
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def increment_intermediate_lists
board.lists.movable.where('position >= ?', new_position)
.where('position < ?', old_position)
.update_all('position = position + 1')
end
+ # rubocop: enable CodeReuse/ActiveRecord
def update_list_position(list)
list.update_attribute(:position, new_position)
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb
index 854b191c45c..c91738fa4c7 100644
--- a/app/services/chat_names/find_user_service.rb
+++ b/app/services/chat_names/find_user_service.rb
@@ -17,6 +17,7 @@ module ChatNames
private
+ # rubocop: disable CodeReuse/ActiveRecord
def find_chat_name
ChatName.find_by(
service: @service,
@@ -24,5 +25,6 @@ module ChatNames
chat_id: @params[:user_id]
)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/ci/compare_test_reports_service.rb b/app/services/ci/compare_test_reports_service.rb
index ec25e934a27..2293f95f56b 100644
--- a/app/services/ci/compare_test_reports_service.rb
+++ b/app/services/ci/compare_test_reports_service.rb
@@ -3,6 +3,7 @@
module Ci
class CompareTestReportsService < ::BaseService
def execute(base_pipeline, head_pipeline)
+ # rubocop: disable CodeReuse/Serializer
comparer = Gitlab::Ci::Reports::TestReportsComparer
.new(base_pipeline&.test_reports, head_pipeline.test_reports)
@@ -19,6 +20,7 @@ module Ci
key: key(base_pipeline, head_pipeline),
status_reason: e.message
}
+ # rubocop: enable CodeReuse/Serializer
end
def latest?(base_pipeline, head_pipeline, data)
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 85df8bcff8c..92a8438ab2f 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -65,6 +65,7 @@ module Ci
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def auto_cancelable_pipelines
project.pipelines
.where(ref: pipeline.ref)
@@ -72,6 +73,7 @@ module Ci
.where.not(sha: project.commit(pipeline.ref).try(:id))
.created_or_pending
end
+ # rubocop: enable CodeReuse/ActiveRecord
def pipeline_created_counter
@pipeline_created_counter ||= Gitlab::Metrics
@@ -84,8 +86,10 @@ module Ci
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def related_merge_requests
pipeline.project.source_of_merge_requests.opened.where(source_branch: pipeline.ref)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/ci/enqueue_build_service.rb b/app/services/ci/enqueue_build_service.rb
deleted file mode 100644
index 8140651d980..00000000000
--- a/app/services/ci/enqueue_build_service.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-module Ci
- class EnqueueBuildService < BaseService
- def execute(build)
- build.enqueue
- end
- end
-end
diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb
index 3d0e39d1b9f..cbb3a2e4709 100644
--- a/app/services/ci/ensure_stage_service.rb
+++ b/app/services/ci/ensure_stage_service.rb
@@ -38,9 +38,11 @@ module Ci
EOS
end
+ # rubocop: disable CodeReuse/ActiveRecord
def find_stage
@build.pipeline.stages.find_by(name: @build.stage)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def create_stage
Ci::Stage.create!(name: @build.stage,
diff --git a/app/services/ci/extract_sections_from_build_trace_service.rb b/app/services/ci/extract_sections_from_build_trace_service.rb
index 693f6d55be3..97f9918fdb7 100644
--- a/app/services/ci/extract_sections_from_build_trace_service.rb
+++ b/app/services/ci/extract_sections_from_build_trace_service.rb
@@ -11,11 +11,13 @@ module Ci
private
+ # rubocop: disable CodeReuse/ActiveRecord
def find_or_create_name(name)
project.build_trace_section_names.find_or_create_by!(name: name)
rescue ActiveRecord::RecordInvalid
project.build_trace_section_names.find_by!(name: name)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def extract_sections(build)
build.trace.extract_sections.map do |attr|
diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb
deleted file mode 100644
index 15eda56cac6..00000000000
--- a/app/services/ci/fetch_kubernetes_token_service.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-##
-# TODO:
-# Almost components in this class were copied from app/models/project_services/kubernetes_service.rb
-# We should dry up those classes not to repeat the same code.
-# Maybe we should have a special facility (e.g. lib/kubernetes_api) to maintain all Kubernetes API caller.
-module Ci
- class FetchKubernetesTokenService
- attr_reader :api_url, :ca_pem, :username, :password
-
- def initialize(api_url, ca_pem, username, password)
- @api_url = api_url
- @ca_pem = ca_pem
- @username = username
- @password = password
- end
-
- def execute
- read_secrets.each do |secret|
- name = secret.dig('metadata', 'name')
- if /default-token/ =~ name
- token_base64 = secret.dig('data', 'token')
- return Base64.decode64(token_base64) if token_base64
- end
- end
-
- nil
- end
-
- private
-
- def read_secrets
- kubeclient = build_kubeclient!
-
- kubeclient.get_secrets.as_json
- rescue Kubeclient::HttpError => err
- raise err unless err.error_code == 404
-
- []
- end
-
- def build_kubeclient!(api_path: 'api', api_version: 'v1')
- raise "Incomplete settings" unless api_url && username && password
-
- ::Kubeclient::Client.new(
- join_api_url(api_path),
- api_version,
- auth_options: { username: username, password: password },
- ssl_options: kubeclient_ssl_options,
- http_proxy_uri: ENV['http_proxy']
- )
- end
-
- def join_api_url(api_path)
- url = URI.parse(api_url)
- prefix = url.path.sub(%r{/+\z}, '')
-
- url.path = [prefix, api_path].join("/")
-
- url.to_s
- end
-
- def kubeclient_ssl_options
- opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
-
- if ca_pem.present?
- opts[:cert_store] = OpenSSL::X509::Store.new
- opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
- end
-
- opts
- end
- end
-end
diff --git a/app/services/ci/process_build_service.rb b/app/services/ci/process_build_service.rb
new file mode 100644
index 00000000000..d9f8e7cb452
--- /dev/null
+++ b/app/services/ci/process_build_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Ci
+ class ProcessBuildService < BaseService
+ def execute(build, current_status)
+ if valid_statuses_for_when(build.when).include?(current_status)
+ if build.schedulable?
+ build.schedule
+ elsif build.action?
+ build.actionize
+ else
+ enqueue(build)
+ end
+
+ true
+ else
+ build.skip
+ false
+ end
+ end
+
+ private
+
+ def enqueue(build)
+ build.enqueue
+ end
+
+ def valid_statuses_for_when(value)
+ case value
+ when 'on_success'
+ %w[success skipped]
+ when 'on_failure'
+ %w[failed]
+ when 'always'
+ %w[success failed skipped]
+ when 'manual'
+ %w[success skipped]
+ when 'delayed'
+ %w[success skipped]
+ else
+ []
+ end
+ end
+ end
+end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index cafee76a33c..446188347df 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -24,53 +24,35 @@ module Ci
def process_stage(index)
current_status = status_for_prior_stages(index)
- return if HasStatus::BLOCKED_STATUS == current_status
+ return if HasStatus::BLOCKED_STATUS.include?(current_status)
if HasStatus::COMPLETED_STATUSES.include?(current_status)
created_builds_in_stage(index).select do |build|
Gitlab::OptimisticLocking.retry_lock(build) do |subject|
- process_build(subject, current_status)
+ Ci::ProcessBuildService.new(project, @user)
+ .execute(build, current_status)
end
end
end
end
- def process_build(build, current_status)
- if valid_statuses_for_when(build.when).include?(current_status)
- build.action? ? build.actionize : enqueue_build(build)
- true
- else
- build.skip
- false
- end
- end
-
- def valid_statuses_for_when(value)
- case value
- when 'on_success'
- %w[success skipped]
- when 'on_failure'
- %w[failed]
- when 'always'
- %w[success failed skipped]
- when 'manual'
- %w[success skipped]
- else
- []
- end
- end
-
+ # rubocop: disable CodeReuse/ActiveRecord
def status_for_prior_stages(index)
pipeline.builds.where('stage_idx < ?', index).latest.status || 'success'
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def stage_indexes_of_created_builds
created_builds.order(:stage_idx).pluck('distinct stage_idx')
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def created_builds_in_stage(index)
created_builds.where(stage_idx: index)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def created_builds
pipeline.builds.created
@@ -80,6 +62,7 @@ module Ci
# This replicates what is db/post_migrate/20170416103934_upate_retried_for_ci_build.rb
# and ensures that functionality will not be broken before migration is run
# this updates only when there are data that needs to be updated, there are two groups with no retried flag
+ # rubocop: disable CodeReuse/ActiveRecord
def update_retried
# find the latest builds for each name
latest_statuses = pipeline.statuses.latest
@@ -93,9 +76,6 @@ module Ci
.where.not(id: latest_statuses.map(&:first))
.update_all(retried: true) if latest_statuses.any?
end
-
- def enqueue_build(build)
- Ci::EnqueueBuildService.new(project, @user).execute(build)
- end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 11f85627faf..5a7be921389 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -15,6 +15,7 @@ module Ci
@runner = runner
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(params = {})
builds =
if runner.instance_type?
@@ -63,6 +64,7 @@ module Ci
register_failure
Result.new(nil, valid)
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
@@ -84,6 +86,7 @@ module Ci
true
end
+ # rubocop: disable CodeReuse/ActiveRecord
def builds_for_shared_runner
new_builds.
# don't run projects which have not enabled shared runners and builds
@@ -97,11 +100,15 @@ module Ci
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
.order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def builds_for_project_runner
new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC')
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def builds_for_group_runner
# Workaround for weird Rails bug, that makes `runner.groups.to_sql` to return `runner_id = NULL`
groups = ::Group.joins(:runner_namespaces).merge(runner.runner_namespaces)
@@ -113,11 +120,14 @@ module Ci
.without_deleted
new_builds.where(project: projects).order('id ASC')
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.instance_type)
.group(:project_id).select(:project_id, 'count(*) AS running_builds')
end
+ # rubocop: enable CodeReuse/ActiveRecord
def new_builds
builds = Ci::Build.pending.unstarted
@@ -138,6 +148,7 @@ module Ci
attempt_counter.increment
end
+ # rubocop: disable CodeReuse/ActiveRecord
def jobs_running_for_project(job)
return '+Inf' unless runner.instance_type?
@@ -146,6 +157,7 @@ module Ci
.limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
end
+ # rubocop: enable CodeReuse/ActiveRecord
def failed_attempt_counter
@failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 6ceb59e4780..218f1e63d08 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -19,6 +19,7 @@ module Ci
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def reprocess!(build)
unless can?(current_user, :update_build, build)
raise Gitlab::Access::AccessDeniedError
@@ -41,5 +42,6 @@ module Ci
project.builds.create!(Hash[attributes])
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/ci/run_scheduled_build_service.rb b/app/services/ci/run_scheduled_build_service.rb
new file mode 100644
index 00000000000..8e4a628296f
--- /dev/null
+++ b/app/services/ci/run_scheduled_build_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class RunScheduledBuildService < ::BaseService
+ def execute(build)
+ unless can?(current_user, :update_build, build)
+ raise Gitlab::Access::AccessDeniedError
+ end
+
+ build.enqueue_scheduled!
+ end
+ end
+end
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
index 35f5cff0e0c..5017fa093f3 100644
--- a/app/services/clusters/applications/check_installation_progress_service.rb
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -14,8 +14,8 @@ module Clusters
else
check_timeout
end
- rescue Kubeclient::HttpError => ke
- app.make_errored!("Kubernetes error: #{ke.message}") unless app.errored?
+ rescue Kubeclient::HttpError
+ app.make_errored!("Kubernetes error") unless app.errored?
end
private
@@ -27,7 +27,7 @@ module Clusters
end
def on_failed
- app.make_errored!(installation_errors || 'Installation silently failed')
+ app.make_errored!('Installation failed')
ensure
remove_installation_pod
end
diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb
index 7e3c0e77a83..dd8d2ed5eb6 100644
--- a/app/services/clusters/applications/install_service.rb
+++ b/app/services/clusters/applications/install_service.rb
@@ -12,10 +12,10 @@ module Clusters
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
- rescue Kubeclient::HttpError => ke
- app.make_errored!("Kubernetes error: #{ke.message}")
- rescue StandardError => e
- app.make_errored!("Can't start installation process. #{e.message}")
+ rescue Kubeclient::HttpError
+ app.make_errored!("Kubernetes error.")
+ rescue StandardError
+ app.make_errored!("Can't start installation process.")
end
end
end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index 264419501dc..3ae0a4a19d0 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -9,17 +9,24 @@ module Clusters
@provider = provider
configure_provider
+ create_gitlab_service_account!
configure_kubernetes
cluster.save!
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ rescue Kubeclient::HttpError => e
+ provider.make_errored!("Failed to run Kubeclient: #{e.message}")
rescue ActiveRecord::RecordInvalid => e
provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}")
end
private
+ def create_gitlab_service_account!
+ Clusters::Gcp::Kubernetes::CreateServiceAccountService.new(kube_client, rbac: create_rbac_cluster?).execute
+ end
+
def configure_provider
provider.endpoint = gke_cluster.endpoint
provider.status_event = :make_created
@@ -32,15 +39,54 @@ module Clusters
ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
username: gke_cluster.master_auth.username,
password: gke_cluster.master_auth.password,
+ authorization_type: authorization_type,
token: request_kubernetes_token)
end
def request_kubernetes_token
- Ci::FetchKubernetesTokenService.new(
+ Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(kube_client).execute
+ end
+
+ def authorization_type
+ create_rbac_cluster? ? 'rbac' : 'abac'
+ end
+
+ def create_rbac_cluster?
+ !provider.legacy_abac?
+ end
+
+ def kube_client
+ @kube_client ||= build_kube_client!(
'https://' + gke_cluster.endpoint,
Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
gke_cluster.master_auth.username,
- gke_cluster.master_auth.password).execute
+ gke_cluster.master_auth.password,
+ api_groups: ['api', 'apis/rbac.authorization.k8s.io']
+ )
+ end
+
+ def build_kube_client!(api_url, ca_pem, username, password, api_groups: ['api'], api_version: 'v1')
+ 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']
+ )
+ end
+
+ def kubeclient_ssl_options(ca_pem)
+ opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
+
+ if ca_pem.present?
+ opts[:cert_store] = OpenSSL::X509::Store.new
+ opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+ end
+
+ opts
end
def gke_cluster
diff --git a/app/services/clusters/gcp/kubernetes.rb b/app/services/clusters/gcp/kubernetes.rb
new file mode 100644
index 00000000000..d014d73b3e8
--- /dev/null
+++ b/app/services/clusters/gcp/kubernetes.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Gcp
+ module Kubernetes
+ SERVICE_ACCOUNT_NAME = 'gitlab'
+ SERVICE_ACCOUNT_NAMESPACE = 'default'
+ SERVICE_ACCOUNT_TOKEN_NAME = 'gitlab-token'
+ CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin'
+ CLUSTER_ROLE_NAME = 'cluster-admin'
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb
new file mode 100644
index 00000000000..d17744591e6
--- /dev/null
+++ b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Gcp
+ module Kubernetes
+ class CreateServiceAccountService
+ attr_reader :kubeclient, :rbac
+
+ def initialize(kubeclient, rbac:)
+ @kubeclient = kubeclient
+ @rbac = rbac
+ end
+
+ def execute
+ kubeclient.create_service_account(service_account_resource)
+ kubeclient.create_secret(service_account_token_resource)
+ kubeclient.create_cluster_role_binding(cluster_role_binding_resource) if rbac
+ end
+
+ private
+
+ def service_account_resource
+ Gitlab::Kubernetes::ServiceAccount.new(service_account_name, service_account_namespace).generate
+ end
+
+ def service_account_token_resource
+ Gitlab::Kubernetes::ServiceAccountToken.new(
+ SERVICE_ACCOUNT_TOKEN_NAME, service_account_name, service_account_namespace).generate
+ end
+
+ def cluster_role_binding_resource
+ subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }]
+
+ Gitlab::Kubernetes::ClusterRoleBinding.new(
+ CLUSTER_ROLE_BINDING_NAME,
+ CLUSTER_ROLE_NAME,
+ subjects
+ ).generate
+ end
+
+ def service_account_name
+ SERVICE_ACCOUNT_NAME
+ end
+
+ def service_account_namespace
+ SERVICE_ACCOUNT_NAMESPACE
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb
new file mode 100644
index 00000000000..9e09345c8dc
--- /dev/null
+++ b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Gcp
+ module Kubernetes
+ class FetchKubernetesTokenService
+ attr_reader :kubeclient
+
+ def initialize(kubeclient)
+ @kubeclient = kubeclient
+ end
+
+ def execute
+ token_base64 = get_secret&.dig('data', 'token')
+ Base64.decode64(token_base64) if token_base64
+ end
+
+ private
+
+ def get_secret
+ kubeclient.get_secret(SERVICE_ACCOUNT_TOKEN_NAME, SERVICE_ACCOUNT_NAMESPACE).as_json
+ rescue Kubeclient::HttpError => err
+ raise err unless err.error_code == 404
+
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb
index ab1bf9c64f6..80040511ec2 100644
--- a/app/services/clusters/gcp/provision_service.rb
+++ b/app/services/clusters/gcp/provision_service.rb
@@ -27,7 +27,9 @@ module Clusters
provider.zone,
provider.cluster.name,
provider.num_nodes,
- machine_type: provider.machine_type)
+ machine_type: provider.machine_type,
+ legacy_abac: provider.legacy_abac
+ )
unless operation.status == 'PENDING' || operation.status == 'RUNNING'
return provider.make_errored!("Operation status is unexpected; #{operation.status_message}")
diff --git a/app/services/cohorts_service.rb b/app/services/cohorts_service.rb
index 7a14e97f749..6d466c2fc9c 100644
--- a/app/services/cohorts_service.rb
+++ b/app/services/cohorts_service.rb
@@ -78,6 +78,7 @@ class CohortsService
# created_at_month can never be nil, but last_activity_on_month can (when a
# user has never logged in, just been created). This covers the last
# MONTHS_INCLUDED months.
+ # rubocop: disable CodeReuse/ActiveRecord
def counts_by_month
@counts_by_month ||=
begin
@@ -91,6 +92,7 @@ class CohortsService
.count
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def column_to_date(column)
if Gitlab::Database.postgresql?
diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb
index 1563ed965df..f0e9862ca30 100644
--- a/app/services/concerns/issues/resolve_discussions.rb
+++ b/app/services/concerns/issues/resolve_discussions.rb
@@ -13,12 +13,14 @@ module Issues
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
+ # rubocop: disable CodeReuse/ActiveRecord
def merge_request_to_resolve_discussions_of
strong_memoize(:merge_request_to_resolve_discussions_of) do
MergeRequestsFinder.new(current_user, project_id: project.id)
.find_by(iid: merge_request_to_resolve_discussions_of_iid)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def discussions_to_resolve
return [] unless merge_request_to_resolve_discussions_of
diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb
index 09c68390007..8d1fdbe11c3 100644
--- a/app/services/create_release_service.rb
+++ b/app/services/create_release_service.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class CreateReleaseService < BaseService
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(tag_name, release_description)
repository = project.repository
existing_tag = repository.find_tag(tag_name)
@@ -21,6 +22,7 @@ class CreateReleaseService < BaseService
error('Tag does not exist', 404)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def success(release)
super().merge(release: release)
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
index ff3e4783fe3..ced87a1c37a 100644
--- a/app/services/delete_merged_branches_service.rb
+++ b/app/services/delete_merged_branches_service.rb
@@ -21,10 +21,12 @@ class DeleteMergedBranchesService < BaseService
private
+ # rubocop: disable CodeReuse/ActiveRecord
def merge_request_branch_names
# reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY
source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch)
target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch)
(source_names + target_names).uniq
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb
index ba7b689a9af..988215ffc78 100644
--- a/app/services/emails/base_service.rb
+++ b/app/services/emails/base_service.rb
@@ -2,6 +2,8 @@
module Emails
class BaseService
+ attr_reader :current_user
+
def initialize(current_user, params = {})
@current_user, @params = current_user, params.dup
@user = params.delete(:user)
diff --git a/app/services/emails/create_service.rb b/app/services/emails/create_service.rb
index acf575e24e5..56925a724fe 100644
--- a/app/services/emails/create_service.rb
+++ b/app/services/emails/create_service.rb
@@ -3,7 +3,12 @@
module Emails
class CreateService < ::Emails::BaseService
def execute(extra_params = {})
- @user.emails.create(@params.merge(extra_params))
+ skip_confirmation = @params.delete(:skip_confirmation)
+
+ email = @user.emails.create(@params.merge(extra_params))
+
+ email&.confirm if skip_confirmation && current_user.admin?
+ email
end
end
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 025f093a428..39e614d6569 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -7,8 +7,10 @@ module Files
def initialize(*args)
super
- @author_email = params[:author_email]
- @author_name = params[:author_name]
+ git_user = Gitlab::Git::User.from_gitlab(current_user) if current_user.present?
+
+ @author_email = params[:author_email] || git_user&.email
+ @author_name = params[:author_name] || git_user&.name
@commit_message = params[:commit_message]
@last_commit_sha = params[:last_commit_sha]
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index 08088f8c592..c9d3ee31d82 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -2,7 +2,7 @@
module Files
class MultiService < Files::BaseService
- UPDATE_FILE_ACTIONS = %w(update move delete).freeze
+ UPDATE_FILE_ACTIONS = %w(update move delete chmod).freeze
def create_commit!
transformer = Lfs::FileTransformer.new(project, @branch_name)
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 26e90e8cf8c..f1883877d56 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -94,6 +94,7 @@ class GitPushService < BaseService
ProjectCacheWorker.perform_async(project.id, types, [:commit_count, :repository_size])
end
+ # rubocop: disable CodeReuse/ActiveRecord
def update_signatures
commit_shas = last_pushed_commits.map(&:sha)
@@ -108,6 +109,7 @@ class GitPushService < BaseService
CreateGpgSignatureWorker.perform_async(commit_shas, project.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Schedules processing of commit messages.
def process_commit_messages
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index 93d84bd8a9c..641111aeadc 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -9,6 +9,7 @@ module Groups
Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
group.prepare_for_destroy
@@ -30,5 +31,6 @@ module Groups
group.destroy
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index ea7576077f3..5efa746dfb9 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -64,9 +64,11 @@ module Groups
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def namespace_with_same_path?
Namespace.exists?(path: @group.path, parent: @new_parent_group)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def update_group_attributes
if @new_parent_group && @new_parent_group.visibility_level < @group.visibility_level
@@ -78,6 +80,7 @@ module Groups
@group.save!
end
+ # rubocop: disable CodeReuse/ActiveRecord
def update_children_and_projects_visibility
descendants = @group.descendants.where("visibility_level > ?", @new_parent_group.visibility_level)
@@ -90,6 +93,7 @@ module Groups
.where("visibility_level > ?", @new_parent_group.visibility_level)
.update_all(visibility_level: @new_parent_group.visibility_level)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def raise_transfer_error(message)
raise TransferError, ERROR_MESSAGES[message]
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
index e75a951944e..3ecb51b60d0 100644
--- a/app/services/import_export_clean_up_service.rb
+++ b/app/services/import_export_clean_up_service.rb
@@ -26,10 +26,12 @@ class ImportExportCleanUpService
Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete))
end
+ # rubocop: disable CodeReuse/ActiveRecord
def clean_up_export_object_files
ImportExportUpload.where('updated_at < ?', mmin.minutes.ago).each do |upload|
upload.remove_export_file!
upload.save!
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
index 051d5ba881d..c4beddf2294 100644
--- a/app/services/issuable/bulk_update_service.rb
+++ b/app/services/issuable/bulk_update_service.rb
@@ -2,6 +2,7 @@
module Issuable
class BulkUpdateService < IssuableBaseService
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(type)
model_class = type.classify.constantize
update_class = type.classify.pluralize.constantize::UpdateService
@@ -28,6 +29,7 @@ module Issuable
success: !items.count.zero?
}
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index 028b350ca07..765de9c66b0 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -17,6 +17,7 @@ module Issuable
create_labels_note(old_labels) if issuable.labels != old_labels
create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked')
create_milestone_note if issuable.previous_changes.include?('milestone_id')
+ create_due_date_note if issuable.previous_changes.include?('due_date')
end
private
@@ -55,7 +56,9 @@ module Issuable
added_labels = issuable.labels - old_labels
removed_labels = old_labels - issuable.labels
- SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels)
+ ResourceEvents::ChangeLabelsService
+ .new(issuable, current_user)
+ .execute(added_labels: added_labels, removed_labels: removed_labels)
end
def create_title_change_note(old_title)
@@ -88,6 +91,10 @@ module Issuable
SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
end
+ def create_due_date_note
+ SystemNoteService.change_due_date(issuable, issuable.project, current_user, issuable.due_date)
+ end
+
def create_discussion_lock_note
SystemNoteService.discussion_lock(issuable, current_user)
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 7d60c65bb79..3e8b9f84042 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -68,11 +68,13 @@ class IssuableBaseService < BaseService
find_or_create_label_ids
end
+ # rubocop: disable CodeReuse/ActiveRecord
def filter_labels_in_param(key)
return if params[key].to_a.empty?
params[key] = available_labels.where(id: params[key]).pluck(:id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def find_or_create_label_ids
labels = params.delete(:labels)
@@ -129,28 +131,19 @@ class IssuableBaseService < BaseService
params.merge!(command_params)
end
- def create_issuable(issuable, attributes, label_ids:)
- issuable.with_transaction_returning_status do
- if issuable.save
- issuable.update(label_ids: label_ids)
- end
- end
- end
-
def create(issuable)
handle_quick_actions_on_create(issuable)
filter_params(issuable)
params.delete(:state_event)
params[:author] ||= current_user
-
- label_ids = process_label_ids(params)
+ params[:label_ids] = issuable.label_ids.to_a + process_label_ids(params)
issuable.assign_attributes(params)
before_create(issuable)
- if params.present? && create_issuable(issuable, params, label_ids: label_ids)
+ if issuable.save
after_create(issuable)
execute_hooks(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees)
@@ -256,6 +249,7 @@ class IssuableBaseService < BaseService
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def change_todo(issuable)
case params.delete(:todo_event)
when 'add'
@@ -265,6 +259,7 @@ class IssuableBaseService < BaseService
todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def toggle_award(issuable)
award = params.delete(:emoji_award)
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 25389a946bb..ef08adf4f92 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -31,6 +31,7 @@ module Issues
issue.project.execute_services(issue_data, hooks_scope)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def filter_assignee(issuable)
return if params[:assignee_ids].blank?
@@ -48,6 +49,7 @@ module Issues
params.delete(:assignee_ids)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def update_project_counter_caches?(issue)
super || issue.confidential_changed?
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index 841bce9949e..d2bdba1e627 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -36,6 +36,7 @@ module Issues
def update_new_issue
rewrite_notes
+ copy_resource_label_events
rewrite_issue_award_emoji
add_note_moved_from
end
@@ -57,6 +58,7 @@ module Issues
CreateService.new(@new_project, @current_user, new_params).execute
end
+ # rubocop: disable CodeReuse/ActiveRecord
def cloneable_label_ids
params = {
project_id: @new_project.id,
@@ -66,6 +68,7 @@ module Issues
LabelsFinder.new(current_user, params).execute.pluck(:id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def cloneable_milestone_id
title = @old_issue.milestone&.title
@@ -96,6 +99,20 @@ module Issues
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
+ def copy_resource_label_events
+ @old_issue.resource_label_events.find_in_batches do |batch|
+ events = batch.map do |event|
+ event.attributes
+ .except('id', 'reference', 'reference_html')
+ .merge('issue_id' => @new_issue.id, 'action' => ResourceLabelEvent.actions[event.action])
+ end
+
+ Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, events)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def rewrite_issue_award_emoji
rewrite_award_emoji(@old_issue, @new_issue)
end
diff --git a/app/services/issues/referenced_merge_requests_service.rb b/app/services/issues/referenced_merge_requests_service.rb
index 40d78502697..a69cd324b1e 100644
--- a/app/services/issues/referenced_merge_requests_service.rb
+++ b/app/services/issues/referenced_merge_requests_service.rb
@@ -2,6 +2,7 @@
module Issues
class ReferencedMergeRequestsService < Issues::BaseService
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(issue)
referenced = referenced_merge_requests(issue)
closed_by = closed_by_merge_requests(issue)
@@ -12,6 +13,7 @@ module Issues
[sort_by_iid(referenced), sort_by_iid(closed_by)]
end
+ # rubocop: enable CodeReuse/ActiveRecord
def referenced_merge_requests(issue)
merge_requests = extract_merge_requests(issue)
@@ -29,6 +31,7 @@ module Issues
)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def closed_by_merge_requests(issue)
return [] unless issue.open?
@@ -39,6 +42,7 @@ module Issues
ids = MergeRequestsClosingIssues.where(merge_request_id: merge_requests.map(&:id), issue_id: issue.id).pluck(:merge_request_id)
merge_requests.select { |mr| mr.id.in?(ids) }
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
@@ -54,10 +58,12 @@ module Issues
ext.merge_requests
end
+ # rubocop: disable CodeReuse/ActiveRecord
def issue_notes(issue)
@issue_notes ||= {}
@issue_notes[issue] ||= issue.notes.includes(:author)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def sort_by_iid(merge_requests)
Gitlab::IssuableSorter.sort(project, merge_requests) { |mr| mr.iid.to_s }
diff --git a/app/services/issues/related_branches_service.rb b/app/services/issues/related_branches_service.rb
new file mode 100644
index 00000000000..76af482b7ac
--- /dev/null
+++ b/app/services/issues/related_branches_service.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# This service fetches all branches containing the current issue's ID, except for
+# those with a merge request open referencing the current issue.
+module Issues
+ class RelatedBranchesService < Issues::BaseService
+ def execute(issue)
+ branches_with_iid_of(issue) - branches_with_merge_request_for(issue)
+ end
+
+ private
+
+ def branches_with_merge_request_for(issue)
+ Issues::ReferencedMergeRequestsService
+ .new(project, current_user)
+ .referenced_merge_requests(issue)
+ .map(&:source_branch)
+ end
+
+ def branches_with_iid_of(issue)
+ project.repository.branch_names.select do |branch|
+ branch =~ /\A#{issue.iid}-(?!\d+-stable)/i
+ end
+ end
+ end
+end
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 3bd53f9ccdc..56d59b235a7 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -3,7 +3,7 @@
module Issues
class ReopenService < Issues::BaseService
def execute(issue)
- return issue unless can?(current_user, :update_issue, issue)
+ return issue unless can?(current_user, :reopen_issue, issue)
if issue.reopen
event_service.reopen_issue(issue, current_user)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index faa4c8a5a4f..b54b0bf6ef6 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -67,6 +67,7 @@ module Issues
issue.move_between(issue_before, issue_after)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def change_issue_duplicate(issue)
canonical_issue_id = params.delete(:canonical_issue_id)
canonical_issue = IssuesFinder.new(current_user).find_by(id: canonical_issue_id)
@@ -75,6 +76,7 @@ module Issues
Issues::DuplicateService.new(project, current_user).execute(issue, canonical_issue)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def move_issue_to_new_project(issue)
target_project = params.delete(:target_project)
@@ -89,6 +91,7 @@ module Issues
private
+ # rubocop: disable CodeReuse/ActiveRecord
def get_issue_if_allowed(id, board_group_id = nil)
return unless id
@@ -101,6 +104,7 @@ module Issues
issue if can?(current_user, :update_issue, issue)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def create_confidentiality_note(issue)
SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user)
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index e4486764a4d..628873519d7 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -29,6 +29,7 @@ module Labels
# Only creates the label if current_user can do so, if the label does not exist
# and the user can not create the label, nil is returned
+ # rubocop: disable CodeReuse/ActiveRecord
def find_or_create_label
new_label = available_labels.find_by(title: title)
@@ -39,6 +40,7 @@ module Labels
new_label
end
+ # rubocop: enable CodeReuse/ActiveRecord
def title
params[:title] || params[:name]
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
index 623a5f0950e..f30ad706c63 100644
--- a/app/services/labels/promote_service.rb
+++ b/app/services/labels/promote_service.rb
@@ -4,6 +4,7 @@ module Labels
class PromoteService < BaseService
BATCH_SIZE = 1000
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(label)
return unless project.group &&
label.is_a?(ProjectLabel)
@@ -13,6 +14,7 @@ module Labels
label_ids_for_merge(new_label).find_in_batches(batch_size: BATCH_SIZE) do |batched_ids|
update_issuables(new_label, batched_ids)
+ update_resource_label_events(new_label, batched_ids)
update_issue_board_lists(new_label, batched_ids)
update_priorities(new_label, batched_ids)
subscribe_users(new_label, batched_ids)
@@ -26,9 +28,11 @@ module Labels
new_label
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
+ # rubocop: disable CodeReuse/ActiveRecord
def subscribe_users(new_label, label_ids)
# users can be subscribed to multiple labels that will be merged into the group one
# we want to keep only one subscription / user
@@ -37,7 +41,9 @@ module Labels
.pluck('MAX(id)')
Subscription.where(id: ids_to_update).update_all(subscribable_id: new_label.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def label_ids_for_merge(new_label)
LabelsFinder
.new(current_user, title: new_label.title, group_id: project.group.id)
@@ -45,28 +51,45 @@ module Labels
.where.not(id: new_label)
.select(:id) # Can't use pluck() to avoid object-creation because of the batching
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def update_issuables(new_label, label_ids)
LabelLink
.where(label: label_ids)
.update_all(label_id: new_label)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
+ def update_resource_label_events(new_label, label_ids)
+ ResourceLabelEvent
+ .where(label: label_ids)
+ .update_all(label_id: new_label)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
def update_issue_board_lists(new_label, label_ids)
List
.where(label: label_ids)
.update_all(label_id: new_label)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def update_priorities(new_label, label_ids)
LabelPriority
.where(label: label_ids)
.update_all(label_id: new_label)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def update_project_labels(label_ids)
Label.where(id: label_ids).destroy_all # rubocop: disable DestroyAll
end
+ # rubocop: enable CodeReuse/ActiveRecord
def clone_label_to_group_label(label)
params = label.attributes.slice('title', 'description', 'color')
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
index 1bd8d9fc325..52360f775dc 100644
--- a/app/services/labels/transfer_service.rb
+++ b/app/services/labels/transfer_service.rb
@@ -32,16 +32,19 @@ module Labels
attr_reader :current_user, :old_group, :project
+ # rubocop: disable CodeReuse/ActiveRecord
def labels_to_transfer
- label_ids = []
- label_ids << group_labels_applied_to_issues.select(:id)
- label_ids << group_labels_applied_to_merge_requests.select(:id)
-
- union = Gitlab::SQL::Union.new(label_ids)
-
- Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq # rubocop:disable GitlabSecurity/SqlInjection
+ Label
+ .from_union([
+ group_labels_applied_to_issues,
+ group_labels_applied_to_merge_requests
+ ])
+ .reorder(nil)
+ .uniq
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def group_labels_applied_to_issues
Label.joins(:issues)
.where(
@@ -49,7 +52,9 @@ module Labels
labels: { type: 'GroupLabel', group_id: old_group.id }
)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def group_labels_applied_to_merge_requests
Label.joins(:merge_requests)
.where(
@@ -57,6 +62,7 @@ module Labels
labels: { type: 'GroupLabel', group_id: old_group.id }
)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def find_or_create_label!(label)
params = label.attributes.slice('title', 'description', 'color')
@@ -65,6 +71,7 @@ module Labels
new_label.id
end
+ # rubocop: disable CodeReuse/ActiveRecord
def update_label_links(labels, old_label_id:, new_label_id:)
# use 'labels' relation to get label_link ids only of issues/MRs
# in the project being transferred.
@@ -76,10 +83,13 @@ module Labels
LabelLink.where(id: link_ids, label_id: old_label_id)
.update_all(label_id: new_label_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def update_label_priorities(old_label_id:, new_label_id:)
LabelPriority.where(project_id: project.id, label_id: old_label_id)
.update_all(label_id: new_label_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/lfs/file_transformer.rb b/app/services/lfs/file_transformer.rb
index c8eccb8e6cd..6ecf583cb6a 100644
--- a/app/services/lfs/file_transformer.rb
+++ b/app/services/lfs/file_transformer.rb
@@ -55,11 +55,13 @@ module Lfs
@cached_attributes ||= Gitlab::Git::AttributesAtRefParser.new(repository, branch_name)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def create_lfs_object!(lfs_pointer_file, file_content)
LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object|
lfs_object.file = CarrierWaveStringFile.new(file_content)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def link_lfs_object!(lfs_object)
project.lfs_objects << lfs_object
diff --git a/app/services/lfs/lock_file_service.rb b/app/services/lfs/lock_file_service.rb
index 78434909d68..c7730d24bdc 100644
--- a/app/services/lfs/lock_file_service.rb
+++ b/app/services/lfs/lock_file_service.rb
@@ -18,9 +18,11 @@ module Lfs
private
+ # rubocop: disable CodeReuse/ActiveRecord
def current_lock
project.lfs_file_locks.find_by(path: params[:path])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def create_lock!
lock = project.lfs_file_locks.create!(user: current_user,
diff --git a/app/services/lfs/locks_finder_service.rb b/app/services/lfs/locks_finder_service.rb
index d52cf0e3cc4..4a5b2a52921 100644
--- a/app/services/lfs/locks_finder_service.rb
+++ b/app/services/lfs/locks_finder_service.rb
@@ -10,10 +10,12 @@ module Lfs
private
+ # rubocop: disable CodeReuse/ActiveRecord
def find_locks
options = params.slice(:id, :path).compact.symbolize_keys
project.lfs_file_locks.where(options)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb
index 4d1443bf772..a42916d86bb 100644
--- a/app/services/lfs/unlock_file_service.rb
+++ b/app/services/lfs/unlock_file_service.rb
@@ -32,6 +32,7 @@ module Lfs
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def lock
return @lock if defined?(@lock)
@@ -41,5 +42,6 @@ module Lfs
project.lfs_file_locks.find_by!(path: params[:path])
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index e6dd0e12a3a..28c3219b37b 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -55,13 +55,15 @@ module MergeRequests
end
# Returns all origin and fork merge requests from `@project` satisfying passed arguments.
+ # rubocop: disable CodeReuse/ActiveRecord
def merge_requests_for(source_branch, mr_states: [:opened])
- MergeRequest
+ @project.source_of_merge_requests
.with_state(mr_states)
- .where(source_branch: source_branch, source_project_id: @project.id)
- .preload(:source_project) # we don't need a #includes since we're just preloading for the #select
+ .where(source_branch: source_branch)
+ .preload(:source_project) # we don't need #includes since we're just preloading for the #select
.select(&:source_project)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def pipeline_merge_requests(pipeline)
merge_requests_for(pipeline.ref).each do |merge_request|
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 55750269bb4..0e76d2cc3ab 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -20,6 +20,8 @@ module MergeRequests
if merge_request.can_be_created
compare_branches
assign_title_and_description
+ assign_labels
+ assign_milestone
end
merge_request
@@ -135,6 +137,20 @@ module MergeRequests
append_closes_description
end
+ def assign_labels
+ return unless target_project.issues_enabled? && issue
+ return if merge_request.label_ids&.any?
+
+ merge_request.label_ids = issue.try(:label_ids)
+ end
+
+ def assign_milestone
+ return unless target_project.issues_enabled? && issue
+ return if merge_request.milestone_id.present?
+
+ merge_request.milestone_id = issue.try(:milestone_id)
+ end
+
def append_closes_description
return unless issue&.to_reference.present?
@@ -185,7 +201,9 @@ module MergeRequests
end
def issue
- @issue ||= target_project.get_issue(issue_iid, current_user)
+ strong_memoize(:issue) do
+ target_project.get_issue(issue_iid, current_user)
+ end
end
end
end
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index fd91dc4acd0..020af0bb950 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -16,8 +16,6 @@ module MergeRequests
def execute
return error('Invalid issue iid') unless @issue_iid.present? && issue.present?
- params[:label_ids] = issue.label_ids if issue.label_ids.any?
-
result = CreateBranchService.new(project, current_user).execute(branch_name, ref)
return result if result[:status] == :error
@@ -34,9 +32,11 @@ module MergeRequests
private
+ # rubocop: disable CodeReuse/ActiveRecord
def issue
@issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: @issue_iid)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def branch_name
@branch ||= @branch_name || issue.to_branch_name
@@ -58,8 +58,7 @@ module MergeRequests
source_project_id: project.id,
source_branch: branch_name,
target_project_id: project.id,
- target_branch: ref,
- milestone_id: issue.milestone_id
+ target_branch: ref
}
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index c36a2ecbfe3..6081a7d1de0 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -49,6 +49,7 @@ module MergeRequests
merge_request.update(head_pipeline_id: pipeline.id) if pipeline
end
+ # rubocop: disable CodeReuse/ActiveRecord
def head_pipeline_for(merge_request)
return unless merge_request.source_project
@@ -59,6 +60,7 @@ module MergeRequests
pipelines.order(id: :desc).first
end
+ # rubocop: enable CodeReuse/ActiveRecord
def set_projects!
# @project is used to determine whether the user can set the merge request's
diff --git a/app/services/merge_requests/delete_non_latest_diffs_service.rb b/app/services/merge_requests/delete_non_latest_diffs_service.rb
index 2a8ea316921..d5929446122 100644
--- a/app/services/merge_requests/delete_non_latest_diffs_service.rb
+++ b/app/services/merge_requests/delete_non_latest_diffs_service.rb
@@ -8,6 +8,7 @@ module MergeRequests
@merge_request = merge_request
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
diffs = @merge_request.non_latest_diffs.with_files
@@ -16,5 +17,6 @@ module MergeRequests
DeleteDiffFilesWorker.bulk_perform_in(index * 5.minutes, ids)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 48da796505f..b03d14fa3cc 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -3,16 +3,16 @@
module MergeRequests
class RefreshService < MergeRequests::BaseService
def execute(oldrev, newrev, ref)
- return true unless Gitlab::Git.branch_ref?(ref)
+ push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref)
+ return true unless push.branch_push?
- do_execute(oldrev, newrev, ref)
+ refresh_merge_requests!(push)
end
private
- def do_execute(oldrev, newrev, ref)
- @oldrev, @newrev = oldrev, newrev
- @branch_name = Gitlab::Git.ref_name(ref)
+ def refresh_merge_requests!(push)
+ @push = push
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an
@@ -25,7 +25,7 @@ module MergeRequests
cache_merge_requests_closing_issues
# Leave a system note if a branch was deleted/added
- if branch_added? || branch_removed?
+ if @push.branch_added? || @push.branch_removed?
comment_mr_branch_presence_changed
end
@@ -51,10 +51,13 @@ module MergeRequests
# and close if push to master include last commit from merge request
# We need this to close(as merged) merge requests that were merged into
# target branch manually
+ # rubocop: disable CodeReuse/ActiveRecord
def post_merge_manually_merged
commit_ids = @commits.map(&:id)
- merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a
- merge_requests = merge_requests.select(&:diff_head_commit)
+ merge_requests = @project.merge_requests.opened
+ .preload(:latest_merge_request_diff)
+ .where(target_branch: @push.branch_name).to_a
+ .select(&:diff_head_commit)
merge_requests = merge_requests.select do |merge_request|
commit_ids.include?(merge_request.diff_head_sha) &&
@@ -67,24 +70,22 @@ module MergeRequests
.execute(merge_request)
end
end
-
- def force_push?
- Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
- end
+ # rubocop: enable CodeReuse/ActiveRecord
# Refresh merge request diff if we push to source or target branch of merge request
# Note: we should update merge requests from forks too
+ # rubocop: disable CodeReuse/ActiveRecord
def reload_merge_requests
merge_requests = @project.merge_requests.opened
- .by_source_or_target_branch(@branch_name).to_a
+ .by_source_or_target_branch(@push.branch_name).to_a
# Fork merge requests
merge_requests += MergeRequest.opened
- .where(source_branch: @branch_name, source_project: @project)
+ .where(source_branch: @push.branch_name, source_project: @project)
.where.not(target_project: @project).to_a
filter_merge_requests(merge_requests).each do |merge_request|
- if merge_request.source_branch == @branch_name || force_push?
+ if merge_request.source_branch == @push.branch_name || @push.force_push?
merge_request.reload_diff(current_user)
else
mr_commit_ids = merge_request.commit_shas
@@ -101,6 +102,7 @@ module MergeRequests
# @source_merge_requests diffs (for MergeRequest#commit_shas for instance).
merge_requests_for_source_branch(reload: true)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def reset_merge_when_pipeline_succeeds
merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds)
@@ -113,7 +115,7 @@ module MergeRequests
end
def find_new_commits
- if branch_added?
+ if @push.branch_added?
@commits = []
merge_request = merge_requests_for_source_branch.first
@@ -122,28 +124,28 @@ module MergeRequests
begin
# Since any number of commits could have been made to the restored branch,
# find the common root to see what has been added.
- common_ref = @project.repository.merge_base(merge_request.diff_head_sha, @newrev)
+ common_ref = @project.repository.merge_base(merge_request.diff_head_sha, @push.newrev)
# If the a commit no longer exists in this repo, gitlab_git throws
# a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52
- @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref
+ @commits = @project.repository.commits_between(common_ref, @push.newrev) if common_ref
rescue
end
- elsif branch_removed?
+ elsif @push.branch_removed?
# No commits for a deleted branch.
@commits = []
else
- @commits = @project.repository.commits_between(@oldrev, @newrev)
+ @commits = @project.repository.commits_between(@push.oldrev, @push.newrev)
end
end
# Add comment about branches being deleted or added to merge requests
def comment_mr_branch_presence_changed
- presence = branch_added? ? :add : :delete
+ presence = @push.branch_added? ? :add : :delete
merge_requests_for_source_branch.each do |merge_request|
SystemNoteService.change_branch_presence(
merge_request, merge_request.project, @current_user,
- :source, @branch_name, presence)
+ :source, @push.branch_name, presence)
end
end
@@ -160,7 +162,7 @@ module MergeRequests
SystemNoteService.add_commits(merge_request, merge_request.project,
@current_user, new_commits,
- existing_commits, @oldrev)
+ existing_commits, @push.oldrev)
notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
end
@@ -191,17 +193,19 @@ module MergeRequests
# Call merge request webhook with update branches
def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request|
- execute_hooks(merge_request, 'update', old_rev: @oldrev)
+ execute_hooks(merge_request, 'update', old_rev: @push.oldrev)
end
end
# If the merge requests closes any issues, save this information in the
# `MergeRequestsClosingIssues` model (as a performance optimization).
+ # rubocop: disable CodeReuse/ActiveRecord
def cache_merge_requests_closing_issues
- @project.merge_requests.where(source_branch: @branch_name).each do |merge_request|
+ @project.merge_requests.where(source_branch: @push.branch_name).each do |merge_request|
merge_request.cache_merge_request_closes_issues!(@current_user)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def filter_merge_requests(merge_requests)
merge_requests.uniq.select(&:source_project)
@@ -209,15 +213,7 @@ module MergeRequests
def merge_requests_for_source_branch(reload: false)
@source_merge_requests = nil if reload
- @source_merge_requests ||= merge_requests_for(@branch_name)
- end
-
- def branch_added?
- Gitlab::Git.blank_ref?(@oldrev)
- end
-
- def branch_removed?
- Gitlab::Git.blank_ref?(@newrev)
+ @source_merge_requests ||= merge_requests_for(@push.branch_name)
end
end
end
diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb
index 8d85dc9eb5f..b4d48fe92ad 100644
--- a/app/services/merge_requests/reload_diffs_service.rb
+++ b/app/services/merge_requests/reload_diffs_service.rb
@@ -27,10 +27,11 @@ module MergeRequests
current_user: current_user)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def clear_cache(new_diff)
# Executing the iteration we cache highlighted diffs for each diff file of
# MergeRequestDiff.
- new_diff.diffs_collection.diff_files.to_a
+ cacheable_collection(new_diff).write_cache
# Remove cache for all diffs on this MR. Do not use the association on the
# model, as that will interfere with other actions happening when
@@ -38,8 +39,15 @@ module MergeRequests
MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
next if merge_request_diff == new_diff
- merge_request_diff.diffs_collection.clear_cache!
+ cacheable_collection(merge_request_diff).clear_cache
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def cacheable_collection(diff)
+ # There are scenarios where we don't need to request Diff Stats.
+ # Mainly when clearing / writing diff caches.
+ diff.diffs(include_stats: false)
+ end
end
end
diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb
index 660b4faaec0..39071b5dc14 100644
--- a/app/services/milestones/promote_service.rb
+++ b/app/services/milestones/promote_service.rb
@@ -26,6 +26,7 @@ module Milestones
private
+ # rubocop: disable CodeReuse/ActiveRecord
def milestone_ids_for_merge(group_milestone)
# Pluck need to be used here instead of select so the array of ids
# is persistent after old milestones gets deleted.
@@ -35,6 +36,7 @@ module Milestones
milestones.pluck(:id)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def move_children_to_group_milestone(group_milestone)
milestone_ids_for_merge(group_milestone).in_groups_of(100, false) do |milestone_ids|
@@ -59,6 +61,7 @@ module Milestones
milestone
end
+ # rubocop: disable CodeReuse/ActiveRecord
def update_children(group_milestone, milestone_ids)
issues = Issue.where(project_id: group_project_ids, milestone_id: milestone_ids)
merge_requests = MergeRequest.where(source_project_id: group_project_ids, milestone_id: milestone_ids)
@@ -67,18 +70,23 @@ module Milestones
issuable_collection.update_all(milestone_id: group_milestone.id)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def group
@group ||= parent.group || raise_error('Project does not belong to a group.')
end
+ # rubocop: disable CodeReuse/ActiveRecord
def destroy_old_milestones(milestone)
Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all # rubocop: disable DestroyAll
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def group_project_ids
@group_project_ids ||= group.projects.pluck(:id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def raise_error(message)
raise PromoteMilestoneError, "Promotion failed - #{message}"
diff --git a/app/services/milestones/update_service.rb b/app/services/milestones/update_service.rb
index 81b20943bab..01ab8b37bac 100644
--- a/app/services/milestones/update_service.rb
+++ b/app/services/milestones/update_service.rb
@@ -2,6 +2,7 @@
module Milestones
class UpdateService < Milestones::BaseService
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(milestone)
state = params[:state_event]
@@ -18,5 +19,6 @@ module Milestones
milestone
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/notes/build_service.rb b/app/services/notes/build_service.rb
index df5fe65de3c..7b92fe6fe14 100644
--- a/app/services/notes/build_service.rb
+++ b/app/services/notes/build_service.rb
@@ -3,6 +3,7 @@
module Notes
class BuildService < ::BaseService
def execute
+ should_resolve = false
in_reply_to_discussion_id = params.delete(:in_reply_to_discussion_id)
if in_reply_to_discussion_id.present?
@@ -15,12 +16,17 @@ module Notes
end
params.merge!(discussion.reply_attributes)
+ should_resolve = discussion.resolved?
end
note = Note.new(params)
note.project = project
note.author = current_user
+ if should_resolve
+ note.resolve_without_save(current_user)
+ end
+
note
end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 5c0e8a35cb0..9c236d7f41d 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -58,6 +58,7 @@ module NotificationRecipientService
@recipients ||= []
end
+ # rubocop: disable CodeReuse/ActiveRecord
def add_recipients(users, type, reason)
if users.is_a?(ActiveRecord::Relation)
users = users.includes(:notification_settings)
@@ -66,10 +67,13 @@ module NotificationRecipientService
users = Array(users).compact
recipients.concat(users.map { |u| make_recipient(u, type, reason) })
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def user_scope
User.includes(:notification_settings)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def make_recipient(user, type, reason)
NotificationRecipient.new(
@@ -112,6 +116,7 @@ module NotificationRecipientService
end
# Get project/group users with CUSTOM notification level
+ # rubocop: disable CodeReuse/ActiveRecord
def add_custom_notifications
user_ids = []
@@ -128,6 +133,7 @@ module NotificationRecipientService
add_recipients(user_scope.where(id: user_ids), :watch, nil)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def add_project_watchers
add_recipients(project_watchers, :watch, nil) if project
@@ -138,6 +144,7 @@ module NotificationRecipientService
end
# Get project users with WATCH notification level
+ # rubocop: disable CodeReuse/ActiveRecord
def project_watchers
project_members_ids = user_ids_notifiable_on(project)
@@ -151,7 +158,9 @@ module NotificationRecipientService
user_scope.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def group_watchers
user_ids_with_group_global = user_ids_notifiable_on(group, :global)
user_ids = user_ids_with_global_level_watch(user_ids_with_group_global)
@@ -159,6 +168,7 @@ module NotificationRecipientService
user_scope.where(id: user_ids_with_group_setting)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def add_subscribed_users
return unless target.respond_to? :subscribers
@@ -166,6 +176,7 @@ module NotificationRecipientService
add_recipients(target.subscribers(project), :subscription, nil)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def user_ids_notifiable_on(resource, notification_level = nil)
return [] unless resource
@@ -177,6 +188,7 @@ module NotificationRecipientService
scope.pluck(:user_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Build a list of user_ids based on project notification settings
def select_project_members_ids(global_setting, user_ids_global_level_watch)
@@ -194,14 +206,19 @@ module NotificationRecipientService
uids + (global_setting & user_ids_global_level_watch) - project_members
end
+ # rubocop: disable CodeReuse/ActiveRecord
def user_ids_with_global_level_watch(ids)
settings_with_global_level_of(:watch, ids).pluck(:user_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def user_ids_with_global_level_custom(ids, action)
settings_with_global_level_of(:custom, ids).pluck(:user_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def settings_with_global_level_of(level, ids)
NotificationSetting.where(
user_id: ids,
@@ -209,6 +226,7 @@ module NotificationRecipientService
level: NotificationSetting.levels[level]
)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def add_labels_subscribers(labels: nil)
return unless target.respond_to? :labels
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 4511c500fca..50fa373025b 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -407,6 +407,12 @@ class NotificationService
end
end
+ def autodevops_disabled(pipeline, recipients)
+ recipients.each do |recipient|
+ mailer.autodevops_disabled_email(pipeline, recipient).deliver_later
+ end
+ end
+
def pages_domain_verification_succeeded(domain)
recipients_for_pages_domain(domain).each do |user|
mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index 11b996ed4b6..de8757006f1 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -43,6 +43,10 @@ class PreviewMarkdownService < BaseService
end
def markdown_engine
- CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i)
+ if params[:legacy_render]
+ :redcarpet
+ else
+ CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i)
+ end
end
end
diff --git a/app/services/projects/auto_devops/disable_service.rb b/app/services/projects/auto_devops/disable_service.rb
new file mode 100644
index 00000000000..1b578a3c5ce
--- /dev/null
+++ b/app/services/projects/auto_devops/disable_service.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Projects
+ module AutoDevops
+ class DisableService < BaseService
+ def execute
+ return false unless implicitly_enabled_and_first_pipeline_failure?
+
+ disable_auto_devops
+ end
+
+ private
+
+ def implicitly_enabled_and_first_pipeline_failure?
+ project.has_auto_devops_implicitly_enabled? &&
+ first_pipeline_failure?
+ end
+
+ # We're using `limit` to optimize `auto_devops pipeline` query,
+ # since we only care about the first element, and using only `.count`
+ # is an expensive operation. See
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21172#note_99037378
+ # for more context.
+ # rubocop: disable CodeReuse/ActiveRecord
+ def first_pipeline_failure?
+ auto_devops_pipelines.success.limit(1).count.zero? &&
+ auto_devops_pipelines.failed.limit(1).count.nonzero?
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def disable_auto_devops
+ project.auto_devops_attributes = { enabled: false }
+ project.save!
+ end
+
+ def auto_devops_pipelines
+ @auto_devops_pipelines ||= project.pipelines.auto_devops_source
+ end
+ end
+ end
+end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 5286b92ab6b..61f6402a810 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -2,6 +2,7 @@
module Projects
class AutocompleteService < BaseService
+ include LabelsAsHash
def issues
IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
@@ -22,34 +23,18 @@ module Projects
MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
- def labels_as_hash(target = nil)
- available_labels = LabelsFinder.new(
- current_user,
- project_id: project.id,
- include_ancestor_groups: true
- ).execute
-
- label_hashes = available_labels.as_json(only: [:title, :color])
-
- if target&.respond_to?(:labels)
- already_set_labels = available_labels & target.labels
- if already_set_labels.present?
- titles = already_set_labels.map(&:title)
- label_hashes.each do |hash|
- if titles.include?(hash['title'])
- hash[:set] = true
- end
- end
- end
- end
-
- label_hashes
- end
-
def commands(noteable, type)
return [] unless noteable
QuickActions::InterpretService.new(project, current_user).available_commands(noteable)
end
+
+ def snippets
+ SnippetsFinder.new(current_user, project: project).execute.select([:id, :title])
+ end
+
+ def labels_as_hash(target)
+ super(target, project_id: project.id, include_ancestor_groups: true)
+ end
end
end
diff --git a/app/services/projects/base_move_relations_service.rb b/app/services/projects/base_move_relations_service.rb
index 78cc2869b72..24dec1f3a45 100644
--- a/app/services/projects/base_move_relations_service.rb
+++ b/app/services/projects/base_move_relations_service.rb
@@ -13,6 +13,7 @@ module Projects
private
+ # rubocop: disable CodeReuse/ActiveRecord
def prepare_relation(relation, id_param = :id)
if Gitlab::Database.postgresql?
relation
@@ -20,5 +21,6 @@ module Projects
relation.model.where("#{id_param}": relation.pluck(id_param))
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/projects/batch_forks_count_service.rb b/app/services/projects/batch_forks_count_service.rb
index 9bf369df999..6467744a435 100644
--- a/app/services/projects/batch_forks_count_service.rb
+++ b/app/services/projects/batch_forks_count_service.rb
@@ -5,6 +5,7 @@
# because the service use maps to retrieve the project ids
module Projects
class BatchForksCountService < Projects::BatchCountService
+ # rubocop: disable CodeReuse/ActiveRecord
def global_count
@global_count ||= begin
count_service.query(project_ids)
@@ -12,6 +13,7 @@ module Projects
.count
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def count_service
::Projects::ForksCountService
diff --git a/app/services/projects/batch_open_issues_count_service.rb b/app/services/projects/batch_open_issues_count_service.rb
index d375fcf9dbd..d6ff2291af8 100644
--- a/app/services/projects/batch_open_issues_count_service.rb
+++ b/app/services/projects/batch_open_issues_count_service.rb
@@ -5,11 +5,13 @@
# because the service use maps to retrieve the project ids
module Projects
class BatchOpenIssuesCountService < Projects::BatchCountService
+ # rubocop: disable CodeReuse/ActiveRecord
def global_count
@global_count ||= begin
count_service.query(project_ids).group(:project_id).count
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def count_service
::Projects::OpenIssuesCountService
diff --git a/app/services/projects/container_repository/destroy_service.rb b/app/services/projects/container_repository/destroy_service.rb
new file mode 100644
index 00000000000..1f5af7970d6
--- /dev/null
+++ b/app/services/projects/container_repository/destroy_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Projects
+ module ContainerRepository
+ class DestroyService < BaseService
+ def execute(container_repository)
+ return false unless can?(current_user, :update_container_image, project)
+
+ # Delete tags outside of the transaction to avoid hitting an idle-in-transaction timeout
+ container_repository.delete_tags!
+ container_repository.destroy
+ end
+ end
+ end
+end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 02a3a3eb096..0e6a7e8da54 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -79,17 +79,21 @@ module Projects
@project.errors.add(:namespace, "is not valid")
end
+ # rubocop: disable CodeReuse/ActiveRecord
def allowed_fork?(source_project_id)
return true if source_project_id.nil?
source_project = Project.find_by(id: source_project_id)
current_user.can?(:fork_project, source_project)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def allowed_namespace?(user, namespace_id)
namespace = Namespace.find_by(id: namespace_id)
current_user.can?(:create_projects, namespace)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def after_create_actions
log_info("#{@project.owner.name} created a new project \"#{@project.full_name}\"")
@@ -167,12 +171,14 @@ module Projects
@project
end
+ # rubocop: disable CodeReuse/ActiveRecord
def create_services_from_active_templates(project)
Service.where(template: true, active: true).each do |template|
service = Service.build_from_template(project.id, template)
service.save!
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def set_project_name_from_path
# Set project name from path
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 76e22507698..210571b6b4e 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -107,15 +107,19 @@ module Projects
mv_repository(old_path, new_path)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def repo_exists?(path)
gitlab_shell.exists?(project.repository_storage, path + '.git')
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def mv_repository(from_path, to_path)
return true unless gitlab_shell.exists?(project.repository_storage, from_path + '.git')
gitlab_shell.mv_repository(project.repository_storage, from_path, to_path)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def attempt_rollback(project, message)
return unless project
@@ -129,11 +133,11 @@ module Projects
end
def attempt_destroy_transaction(project)
- Project.transaction do
- unless remove_legacy_registry_tags
- raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
- end
+ unless remove_registry_tags
+ raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
+ end
+ Project.transaction do
log_destroy_event
trash_repositories!
@@ -152,6 +156,17 @@ module Projects
log_info("Attempting to destroy #{project.full_path} (#{project.id})")
end
+ def remove_registry_tags
+ return false unless remove_legacy_registry_tags
+
+ project.container_repositories.find_each do |container_repository|
+ service = Projects::ContainerRepository::DestroyService.new(project, current_user)
+ service.execute(container_repository)
+ end
+
+ true
+ end
+
##
# This method makes sure that we correctly remove registry tags
# for legacy image repository (when repository path equals project path).
@@ -159,7 +174,7 @@ module Projects
def remove_legacy_registry_tags
return true unless Gitlab.config.registry.enabled
- ContainerRepository.build_root_repository(project).tap do |repository|
+ ::ContainerRepository.build_root_repository(project).tap do |repository|
break repository.has_tags? ? repository.delete_tags! : true
end
end
diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb
index 3488b9ce47e..4a837a4fb6a 100644
--- a/app/services/projects/detect_repository_languages_service.rb
+++ b/app/services/projects/detect_repository_languages_service.rb
@@ -4,6 +4,7 @@ module Projects
class DetectRepositoryLanguagesService < BaseService
attr_reader :detected_repository_languages, :programming_languages
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
repository_languages = project.repository_languages
detection = Gitlab::LanguageDetection.new(repository, repository_languages)
@@ -28,9 +29,11 @@ module Projects
project.repository_languages.reload
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
+ # rubocop: disable CodeReuse/ActiveRecord
def ensure_programming_languages(detection)
existing_languages = ProgrammingLanguage.where(name: detection.languages)
return existing_languages if detection.languages.size == existing_languages.size
@@ -42,7 +45,9 @@ module Projects
existing_languages + created_languages
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def create_language(name, color)
ProgrammingLanguage.transaction do
ProgrammingLanguage.where(name: name).first_or_create(color: color)
@@ -50,5 +55,6 @@ module Projects
rescue ActiveRecord::RecordNotUnique
retry
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/projects/enable_deploy_key_service.rb b/app/services/projects/enable_deploy_key_service.rb
index b7c172028e9..102088e9557 100644
--- a/app/services/projects/enable_deploy_key_service.rb
+++ b/app/services/projects/enable_deploy_key_service.rb
@@ -2,6 +2,7 @@
module Projects
class EnableDeployKeyService < BaseService
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
key = accessible_keys.find_by(id: params[:key_id] || params[:id])
return unless key
@@ -12,6 +13,7 @@ module Projects
key
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb
index b570c6d4754..00e73148358 100644
--- a/app/services/projects/forks_count_service.rb
+++ b/app/services/projects/forks_count_service.rb
@@ -7,11 +7,13 @@ module Projects
'forks_count'
end
+ # rubocop: disable CodeReuse/ActiveRecord
def self.query(project_ids)
# We can't directly change ForkedProjectLink to ForkNetworkMember here
# Nowadays, when a call using v3 to projects/:id/fork is made,
# the relationship to ForkNetworkMember is not updated
ForkedProjectLink.where(forked_from_project: project_ids)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb
index 044afa1d5e1..a315adf42f0 100644
--- a/app/services/projects/gitlab_projects_import_service.rb
+++ b/app/services/projects/gitlab_projects_import_service.rb
@@ -32,11 +32,13 @@ module Projects
Project.find_by_full_path("#{current_namespace.full_path}/#{params[:path]}").present?
end
+ # rubocop: disable CodeReuse/ActiveRecord
def current_namespace
strong_memoize(:current_namespace) do
Namespace.find_by(id: params[:namespace_id])
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def overwrite?
strong_memoize(:overwrite) do
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index 641d46e6591..4462d504071 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -47,10 +47,13 @@ module Projects
private
+ # rubocop: disable CodeReuse/ActiveRecord
def has_wiki?
gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git")
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def move_repository(from_name, to_name)
from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git")
to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git")
@@ -67,6 +70,7 @@ module Projects
gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def rollback_folder_move
move_repository(new_disk_path, old_disk_path)
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index 7d4fa4e08df..1c4a8d05be6 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -4,6 +4,7 @@
module Projects
module LfsPointers
class LfsDownloadService < BaseService
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(oid, url)
return unless project&.lfs_enabled? && oid.present? && url.present?
@@ -20,6 +21,7 @@ module Projects
rescue StandardError => e
Rails.logger.error("LFS file with oid #{oid} could't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb
index 97ce681a911..9215fa0a7bf 100644
--- a/app/services/projects/lfs_pointers/lfs_import_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_import_service.rb
@@ -41,6 +41,7 @@ module Projects
project.update(lfs_enabled: false)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def get_download_links
existent_lfs = LfsListService.new(project).execute
linked_oids = LfsLinkService.new(project).execute(existent_lfs.keys)
@@ -50,6 +51,7 @@ module Projects
LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(not_linked_lfs)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def lfsconfig_endpoint_uri
strong_memoize(:lfsconfig_endpoint_uri) do
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
index a2eba8e124e..8401f3d1d89 100644
--- a/app/services/projects/lfs_pointers/lfs_link_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -16,6 +16,7 @@ module Projects
private
+ # rubocop: disable CodeReuse/ActiveRecord
def link_existing_lfs_objects(oids)
existent_lfs_objects = LfsObject.where(oid: oids)
@@ -26,6 +27,7 @@ module Projects
existent_lfs_objects.pluck(:oid)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/services/projects/move_deploy_keys_projects_service.rb b/app/services/projects/move_deploy_keys_projects_service.rb
index 9f3f44f30ea..b6a3af8c7b8 100644
--- a/app/services/projects/move_deploy_keys_projects_service.rb
+++ b/app/services/projects/move_deploy_keys_projects_service.rb
@@ -20,11 +20,13 @@ module Projects
.update_all(project_id: @project.id)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def non_existent_deploy_keys_projects
source_project.deploy_keys_projects
.joins(:deploy_key)
.where.not(keys: { fingerprint: @project.deploy_keys.select(:fingerprint) })
end
+ # rubocop: enable CodeReuse/ActiveRecord
def remove_remaining_deploy_keys_projects
source_project.deploy_keys_projects.destroy_all # rubocop: disable DestroyAll
diff --git a/app/services/projects/move_forks_service.rb b/app/services/projects/move_forks_service.rb
index 076a7a50aa9..2948555a17c 100644
--- a/app/services/projects/move_forks_service.rb
+++ b/app/services/projects/move_forks_service.rb
@@ -17,6 +17,7 @@ module Projects
private
+ # rubocop: disable CodeReuse/ActiveRecord
def move_forked_project_links
# Update ancestor
ForkedProjectLink.where(forked_to_project: source_project)
@@ -26,16 +27,21 @@ module Projects
ForkedProjectLink.where(forked_from_project: source_project)
.update_all(forked_from_project_id: @project.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def move_fork_network_members
ForkNetworkMember.where(project: source_project).update_all(project_id: @project.id)
ForkNetworkMember.where(forked_from_project: source_project).update_all(forked_from_project_id: @project.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def update_root_project
# Update root network project
ForkNetwork.where(root_project: source_project).update_all(root_project_id: @project.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb
index f78546a1e9c..308a54ad06e 100644
--- a/app/services/projects/move_lfs_objects_projects_service.rb
+++ b/app/services/projects/move_lfs_objects_projects_service.rb
@@ -24,8 +24,10 @@ module Projects
source_project.lfs_objects_projects.destroy_all # rubocop: disable DestroyAll
end
+ # rubocop: disable CodeReuse/ActiveRecord
def non_existent_lfs_objects_projects
source_project.lfs_objects_projects.where.not(lfs_object: @project.lfs_objects)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/projects/move_notification_settings_service.rb b/app/services/projects/move_notification_settings_service.rb
index 109a00dd6d9..e740c44bd26 100644
--- a/app/services/projects/move_notification_settings_service.rb
+++ b/app/services/projects/move_notification_settings_service.rb
@@ -31,10 +31,12 @@ module Projects
end
# Look for notification_settings in source_project that are not in the target project
+ # rubocop: disable CodeReuse/ActiveRecord
def non_existent_notifications
source_project.notification_settings
.select(:id)
.where.not(user_id: users_in_target_project)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/projects/move_project_authorizations_service.rb b/app/services/projects/move_project_authorizations_service.rb
index 60f2af88e99..2060a263751 100644
--- a/app/services/projects/move_project_authorizations_service.rb
+++ b/app/services/projects/move_project_authorizations_service.rb
@@ -33,10 +33,12 @@ module Projects
end
# Look for authorizations in source_project that are not in the target project
+ # rubocop: disable CodeReuse/ActiveRecord
def non_existent_authorization
source_project.project_authorizations
.select(:user_id)
.where.not(user: @project.authorized_users)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb
index 1efafdce36d..fb395ecb9a1 100644
--- a/app/services/projects/move_project_group_links_service.rb
+++ b/app/services/projects/move_project_group_links_service.rb
@@ -34,9 +34,11 @@ module Projects
end
# Look for groups in source_project that are not in the target project
+ # rubocop: disable CodeReuse/ActiveRecord
def non_existent_group_links
source_project.project_group_links
.where.not(group_id: group_links_in_target_project)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb
index ec983582d94..f28f44adc03 100644
--- a/app/services/projects/move_project_members_service.rb
+++ b/app/services/projects/move_project_members_service.rb
@@ -33,10 +33,12 @@ module Projects
end
# Look for members in source_project that are not in the target project
+ # rubocop: disable CodeReuse/ActiveRecord
def non_existent_members
source_project.members
.select(:id)
.where.not(user_id: @project.project_members.select(:user_id))
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index 5d6620c3c54..ee9884e9042 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -42,6 +42,7 @@ module Projects
cache_key(TOTAL_COUNT_KEY)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def refresh_cache(&block)
if block_given?
super(&block)
@@ -59,11 +60,13 @@ module Projects
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
# We only show total issues count for reporters
# which are allowed to view confidential issues
# This will still show a discrepancy on issues number but should be less than before.
# Check https://gitlab.com/gitlab-org/gitlab-ce/issues/38418 description.
+ # rubocop: disable CodeReuse/ActiveRecord
def self.query(projects, public_only: true)
if public_only
Issue.opened.public_only.where(project: projects)
@@ -71,5 +74,6 @@ module Projects
Issue.opened.where(project: projects)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_service_template.rb
index fdfa91801ab..633a263af7b 100644
--- a/app/services/projects/propagate_service_template.rb
+++ b/app/services/projects/propagate_service_template.rb
@@ -70,6 +70,7 @@ module Projects
)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def service_hash
@service_hash ||=
begin
@@ -83,7 +84,9 @@ module Projects
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def run_callbacks(batch)
if active_external_issue_tracker?
Project.where(id: batch).update_all(has_external_issue_tracker: true)
@@ -93,6 +96,7 @@ module Projects
Project.where(id: batch).update_all(has_external_wiki: true)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def active_external_issue_tracker?
@template.issue_tracker? && !@template.default
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 3746cfef702..9d40ab166ff 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -37,6 +37,7 @@ module Projects
private
+ # rubocop: disable CodeReuse/ActiveRecord
def transfer(project)
@old_path = project.full_path
@old_group = project.group
@@ -54,6 +55,7 @@ module Projects
attempt_transfer_transaction
end
+ # rubocop: enable CodeReuse/ActiveRecord
def attempt_transfer_transaction
Project.transaction do
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index 2c0d91fe34f..a8b7c7f136a 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -2,6 +2,7 @@
module Projects
class UnlinkForkService < BaseService
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
return unless @project.forked?
@@ -26,6 +27,7 @@ module Projects
@project.fork_network_member.destroy
@project.forked_project_link.destroy
end
+ # rubocop: enable CodeReuse/ActiveRecord
def refresh_forks_count(project)
Projects::ForksCountService.new(project).refresh_cache
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
index efbd4c7b323..abf40b3ad7a 100644
--- a/app/services/projects/update_pages_configuration_service.rb
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -21,7 +21,9 @@ module Projects
def pages_config
{
domains: pages_domains_config,
- https_only: project.pages_https_only?
+ https_only: project.pages_https_only?,
+ id: project.project_id,
+ access_control: !project.public_pages?
}
end
@@ -31,7 +33,9 @@ module Projects
domain: domain.domain,
certificate: domain.certificate,
key: domain.key,
- https_only: project.pages_https_only? && domain.https?
+ https_only: project.pages_https_only? && domain.https?,
+ id: project.project_id,
+ access_control: !project.public_pages?
}
end
end
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
index 591b38b8151..9d0877d1ab2 100644
--- a/app/services/projects/update_remote_mirror_service.rb
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -5,10 +5,10 @@ module Projects
attr_reader :errors
def execute(remote_mirror)
- @errors = []
-
return success unless remote_mirror.enabled?
+ errors = []
+
begin
remote_mirror.ensure_remote!
repository.fetch_remote(remote_mirror.remote_name, no_tags: true)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index e390d7a04c3..f25a4e30938 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -6,6 +6,7 @@ module Projects
ValidationError = Class.new(StandardError)
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
validate!
@@ -26,6 +27,7 @@ module Projects
rescue ValidationError => e
error(e.message)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def run_auto_devops_pipeline?
return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled')
@@ -70,7 +72,11 @@ module Projects
system_hook_service.execute_hooks_for(project, :update)
end
- update_pages_config if changing_pages_https_only?
+ update_pages_config if changing_pages_related_config?
+ end
+
+ def changing_pages_related_config?
+ changing_pages_https_only? || changing_pages_access_level?
end
def update_failed!
@@ -100,6 +106,10 @@ module Projects
params.dig(:project_feature_attributes, :wiki_access_level).to_i > ProjectFeature::DISABLED
end
+ def changing_pages_access_level?
+ params.dig(:project_feature_attributes, :pages_access_level)
+ end
+
def ensure_wiki_exists
ProjectWiki.new(project, project.owner).wiki
rescue ProjectWiki::CouldNotCreateWikiError
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index a4c4c9e4812..defa579f9a8 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -111,10 +111,12 @@ module QuickActions
end
desc 'Assign'
+ # rubocop: disable CodeReuse/ActiveRecord
explanation do |users|
users = issuable.allows_multiple_assignees? ? users : users.take(1)
"Assigns #{users.map(&:to_reference).to_sentence}."
end
+ # rubocop: enable CodeReuse/ActiveRecord
params do
issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user'
end
@@ -127,12 +129,12 @@ module QuickActions
command :assign do |users|
next if users.empty?
- @updates[:assignee_ids] =
- if issuable.allows_multiple_assignees?
- issuable.assignees.pluck(:id) + users.map(&:id)
- else
- [users.first.id]
- end
+ if issuable.allows_multiple_assignees?
+ @updates[:assignee_ids] ||= issuable.assignees.map(&:id)
+ @updates[:assignee_ids] += users.map(&:id)
+ else
+ @updates[:assignee_ids] = [users.first.id]
+ end
end
desc do
@@ -161,12 +163,12 @@ module QuickActions
extract_users(unassign_param) if issuable.allows_multiple_assignees?
end
command :unassign do |users = nil|
- @updates[:assignee_ids] =
- if users&.any?
- issuable.assignees.pluck(:id) - users.map(&:id)
- else
- []
- end
+ if issuable.allows_multiple_assignees? && users&.any?
+ @updates[:assignee_ids] ||= issuable.assignees.map(&:id)
+ @updates[:assignee_ids] -= users.map(&:id)
+ else
+ @updates[:assignee_ids] = []
+ end
end
desc 'Set milestone'
@@ -208,9 +210,14 @@ module QuickActions
end
params '~label1 ~"label 2"'
condition do
- available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute
+ if project
+ available_labels = LabelsFinder
+ .new(current_user, project_id: project.id, include_ancestor_groups: true)
+ .execute
+ end
- current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
+ project &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any?
end
command :label do |labels_param|
@@ -284,7 +291,7 @@ module QuickActions
end
params '#issue | !merge_request'
condition do
- issuable.persisted? &&
+ [MergeRequest, Issue].include?(issuable.class) &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
parse_params do |issuable_param|
@@ -442,7 +449,8 @@ module QuickActions
end
params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
condition do
- current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
+ issuable.is_a?(TimeTrackable) &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
parse_params do |raw_time_date|
Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute
@@ -489,6 +497,30 @@ module QuickActions
"#{comment} #{TABLEFLIP}"
end
+ desc "Lock the discussion"
+ explanation "Locks the discussion"
+ condition do
+ [MergeRequest, Issue].include?(issuable.class) &&
+ issuable.persisted? &&
+ !issuable.discussion_locked? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
+ end
+ command :lock do
+ @updates[:discussion_locked] = true
+ end
+
+ desc "Unlock the discussion"
+ explanation "Unlocks the discussion"
+ condition do
+ [MergeRequest, Issue].include?(issuable.class) &&
+ issuable.persisted? &&
+ issuable.discussion_locked? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
+ end
+ command :unlock do
+ @updates[:discussion_locked] = false
+ end
+
# This is a dummy command, so that it appears in the autocomplete commands
desc 'CC'
params '@user'
@@ -522,6 +554,7 @@ module QuickActions
current_user.can?(:"update_#{issuable.to_ability_name}", issuable) &&
issuable.project.boards.count == 1
end
+ # rubocop: disable CodeReuse/ActiveRecord
command :board_move do |target_list_name|
label_ids = find_label_ids(target_list_name)
@@ -536,6 +569,7 @@ module QuickActions
@updates[:add_label_ids] = [label_id]
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
desc 'Mark this issue as a duplicate of another issue'
explanation do |duplicate_reference|
@@ -601,6 +635,7 @@ module QuickActions
@updates[:tag_message] = message
end
+ # rubocop: disable CodeReuse/ActiveRecord
def extract_users(params)
return [] if params.nil?
@@ -617,6 +652,7 @@ module QuickActions
users
end
+ # rubocop: enable CodeReuse/ActiveRecord
def find_milestones(project, params = {})
MilestonesFinder.new(params.merge(project_ids: [project.id], group_ids: [project.group&.id])).execute
@@ -653,6 +689,7 @@ module QuickActions
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def extract_references(arg, type)
ext = Gitlab::ReferenceExtractor.new(project, current_user)
@@ -660,5 +697,6 @@ module QuickActions
ext.references(type)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/quick_actions/target_service.rb b/app/services/quick_actions/target_service.rb
index d8ba52c6e50..69464c3c1ae 100644
--- a/app/services/quick_actions/target_service.rb
+++ b/app/services/quick_actions/target_service.rb
@@ -15,13 +15,17 @@ module QuickActions
private
+ # rubocop: disable CodeReuse/ActiveRecord
def issue(type_id)
IssuesFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.issues.build
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def merge_request(type_id)
MergeRequestsFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.merge_requests.build
end
+ # rubocop: enable CodeReuse/ActiveRecord
def commit(type_id)
project.commit(type_id)
diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb
index 8edb0ddb3ed..039d6e2ebad 100644
--- a/app/services/resource_events/change_labels_service.rb
+++ b/app/services/resource_events/change_labels_service.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-# This service is not used yet, it will be used for:
-# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483
module ResourceEvents
class ChangeLabelsService
attr_reader :resource, :user
@@ -25,6 +23,7 @@ module ResourceEvents
end
Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels)
+ resource.expire_note_etag_cache
end
private
diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb
new file mode 100644
index 00000000000..596c0105ea0
--- /dev/null
+++ b/app/services/resource_events/merge_into_notes_service.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+# We store events about issuable label changes in a separate table (not as
+# other system notes), but we still want to display notes about label changes
+# as classic system notes in UI. This service generates "synthetic" notes for
+# label event changes and merges them with classic notes and sorts them by
+# creation time.
+
+module ResourceEvents
+ class MergeIntoNotesService
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :resource, :current_user, :params
+
+ def initialize(resource, current_user, params = {})
+ @resource = resource
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute(notes = [])
+ (notes + label_notes).sort_by { |n| n.created_at }
+ end
+
+ private
+
+ def label_notes
+ label_events_by_discussion_id.map do |discussion_id, events|
+ LabelNote.from_events(events, resource: resource, resource_parent: resource_parent)
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def label_events_by_discussion_id
+ return [] unless resource.respond_to?(:resource_label_events)
+
+ events = resource.resource_label_events.includes(:label, :user)
+ events = since_fetch_at(events)
+
+ events.group_by { |event| event.discussion_id }
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def since_fetch_at(events)
+ return events unless params[:last_fetched_at].present?
+
+ last_fetched_at = Time.at(params.fetch(:last_fetched_at).to_i)
+ events.created_after(last_fetched_at - NotesFinder::FETCH_OVERLAP)
+ end
+
+ def resource_parent
+ strong_memoize(:resource_parent) do
+ resource.project || resource.group
+ end
+ end
+ end
+end
diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb
index 34803d005e3..00372887985 100644
--- a/app/services/search/group_service.rb
+++ b/app/services/search/group_service.rb
@@ -11,11 +11,13 @@ module Search
@group = group
end
+ # rubocop: disable CodeReuse/ActiveRecord
def projects
return Project.none unless group
return @projects if defined? @projects
@projects = super.inside_path(group.full_path)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 1b707d79b43..e0cbfac2420 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -8,6 +8,7 @@ class SearchService
@params = params.dup
end
+ # rubocop: disable CodeReuse/ActiveRecord
def project
return @project if defined?(@project)
@@ -19,7 +20,9 @@ class SearchService
nil
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def group
return @group if defined?(@group)
@@ -31,6 +34,7 @@ class SearchService
nil
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def show_snippets?
return @show_snippets if defined?(@show_snippets)
diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb
index 895261925ba..51d300d4f1d 100644
--- a/app/services/spam_check_service.rb
+++ b/app/services/spam_check_service.rb
@@ -22,6 +22,7 @@ module SpamCheckService
# a dirty instance, which means it should be already assigned with the new
# attribute values.
# rubocop:disable Gitlab/ModuleWithInstanceVariables
+ # rubocop: disable CodeReuse/ActiveRecord
def spam_check(spammable, user)
spam_service = SpamService.new(spammable, @request)
@@ -29,5 +30,6 @@ module SpamCheckService
user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb
index 93c2e222963..62222d3fd2a 100644
--- a/app/services/submit_usage_ping_service.rb
+++ b/app/services/submit_usage_ping_service.rb
@@ -15,6 +15,7 @@ class SubmitUsagePingService
def execute
return false unless Gitlab::CurrentSettings.usage_ping_enabled?
+ return false if User.single_user&.requires_usage_stats_consent?
response = Gitlab::HTTP.post(
URL,
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index dda89830179..575678da1fa 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -98,66 +98,45 @@ module SystemNoteService
create_note(NoteSummary.new(issue, project, author, body, action: 'assignee'))
end
- # Called when one or more labels on a Noteable are added and/or removed
+ # Called when the milestone of a Noteable is changed
#
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # added_labels - Array of Labels added
- # removed_labels - Array of Labels removed
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # milestone - Milestone being assigned, or nil
#
# Example Note text:
#
- # "added ~1 and removed ~2 ~3 labels"
- #
- # "added ~4 label"
+ # "removed milestone"
#
- # "removed ~5 label"
+ # "changed milestone to 7.11"
#
# Returns the created Note object
- def change_label(noteable, project, author, added_labels, removed_labels)
- labels_count = added_labels.count + removed_labels.count
-
- references = ->(label) { label.to_reference(format: :id) }
- added_labels = added_labels.map(&references).join(' ')
- removed_labels = removed_labels.map(&references).join(' ')
-
- text_parts = []
-
- if added_labels.present?
- text_parts << "added #{added_labels}"
- text_parts << 'and' if removed_labels.present?
- end
-
- if removed_labels.present?
- text_parts << "removed #{removed_labels}"
- end
-
- text_parts << 'label'.pluralize(labels_count)
- body = text_parts.join(' ')
+ def change_milestone(noteable, project, author, milestone)
+ format = milestone&.group_milestone? ? :name : :iid
+ body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
- create_note(NoteSummary.new(noteable, project, author, body, action: 'label'))
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
end
- # Called when the milestone of a Noteable is changed
+ # Called when the due_date of a Noteable is changed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
- # milestone - Milestone being assigned, or nil
+ # due_date - Due date being assigned, or nil
#
# Example Note text:
#
- # "removed milestone"
+ # "removed due date"
#
- # "changed milestone to 7.11"
+ # "changed due date to September 20, 2018"
#
# Returns the created Note object
- def change_milestone(noteable, project, author, milestone)
- format = milestone&.group_milestone? ? :name : :iid
- body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
+ def change_due_date(noteable, project, author, due_date)
+ body = due_date ? "changed due date to #{due_date.to_s(:long)}" : 'removed due date'
- create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'due_date'))
end
# Called when the estimated time of a Noteable is changed
@@ -601,6 +580,7 @@ module SystemNoteService
private
+ # rubocop: disable CodeReuse/ActiveRecord
def notes_for_mentioner(mentioner, noteable, notes)
if mentioner.is_a?(Commit)
text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
@@ -611,6 +591,7 @@ module SystemNoteService
notes.where(note: [text, text.capitalize])
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def create_note(note_summary)
note = Note.create(note_summary.note.merge(system: true))
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index 800268485a4..6bfef09ac54 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -2,6 +2,7 @@
module Tags
class DestroyService < BaseService
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(tag_name)
repository = project.repository
tag = repository.find_tag(tag_name)
@@ -26,6 +27,7 @@ module Tags
rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def error(message, return_code = 400)
super(message).merge(return_code: return_code)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 0df61ad3bce..4fe6c1ec986 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -41,6 +41,7 @@ class TodoService
# collects the todo users before the todos themselves are deleted, then
# updates the todo counts for those users.
#
+ # rubocop: disable CodeReuse/ActiveRecord
def destroy_target(target)
todo_users = User.where(id: target.todos.pending.select(:user_id)).to_a
@@ -48,6 +49,7 @@ class TodoService
todo_users.each(&:update_todos_count_cache)
end
+ # rubocop: enable CodeReuse/ActiveRecord
# When we reassign an issue we should:
#
@@ -198,16 +200,21 @@ class TodoService
create_todos(current_user, attributes)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def todo_exist?(issuable, current_user)
TodosFinder.new(current_user).execute.exists?(target: issuable)
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
+ # rubocop: disable CodeReuse/ActiveRecord
def todos_by_ids(ids, current_user)
current_user.todos.where(id: Array(ids))
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def update_todos_state(todos, current_user, state)
# Only update those that are not really on that state
todos = todos.where.not(state: state)
@@ -216,6 +223,7 @@ class TodoService
current_user.update_todos_count_cache
todos_ids
end
+ # rubocop: enable CodeReuse/ActiveRecord
def create_todos(users, attributes)
Array(users).map do |user|
@@ -340,8 +348,10 @@ class TodoService
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def pending_todos(user, criteria = {})
valid_keys = [:project_id, :target_id, :target_type, :commit_id]
user.todos.pending.where(criteria.slice(*valid_keys))
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb
index aeb60e50c64..f3f1dbb5698 100644
--- a/app/services/todos/destroy/base_service.rb
+++ b/app/services/todos/destroy/base_service.rb
@@ -11,13 +11,17 @@ module Todos
private
+ # rubocop: disable CodeReuse/ActiveRecord
def without_authorized(items)
items.where('user_id NOT IN (?)', authorized_users)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def authorized_users
ProjectAuthorization.select(:user_id).where(project_id: project_ids)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def todos
raise NotImplementedError
diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb
index efec0f22da5..6276e332448 100644
--- a/app/services/todos/destroy/confidential_issue_service.rb
+++ b/app/services/todos/destroy/confidential_issue_service.rb
@@ -7,18 +7,22 @@ module Todos
attr_reader :issue
+ # rubocop: disable CodeReuse/ActiveRecord
def initialize(issue_id)
@issue = Issue.find_by(id: issue_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
override :todos
+ # rubocop: disable CodeReuse/ActiveRecord
def todos
Todo.where(target: issue)
.where('user_id != ?', issue.author_id)
.where('user_id NOT IN (?)', issue.assignees.select(:id))
end
+ # rubocop: enable CodeReuse/ActiveRecord
override :todos_to_remove?
def todos_to_remove?
@@ -31,11 +35,13 @@ module Todos
end
override :authorized_users
+ # rubocop: disable CodeReuse/ActiveRecord
def authorized_users
ProjectAuthorization.select(:user_id)
.where(project_id: project_ids)
.where('access_level >= ?', Gitlab::Access::REPORTER)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb
index 4cb9d08713d..e8d1bcdd142 100644
--- a/app/services/todos/destroy/entity_leave_service.rb
+++ b/app/services/todos/destroy/entity_leave_service.rb
@@ -7,6 +7,7 @@ module Todos
attr_reader :user, :entity
+ # rubocop: disable CodeReuse/ActiveRecord
def initialize(user_id, entity_id, entity_type)
unless %w(Group Project).include?(entity_type)
raise ArgumentError.new("#{entity_type} is not an entity user can leave")
@@ -15,6 +16,7 @@ module Todos
@user = User.find_by(id: user_id)
@entity = entity_type.constantize.find_by(id: entity_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def execute
return unless entity && user
@@ -40,21 +42,28 @@ module Todos
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def remove_confidential_issue_todos
Todo.where(
target_id: confidential_issues.select(:id), target_type: Issue, user_id: user.id
).delete_all
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def remove_project_todos
Todo.where(project_id: non_authorized_projects, user_id: user.id).delete_all
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def remove_group_todos
Todo.where(group_id: non_authorized_groups, user_id: user.id).delete_all
end
+ # rubocop: enable CodeReuse/ActiveRecord
override :project_ids
+ # rubocop: disable CodeReuse/ActiveRecord
def project_ids
condition = case entity
when Project
@@ -65,22 +74,29 @@ module Todos
Project.where(condition).select(:id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def non_authorized_projects
project_ids.where('id NOT IN (?)', user.authorized_projects.select(:id))
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def non_authorized_groups
return [] unless entity.is_a?(Namespace)
entity.self_and_descendants.select(:id)
.where('id NOT IN (?)', GroupsFinder.new(user).execute.select(:id))
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def non_member_groups
entity.self_and_descendants.select(:id)
.where('id NOT IN (?)', user.membership_groups.select(:id))
end
+ # rubocop: enable CodeReuse/ActiveRecord
def user_has_reporter_access?
return unless entity.is_a?(Namespace)
@@ -88,6 +104,7 @@ module Todos
entity.member?(User.find(user.id), Gitlab::Access::REPORTER)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def confidential_issues
assigned_ids = IssueAssignee.select(:issue_id).where(user_id: user.id)
authorized_reporter_projects = user
@@ -98,6 +115,7 @@ module Todos
.where('author_id != ?', user.id)
.where('id NOT IN (?)', assigned_ids)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/services/todos/destroy/group_private_service.rb b/app/services/todos/destroy/group_private_service.rb
index f67f1d40597..d7ecbb952aa 100644
--- a/app/services/todos/destroy/group_private_service.rb
+++ b/app/services/todos/destroy/group_private_service.rb
@@ -7,16 +7,20 @@ module Todos
attr_reader :group
+ # rubocop: disable CodeReuse/ActiveRecord
def initialize(group_id)
@group = Group.find_by(id: group_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
override :todos
+ # rubocop: disable CodeReuse/ActiveRecord
def todos
Todo.where(group_id: group.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
override :authorized_users
def authorized_users
diff --git a/app/services/todos/destroy/private_features_service.rb b/app/services/todos/destroy/private_features_service.rb
index 7e204885b31..a8c3fe0ef5a 100644
--- a/app/services/todos/destroy/private_features_service.rb
+++ b/app/services/todos/destroy/private_features_service.rb
@@ -10,6 +10,7 @@ module Todos
@user_id = user_id
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute
ProjectFeature.where(project_id: project_ids).each do |project_features|
target_types = []
@@ -22,6 +23,7 @@ module Todos
remove_todos(project_features.project_id, target_types)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
@@ -29,6 +31,7 @@ module Todos
feature_level == ProjectFeature::PRIVATE
end
+ # rubocop: disable CodeReuse/ActiveRecord
def remove_todos(project_id, target_types)
items = Todo.where(project_id: project_id)
items = items.where(user_id: user_id) if user_id
@@ -37,6 +40,7 @@ module Todos
.where(target_type: target_types)
.delete_all
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/services/todos/destroy/project_private_service.rb b/app/services/todos/destroy/project_private_service.rb
index ae8fab3ffca..e00d10c3780 100644
--- a/app/services/todos/destroy/project_private_service.rb
+++ b/app/services/todos/destroy/project_private_service.rb
@@ -7,16 +7,20 @@ module Todos
attr_reader :project
+ # rubocop: disable CodeReuse/ActiveRecord
def initialize(project_id)
@project = Project.find_by(id: project_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
override :todos
+ # rubocop: disable CodeReuse/ActiveRecord
def todos
Todo.where(project_id: project.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
override :project_ids
def project_ids
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
index 422ba668e35..e2228ca026c 100644
--- a/app/services/update_release_service.rb
+++ b/app/services/update_release_service.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class UpdateReleaseService < BaseService
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(tag_name, release_description)
repository = project.repository
existing_tag = repository.find_tag(tag_name)
@@ -19,6 +20,7 @@ class UpdateReleaseService < BaseService
error('Tag does not exist', 404)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def success(release)
super().merge(release: release)
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index 9417c63c43a..de6ff92d1da 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -55,7 +55,6 @@ module Users
:force_random_password,
:hide_no_password,
:hide_no_ssh_key,
- :key_id,
:linkedin,
:name,
:password,
@@ -69,7 +68,10 @@ module Users
:twitter,
:username,
:website_url,
- :private_profile
+ :private_profile,
+ :organization,
+ :location,
+ :public_email
]
end
diff --git a/app/services/users/last_push_event_service.rb b/app/services/users/last_push_event_service.rb
index a9c9497520b..b3980b8e32c 100644
--- a/app/services/users/last_push_event_service.rb
+++ b/app/services/users/last_push_event_service.rb
@@ -58,11 +58,13 @@ module Users
private
+ # rubocop: disable CodeReuse/ActiveRecord
def find_event_in_database(id)
PushEvent
.without_existing_merge_requests
.find_by(id: id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def user_cache_key
"last-push-event/#{@user.id}"
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index 4d47078bf43..04fd6e37501 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -54,15 +54,19 @@ module Users
migrate_award_emoji
end
+ # rubocop: disable CodeReuse/ActiveRecord
def migrate_issues
user.issues.update_all(author_id: ghost_user.id)
Issue.where(last_edited_by_id: user.id).update_all(last_edited_by_id: ghost_user.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def migrate_merge_requests
user.merge_requests.update_all(author_id: ghost_user.id)
MergeRequest.where(merge_user_id: user.id).update_all(merge_user_id: ghost_user.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def migrate_notes
user.notes.update_all(author_id: ghost_user.id)
diff --git a/app/services/users/respond_to_terms_service.rb b/app/services/users/respond_to_terms_service.rb
index 9efa3b285a8..254480304f9 100644
--- a/app/services/users/respond_to_terms_service.rb
+++ b/app/services/users/respond_to_terms_service.rb
@@ -6,6 +6,7 @@ module Users
@user, @term = user, term
end
+ # rubocop: disable CodeReuse/ActiveRecord
def execute(accepted:)
agreement = @user.term_agreements.find_or_initialize_by(term: @term)
agreement.accepted = accepted
@@ -16,6 +17,7 @@ module Users
agreement
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb
new file mode 100644
index 00000000000..df31ad7c8ea
--- /dev/null
+++ b/app/services/wikis/create_attachment_service.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module Wikis
+ class CreateAttachmentService < Files::CreateService
+ ATTACHMENT_PATH = 'uploads'.freeze
+ MAX_FILENAME_LENGTH = 255
+
+ delegate :wiki, to: :project
+ delegate :repository, to: :wiki
+
+ def initialize(*args)
+ super
+
+ @file_name = clean_file_name(params[:file_name])
+ @file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name
+ @commit_message ||= "Upload attachment #{@file_name}"
+ @branch_name ||= wiki.default_branch
+ end
+
+ def create_commit!
+ commit_result(create_transformed_commit(@file_content))
+ end
+
+ private
+
+ def clean_file_name(file_name)
+ return unless file_name.present?
+
+ file_name = truncate_file_name(file_name)
+ # CommonMark does not allow Urls with whitespaces, so we have to replace them
+ # Using the same regex Carrierwave use to replace invalid characters
+ file_name.gsub(CarrierWave::SanitizedFile.sanitize_regexp, '_')
+ end
+
+ def truncate_file_name(file_name)
+ return file_name if file_name.length <= MAX_FILENAME_LENGTH
+
+ extension = File.extname(file_name)
+ truncate_at = MAX_FILENAME_LENGTH - extension.length - 1
+ base_name = File.basename(file_name, extension)[0..truncate_at]
+ base_name + extension
+ end
+
+ def validate!
+ validate_file_name!
+ validate_permissions!
+ end
+
+ def validate_file_name!
+ raise_error('The file name cannot be empty') unless @file_name
+ end
+
+ def validate_permissions!
+ unless can?(current_user, :create_wiki, project)
+ raise_error('You are not allowed to push to the wiki')
+ end
+ end
+
+ def create_transformed_commit(content)
+ repository.create_file(
+ current_user,
+ @file_path,
+ content,
+ message: @commit_message,
+ branch_name: @branch_name,
+ author_email: @author_email,
+ author_name: @author_name)
+ end
+
+ def commit_result(commit_id)
+ {
+ file_name: @file_name,
+ file_path: @file_path,
+ branch: @branch_name,
+ commit: commit_id
+ }
+ end
+ end
+end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index b29ef57b071..c0165759203 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -18,6 +18,10 @@ class AvatarUploader < GitlabUploader
false
end
+ def absolute_path
+ self.class.absolute_path(model.avatar.upload)
+ end
+
private
def dynamic_segment
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index b1365659834..ffc1e5f75ca 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -122,12 +122,6 @@ class FileUploader < GitlabUploader
}
end
- def markdown_link
- markdown = +"[#{markdown_name}](#{secure_url})"
- markdown.prepend("!") if image_or_video? || dangerous?
- markdown
- end
-
def to_h
{
alt: markdown_name,
@@ -192,10 +186,6 @@ class FileUploader < GitlabUploader
storage.delete_dir!(store_dir) # only remove when empty
end
- def markdown_name
- (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
- end
-
def identifier
@identifier ||= filename
end
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 719bd6ef418..cefcd3d3f5a 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -63,6 +63,12 @@ class GitlabUploader < CarrierWave::Uploader::Base
super || file&.filename
end
+ def relative_path
+ return path if pathname.relative?
+
+ pathname.relative_path_from(Pathname.new(root))
+ end
+
def model_valid?
!!model
end
@@ -115,4 +121,8 @@ class GitlabUploader < CarrierWave::Uploader::Base
# the cache directory.
File.join(work_dir, cache_id, version_name.to_s, for_file)
end
+
+ def pathname
+ @pathname ||= Pathname.new(path)
+ end
end
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index f6af023e0f9..400f0b3dcc6 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -5,9 +5,12 @@ class JobArtifactUploader < GitlabUploader
include ObjectStorage::Concern
ObjectNotReadyError = Class.new(StandardError)
+ UnknownFileLocationError = Class.new(StandardError)
storage_options Gitlab.config.artifacts
+ alias_method :upload, :model
+
def cached_size
return model.size if model.size.present? && !model.file_changed?
@@ -23,10 +26,22 @@ class JobArtifactUploader < GitlabUploader
def dynamic_segment
raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id
- creation_date = model.created_at.utc.strftime('%Y_%m_%d')
+ if model.hashed_path?
+ hashed_path
+ elsif model.legacy_path?
+ legacy_path
+ else
+ raise UnknownFileLocationError
+ end
+ end
+ def hashed_path
File.join(disk_hash[0..1], disk_hash[2..3], disk_hash,
- creation_date, model.job_id.to_s, model.id.to_s)
+ model.created_at.utc.strftime('%Y_%m_%d'), model.job_id.to_s, model.id.to_s)
+ end
+
+ def legacy_path
+ File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.job_id.to_s)
end
def disk_hash
diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb
index b4d0d752016..a9afc104ed1 100644
--- a/app/uploaders/legacy_artifact_uploader.rb
+++ b/app/uploaders/legacy_artifact_uploader.rb
@@ -8,6 +8,8 @@ class LegacyArtifactUploader < GitlabUploader
storage_options Gitlab.config.artifacts
+ alias_method :upload, :model
+
def store_dir
dynamic_segment
end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index f3d32e6b39d..0a966f3d44f 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -6,6 +6,8 @@ class LfsObjectUploader < GitlabUploader
storage_options Gitlab.config.lfs
+ alias_method :upload, :model
+
def filename
model.oid[4..-1]
end
diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb
index 52969762b7d..4965bd7f057 100644
--- a/app/uploaders/namespace_file_uploader.rb
+++ b/app/uploaders/namespace_file_uploader.rb
@@ -6,23 +6,27 @@ class NamespaceFileUploader < FileUploader
options.storage_path
end
- def self.base_dir(model, _store = nil)
- File.join(options.base_dir, 'namespace', model_path_segment(model))
+ def self.base_dir(model, store = nil)
+ base_dirs(model)[store || Store::LOCAL]
+ end
+
+ def self.base_dirs(model)
+ {
+ Store::LOCAL => File.join(options.base_dir, 'namespace', model_path_segment(model)),
+ Store::REMOTE => File.join('namespace', model_path_segment(model))
+ }
end
def self.model_path_segment(model)
File.join(model.id.to_s)
end
+ def self.workhorse_local_upload_path
+ File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH)
+ end
+
# Re-Override
def store_dir
store_dirs[object_store]
end
-
- def store_dirs
- {
- Store::LOCAL => File.join(base_dir, dynamic_segment),
- Store::REMOTE => File.join('namespace', self.class.model_path_segment(model), dynamic_segment)
- }
- end
end
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
index 5795065ae11..0efca895a50 100644
--- a/app/uploaders/records_uploads.rb
+++ b/app/uploaders/records_uploads.rb
@@ -18,6 +18,7 @@ module RecordsUploads
# `Tempfile` object the callback gets.
#
# Called `after :store`
+ # rubocop: disable CodeReuse/ActiveRecord
def record_upload(_tempfile = nil)
return unless model
return unless file && file.exists?
@@ -29,6 +30,7 @@ module RecordsUploads
self.upload = build_upload.tap(&:save!)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def upload_path
File.join(store_dir, filename.to_s)
@@ -36,9 +38,11 @@ module RecordsUploads
private
+ # rubocop: disable CodeReuse/ActiveRecord
def uploads
Upload.order(id: :desc).where(uploader: self.class.to_s)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def build_upload
Upload.new(
@@ -53,11 +57,13 @@ module RecordsUploads
# Before removing an attachment, destroy any Upload records at the same path
#
# Called `before :remove`
+ # rubocop: disable CodeReuse/ActiveRecord
def destroy_upload(*args)
return unless file && file.exists?
self.upload = nil
uploads.where(path: upload_path).delete_all
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index 2a2b54a9270..e8a2dce7755 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -2,32 +2,7 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
- # We recommend using the .mp4 format over .mov. Videos in .mov format can
- # still be used but you really need to make sure they are served with the
- # proper MIME type video/mp4 and not video/quicktime or your videos won't play
- # on IE >= 9.
- # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
- VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
- # These extension types can contain dangerous code and should only be embedded inline with
- # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
- DANGEROUS_EXT = %w[svg].freeze
-
- def image?
- extension_match?(IMAGE_EXT)
- end
-
- def video?
- extension_match?(VIDEO_EXT)
- end
-
- def image_or_video?
- image? || video?
- end
-
- def dangerous?
- extension_match?(DANGEROUS_EXT)
- end
+ include Gitlab::FileMarkdownLinkBuilder
private
diff --git a/app/validators/branch_filter_validator.rb b/app/validators/branch_filter_validator.rb
new file mode 100644
index 00000000000..6a0899be850
--- /dev/null
+++ b/app/validators/branch_filter_validator.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+# BranchFilterValidator
+#
+# Custom validator for branch names. Squishes whitespace and ignores empty
+# string. This only checks that a string is a valid git branch name. It does
+# not check whether a branch already exists.
+#
+# Example:
+#
+# class Webhook < ActiveRecord::Base
+# validates :push_events_branch_filter, branch_name: true
+# end
+#
+class BranchFilterValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ value.squish! unless value.nil?
+
+ if value.present?
+ value_without_wildcards = value.tr('*', 'x')
+
+ unless Gitlab::GitRefValidator.validate(value_without_wildcards)
+ record.errors[attribute] << "is not a valid branch name"
+ end
+
+ unless value.length <= 4000
+ record.errors[attribute] << "is longer than the allowed length of 4000 characters."
+ end
+ end
+ end
+
+ private
+
+ def contains_wildcard?(value)
+ value.include?('*')
+ end
+end
diff --git a/app/validators/js_regex_validator.rb b/app/validators/js_regex_validator.rb
index a515af7b919..be715967b4a 100644
--- a/app/validators/js_regex_validator.rb
+++ b/app/validators/js_regex_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class JsRegexValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return true if value.blank?
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index faaf1283078..216acf79cbd 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -41,12 +41,13 @@ class UrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
@record = record
- if value.present?
- value.strip!
- else
+ unless value.present?
record.errors.add(attribute, 'must be a valid URL')
+ return
end
+ value = strip_value!(record, attribute, value)
+
Gitlab::UrlBlocker.validate!(value, blocker_args)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
record.errors.add(attribute, "is blocked: #{e.message}")
@@ -54,6 +55,13 @@ class UrlValidator < ActiveModel::EachValidator
private
+ def strip_value!(record, attribute, value)
+ new_value = value.strip
+ return value if new_value == value
+
+ record.public_send("#{attribute}=", new_value) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
def default_options
# By default the validator doesn't block any url based on the ip address
{
diff --git a/app/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb
index 90193e85f2a..d36a56e81b9 100644
--- a/app/validators/variable_duplicates_validator.rb
+++ b/app/validators/variable_duplicates_validator.rb
@@ -21,6 +21,7 @@ class VariableDuplicatesValidator < ActiveModel::EachValidator
private
+ # rubocop: disable CodeReuse/ActiveRecord
def validate_duplicates(record, attribute, values)
duplicates = values.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
if duplicates.any?
@@ -29,4 +30,5 @@ class VariableDuplicatesValidator < ActiveModel::EachValidator
record.errors.add(attribute, error_message)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index 278ad210543..391115a67b5 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -19,4 +19,4 @@
Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
.form-actions
- = f.submit "Send report", class: "btn btn-create"
+ = f.submit "Send report", class: "btn btn-success"
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index a0861870ba4..cb67079853e 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -5,7 +5,7 @@
%legend
Navigation bar:
.form-group.row
- = f.label :header_logo, 'Header logo', class: 'col-sm-2 col-form-label'
+ = f.label :header_logo, 'Header logo', class: 'col-sm-2 col-form-label pt-0'
.col-sm-10
- if @appearance.header_logo?
= image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
@@ -22,7 +22,7 @@
%legend
Favicon:
.form-group.row
- = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label'
+ = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label pt-0'
.col-sm-10
- if @appearance.favicon?
= image_tag @appearance.favicon_url, class: 'appearance-light-logo-preview'
@@ -51,7 +51,7 @@
.hint
Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
.form-group.row
- = f.label :logo, class: 'col-sm-2 col-form-label'
+ = f.label :logo, class: 'col-sm-2 col-form-label pt-0'
.col-sm-10
- if @appearance.logo?
= image_tag @appearance.logo_url, class: 'appearance-logo-preview'
@@ -75,7 +75,7 @@
Guidelines parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
.form-actions
- = f.submit 'Save', class: 'btn btn-save append-right-10'
+ = f.submit 'Save', class: 'btn btn-success append-right-10'
- if @appearance.persisted?
Preview last save:
= link_to 'Sign-in page', preview_sign_in_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/admin/appearances/preview_sign_in.html.haml b/app/views/admin/appearances/preview_sign_in.html.haml
index 1af7dd5bb67..2cd95071c73 100644
--- a/app/views/admin/appearances/preview_sign_in.html.haml
+++ b/app/views/admin/appearances/preview_sign_in.html.haml
@@ -8,5 +8,5 @@
= label_tag :password
= password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.'
.form-group
- = button_tag "Sign in", class: "btn-create btn"
+ = button_tag "Sign in", class: "btn-success btn"
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index 9121e44d31b..10bc3452d8b 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -14,7 +14,10 @@
= f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'label-bold'
= f.number_field :max_attachment_size, class: 'form-control'
.form-group
- = f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-bold'
+ = f.label :receive_max_input_size, 'Maximum push size (MB)', class: 'label-light'
+ = f.number_field :receive_max_input_size, class: 'form-control'
+ .form-group
+ = f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-light'
= f.number_field :session_expire_delay, class: 'form-control'
%span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes
.form-group
diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml
deleted file mode 100644
index 7d1a64b645a..00000000000
--- a/app/views/admin/application_settings/_background_jobs.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-background-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
-
- %fieldset
- %p
- These settings require a
- = link_to 'restart', help_page_path('administration/restart_gitlab')
- to take effect.
- .form-group
- .form-check
- = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input'
- = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do
- Enable Sidekiq Job Throttling
- .form-text.text-muted
- Limit the amount of resources slow running jobs are assigned.
- .form-group
- = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'label-bold'
- = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
- .form-text.text-muted
- Choose which queues you wish to throttle.
- .form-group
- = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'label-bold'
- = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
- .form-text.text-muted
- The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
-
- = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_diff_limits.html.haml b/app/views/admin/application_settings/_diff_limits.html.haml
new file mode 100644
index 00000000000..408e569fe07
--- /dev/null
+++ b/app/views/admin/application_settings/_diff_limits.html.haml
@@ -0,0 +1,16 @@
+= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-merge-request-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :diff_max_patch_bytes, 'Maximum diff patch size (Bytes)', class: 'label-light'
+ = f.number_field :diff_max_patch_bytes, class: 'form-control'
+ %span.form-text.text-muted
+ Diff files surpassing this limit will be presented as 'too large'
+ and won't be expandable.
+
+ = link_to icon('question-circle'),
+ help_page_path('user/admin_area/diff_limits',
+ anchor: 'maximum-diff-patch-size')
+
+ = f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml
index a1eeacd8290..dc5cbb8fa94 100644
--- a/app/views/admin/application_settings/_influx.html.haml
+++ b/app/views/admin/application_settings/_influx.html.haml
@@ -3,7 +3,7 @@
%fieldset
%p
- Setup InfluxDB to measure a wide variety of statistics like the time spent
+ Set up InfluxDB to measure a wide variety of statistics like the time spent
in running SQL queries. These settings require a
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
index c94f4c74820..615aa6317b0 100644
--- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -7,9 +7,9 @@
.form-check
= f.check_box :mirror_available, class: 'form-check-input'
= f.label :mirror_available, class: 'form-check-label' do
- Allow mirrors to be setup for projects
+ Allow mirrors to be set up for projects
%span.form-text.text-muted
- If disabled, only admins will be able to setup mirrors in projects.
+ If disabled, only admins will be able to set up mirrors in projects.
= link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index 4523332493b..908b30cc3ce 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -5,7 +5,7 @@
.sub-section
.form-group
.form-check
- = f.check_box :hashed_storage_enabled, class: 'form-check-input'
+ = f.check_box :hashed_storage_enabled, class: 'form-check-input qa-hashed-storage-checkbox'
= f.label :hashed_storage_enabled, class: 'form-check-label' do
Use hashed storage paths for newly created and renamed projects
.form-text.text-muted
@@ -48,4 +48,4 @@
.form-text.text-muted
= circuitbreaker_failure_reset_time_help_text
- = f.submit 'Save changes', class: "btn btn-success"
+ = f.submit 'Save changes', class: "btn btn-success qa-save-changes-button"
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 635a6751e5b..5f36358f599 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -31,7 +31,7 @@
.form-check
= f.check_box :require_two_factor_authentication, class: 'form-check-input'
= f.label :require_two_factor_authentication, class: 'form-check-label' do
- Require all users to setup Two-factor authentication
+ Require all users to set up Two-factor authentication
.form-group
= f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'label-bold'
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 2495defb6a7..788595877ea 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -2,7 +2,7 @@
= form_errors(@application_setting)
%fieldset
- .form-group
+ .form-group.mb-2
.form-check
= f.check_box :version_check_enabled, class: 'form-check-input'
= f.label :version_check_enabled, class: 'form-check-label' do
@@ -16,23 +16,26 @@
.form-check
= f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input'
= f.label :usage_ping_enabled, class: 'form-check-label' do
- Enable usage ping
+ = _('Enable usage ping')
.form-text.text-muted
- if can_be_configured
- To help improve GitLab and its user experience, GitLab will
- periodically collect usage information.
- = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
- about what information is shared with GitLab Inc. Visit
- = link_to _('Cohorts'), instance_statistics_cohorts_path(anchor: 'usage-ping')
- to see the JSON payload sent.
+ %p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.')
+
+ - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
+ - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
+ %p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
+
+ %button.btn.js-usage-ping-payload-trigger{ type: 'button' }
+ .js-spinner.d-none= icon('spinner spin')
+ .js-text.d-inline= _('Preview payload')
+ %pre.usage-data.js-usage-ping-payload.js-syntax-highlight.code.highlight.mt-2.d-none{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else
- The usage ping is disabled, and cannot be configured through this
- form. For more information, see the documentation on
- = succeed '.' do
- = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
- .form-group
+ = _('The usage ping is disabled, and cannot be configured through this form.')
+ - deactivating_usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
+ - deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path }
+ = s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe }
+ .form-group.mt-3
= f.label :instance_statistics_visibility_private, _('Instance Statistics visibility')
= f.select :instance_statistics_visibility_private, options_for_select({_('All users') => false, _('Only admins') => true}, Gitlab::CurrentSettings.instance_statistics_visibility_private?), {}, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
-
diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml
new file mode 100644
index 00000000000..db24c9982f7
--- /dev/null
+++ b/app/views/admin/application_settings/ci_cd.html.haml
@@ -0,0 +1,26 @@
+- breadcrumb_title _("CI/CD")
+- page_title _("CI/CD")
+- @content_class = "limit-container-width" unless fluid_layout
+
+%section.settings.as-ci-cd.no-animate#js-ci-cd-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Continuous Integration and Deployment')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Auto DevOps, runners and job artifacts')
+ .settings-content
+ = render 'ci_cd'
+
+- if Gitlab.config.registry.enabled
+ %section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Container Registry')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Various container registry settings.')
+ .settings-content
+ = render 'registry'
diff --git a/app/views/admin/application_settings/integrations.html.haml b/app/views/admin/application_settings/integrations.html.haml
new file mode 100644
index 00000000000..310e86b1377
--- /dev/null
+++ b/app/views/admin/application_settings/integrations.html.haml
@@ -0,0 +1,31 @@
+- breadcrumb_title _("Integrations")
+- page_title _("Integrations")
+- @content_class = "limit-container-width" unless fluid_layout
+
+= render_if_exists 'admin/application_settings/elasticsearch_form', expanded: expanded_by_default?
+
+%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('PlantUML')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
+ .settings-content
+ = render 'plantuml'
+
+= render_if_exists 'admin/application_settings/slack', expanded: expanded_by_default?
+
+%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Third party offers')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Control the display of third party offers.')
+ .settings-content
+ = render 'third_party_offers', application_setting: @application_setting
+
+= render_if_exists 'admin/application_settings/snowplow', expanded: expanded_by_default?
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
new file mode 100644
index 00000000000..f50aca32bdf
--- /dev/null
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -0,0 +1,50 @@
+- breadcrumb_title _("Metrics and profiling")
+- page_title _("Metrics and profiling")
+- @content_class = "limit-container-width" unless fluid_layout
+
+%section.settings.as-influx.no-animate#js-influx-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Metrics - Influx')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Enable and configure InfluxDB metrics.')
+ .settings-content
+ = render 'influx'
+
+%section.settings.as-prometheus.no-animate#js-prometheus-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Metrics - Prometheus')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Enable and configure Prometheus metrics.')
+ .settings-content
+ = render 'prometheus'
+
+%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Profiling - Performance bar')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Enable the Performance Bar for a given group.')
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
+ .settings-content
+ = render 'performance_bar'
+
+%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header#usage-statistics
+ %h4
+ = _('Usage statistics')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Enable or disable version check and usage ping.')
+ .settings-content
+ = render 'usage'
+
+= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded_by_default?
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
new file mode 100644
index 00000000000..26fd745f45f
--- /dev/null
+++ b/app/views/admin/application_settings/network.html.haml
@@ -0,0 +1,36 @@
+- breadcrumb_title _("Network")
+- page_title _("Network")
+- @content_class = "limit-container-width" unless fluid_layout
+
+%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Performance optimization')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Various settings that affect GitLab performance.')
+ .settings-content
+ = render 'performance'
+
+%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('User and IP Rate Limits')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Configure limits for web and API requests.')
+ .settings-content
+ = render 'ip_limits'
+
+%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Outbound requests')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Allow requests to the local network from hooks and services.')
+ .settings-content
+ = render 'outbound'
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
new file mode 100644
index 00000000000..00000b86ab7
--- /dev/null
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -0,0 +1,58 @@
+- breadcrumb_title _("Preferences")
+- page_title _("Preferences")
+- @content_class = "limit-container-width" unless fluid_layout
+
+%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Email')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Various email settings.')
+ .settings-content
+ = render 'email'
+
+%section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Help page')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Help page text and support page url.')
+ .settings-content
+ = render 'help_page'
+
+%section.settings.as-pages.no-animate#js-pages-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Pages')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Size and domain settings for static websites')
+ .settings-content
+ = render 'pages'
+
+%section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Real-time features')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Change this value to influence how frequently the GitLab UI polls for updates.')
+ .settings-content
+ = render 'realtime'
+
+%section.settings.as-gitaly.no-animate#js-gitaly-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Gitaly')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Configure Gitaly timeouts.')
+ .settings-content
+ = render 'gitaly'
diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml
new file mode 100644
index 00000000000..1c2d9ccdb2d
--- /dev/null
+++ b/app/views/admin/application_settings/reporting.html.haml
@@ -0,0 +1,36 @@
+- breadcrumb_title _("Reporting")
+- page_title _("Reporting")
+- @content_class = "limit-container-width" unless fluid_layout
+
+%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Spam and Anti-bot Protection')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Enable reCAPTCHA or Akismet and set IP limits.')
+ .settings-content
+ = render 'spam'
+
+%section.settings.as-abuse.no-animate#js-abuse-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Abuse reports')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Set notification email for abuse reports.')
+ .settings-content
+ = render 'abuse'
+
+%section.settings.as-logging.no-animate#js-logging-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Error Reporting and Logging')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Enable Sentry for error reporting and logging.')
+ .settings-content
+ = render 'logging'
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
new file mode 100644
index 00000000000..be13138a764
--- /dev/null
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -0,0 +1,36 @@
+- breadcrumb_title _("Repository")
+- page_title _("Repository")
+- @content_class = "limit-container-width" unless fluid_layout
+
+%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Repository mirror')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure push mirrors.')
+ .settings-content
+ = render partial: 'repository_mirrors_form'
+
+%section.settings.qa-repository-storage-settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Repository storage')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Configure storage path and circuit breaker settings.')
+ .settings-content
+ = render 'repository_storage'
+
+%section.settings.as-repository-check.no-animate#js-repository-check-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Repository maintenance')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Configure automatic git checks and housekeeping on repositories.')
+ .settings-content
+ = render 'repository_check'
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 6133a7646f4..279db189a24 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -1,351 +1,104 @@
-- breadcrumb_title "Settings"
-- page_title "Settings"
+- breadcrumb_title _("Settings")
+- page_title _("Settings")
- @content_class = "limit-container-width" unless fluid_layout
-- expanded = Rails.env.test?
-%section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded) }
+%section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Visibility and access controls')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
+ = expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
.settings-content
= render 'visibility_and_access'
-%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded) }
+%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Account and limit')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
+ = expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Session expiration, projects limit and attachment size.')
.settings-content
= render 'account_and_limit'
-%section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded) }
+%section.settings.as-diff-limits.no-animate#js-merge-request-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Diff limits')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Diff content limits')
+ .settings-content
+ = render 'diff_limits'
+
+%section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Sign-up restrictions')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
+ = expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure the way a user creates a new account.')
.settings-content
= render 'signup'
-%section.settings.as-signin.no-animate#js-signin-settings{ class: ('expanded' if expanded) }
+%section.settings.as-signin.no-animate#js-signin-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Sign-in restrictions')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
+ = expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
.settings-content
= render 'signin'
-%section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded) }
+%section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Terms of Service and Privacy Policy')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
+ = expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Include a Terms of Service agreement and Privacy Policy that all users must accept.')
.settings-content
= render 'terms'
-%section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Help page')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Help page text and support page url.')
- .settings-content
- = render 'help_page'
-
-%section.settings.as-pages.no-animate#js-pages-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Pages')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Size and domain settings for static websites')
- .settings-content
- = render 'pages'
-
-%section.settings.as-ci-cd.no-animate#js-ci-cd-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Continuous Integration and Deployment')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Auto DevOps, runners and job artifacts')
- .settings-content
- = render 'ci_cd'
-
-%section.settings.as-influx.no-animate#js-influx-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Metrics - Influx')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Enable and configure InfluxDB metrics.')
- .settings-content
- = render 'influx'
-
-%section.settings.as-prometheus.no-animate#js-prometheus-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Metrics - Prometheus')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Enable and configure Prometheus metrics.')
- .settings-content
- = render 'prometheus'
-
-%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Profiling - Performance bar')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Enable the Performance Bar for a given group.')
- = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
- .settings-content
- = render 'performance_bar'
-
-%section.settings.as-background.no-animate#js-background-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Background jobs')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Configure Sidekiq job throttling.')
- .settings-content
- = render 'background_jobs'
-
-%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Spam and Anti-bot Protection')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Enable reCAPTCHA or Akismet and set IP limits.')
- .settings-content
- = render 'spam'
-
-%section.settings.as-abuse.no-animate#js-abuse-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Abuse reports')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Set notification email for abuse reports.')
- .settings-content
- = render 'abuse'
-
-%section.settings.as-logging.no-animate#js-logging-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Error Reporting and Logging')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Enable Sentry for error reporting and logging.')
- .settings-content
- = render 'logging'
-
-%section.qa-repository-storage-settings.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Repository storage')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Configure storage path and circuit breaker settings.')
- .settings-content
- = render 'repository_storage'
-
-%section.settings.as-repository-check.no-animate#js-repository-check-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Repository maintenance')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Configure automatic git checks and housekeeping on repositories.')
- .settings-content
- = render 'repository_check'
-
-- if Gitlab.config.registry.enabled
- %section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Container Registry')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Various container registry settings.')
- .settings-content
- = render 'registry'
-
- if koding_enabled?
- %section.settings.as-koding.no-animate#js-koding-settings{ class: ('expanded' if expanded) }
+ %section.settings.as-koding.no-animate#js-koding-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Koding')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
+ = expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Online IDE integration settings.')
.settings-content
= render 'koding'
-%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('PlantUML')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
- .settings-content
- = render 'plantuml'
-
-%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded) }
- .settings-header#usage-statistics
- %h4
- = _('Usage statistics')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Enable or disable version check and usage ping.')
- .settings-content
- = render 'usage'
-
-%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Email')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Various email settings.')
- .settings-content
- = render 'email'
-
-%section.settings.as-gitaly.no-animate#js-gitaly-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Gitaly')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Configure Gitaly timeouts.')
- .settings-content
- = render 'gitaly'
+= render_if_exists 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default?
-%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded) }
+%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Web terminal')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
+ = expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set max session time for web terminal.')
.settings-content
= render 'terminal'
-%section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Real-time features')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Change this value to influence how frequently the GitLab UI polls for updates.')
- .settings-content
- = render 'realtime'
-
-%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Performance optimization')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Various settings that affect GitLab performance.')
- .settings-content
- = render 'performance'
-
-%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('User and IP Rate Limits')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Configure limits for web and API requests.')
- .settings-content
- = render 'ip_limits'
-
-%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Outbound requests')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Allow requests to the local network from hooks and services.')
- .settings-content
- = render 'outbound'
-
-%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Repository mirror')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
- %p
- = _('Configure push mirrors.')
- .settings-content
- = render partial: 'repository_mirrors_form'
-
-= render_if_exists 'admin/application_settings/templates', expanded: expanded
-
-%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Third party offers')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Control the display of third party offers.')
- .settings-content
- = render 'third_party_offers', application_setting: @application_setting
-
-= render_if_exists 'admin/application_settings/custom_templates_form', expanded: expanded
-
-%section.settings.no-animate#js-web-ide-settings{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-web-ide-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Web IDE')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
+ = expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Manage Web IDE features')
.settings-content
@@ -362,5 +115,3 @@
= s_('IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation.')
= f.submit _('Save changes'), class: "btn btn-success"
-
-= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 7f14cddebd8..12690343f6e 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -21,17 +21,17 @@
for local tests
= content_tag :div, class: 'form-group row' do
- = f.label :trusted, class: 'col-sm-2 col-form-label'
+ = f.label :trusted, class: 'col-sm-2 col-form-label pt-0'
.col-sm-10
= f.check_box :trusted
%span.form-text.text-muted
Trusted applications are automatically authorized on GitLab OAuth flow.
.form-group.row
- = f.label :scopes, class: 'col-sm-2 col-form-label'
+ = f.label :scopes, class: 'col-sm-2 col-form-label pt-0'
.col-sm-10
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
.form-actions
- = f.submit 'Submit', class: "btn btn-save wide"
+ = f.submit 'Submit', class: "btn btn-success wide"
= link_to "Cancel", admin_applications_path, class: "btn btn-cancel"
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index 94d33fa6489..2cdf98075d1 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -5,7 +5,7 @@
System OAuth applications don't belong to any user and can only be managed by admins
%hr
%p= link_to 'New application', new_admin_application_path, class: 'btn btn-success'
-%table.table.table-striped
+%table.table
%thead
%tr
%th Name
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 593a6d816e3..e69143abe45 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -1,4 +1,5 @@
- page_title @application.name, "Applications"
+
%h3.page-title
Application: #{@application.name}
@@ -6,23 +7,29 @@
%table.table
%tr
%td
- Application Id
+ = _('Application ID')
%td
- %code#application_id= @application.uid
+ .clipboard-group
+ .input-group
+ %input.label.label-monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
+ .input-group-append
+ = clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default")
%tr
%td
- Secret:
+ = _('Secret')
%td
- %code#secret= @application.secret
-
+ .clipboard-group
+ .input-group
+ %input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
+ .input-group-append
+ = clipboard_button(target: '#application_id', title: _("Copy secret to clipboard"), class: "btn btn btn-default")
%tr
%td
- Callback url
+ = _('Callback URL')
%td
- @application.redirect_uri.split.each do |uri|
%div
%span.monospace= uri
-
%tr
%td
Trusted
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 7f34357f147..c465d9f51d6 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -36,6 +36,6 @@
= f.datetime_select :ends_at, {}, class: 'form-control form-control-inline'
.form-actions
- if @broadcast_message.persisted?
- = f.submit "Update broadcast message", class: "btn btn-create"
+ = f.submit "Update broadcast message", class: "btn btn-success"
- else
- = f.submit "Add broadcast message", class: "btn btn-create"
+ = f.submit "Add broadcast message", class: "btn btn-success"
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index fac61f9d249..85c04f8a01d 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -14,7 +14,7 @@
Projects:
= approximate_count_with_delimiters(@counts, Project)
%hr
- = link_to('New project', new_project_path, class: "btn btn-new")
+ = link_to('New project', new_project_path, class: "btn btn-success")
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
@@ -24,7 +24,7 @@
= approximate_count_with_delimiters(@counts, User)
= render_if_exists 'admin/dashboard/users_statistics'
%hr
- = link_to 'New user', new_admin_user_path, class: "btn btn-new"
+ = link_to 'New user', new_admin_user_path, class: "btn btn-success"
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
@@ -33,7 +33,7 @@
Groups:
= approximate_count_with_delimiters(@counts, Group)
%hr
- = link_to 'New group', new_admin_group_path, class: "btn btn-new"
+ = link_to 'New group', new_admin_group_path, class: "btn btn-success"
.row
.col-md-4
.info-well
diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml
index b50adef362f..7c04ef03947 100644
--- a/app/views/admin/deploy_keys/edit.html.haml
+++ b/app/views/admin/deploy_keys/edit.html.haml
@@ -6,5 +6,5 @@
= form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
- = f.submit 'Save changes', class: 'btn-save btn'
+ = f.submit 'Save changes', class: 'btn-success btn'
= link_to 'Cancel', admin_deploy_keys_path, class: 'btn btn-cancel'
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 52ab8bae119..01013be06d6 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -3,7 +3,7 @@
%h3.page-title.deploy-keys-title
Public deploy keys (#{@deploy_keys.count})
.float-right
- = link_to 'New deploy key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted'
+ = link_to 'New deploy key', new_admin_deploy_key_path, class: 'btn btn-success btn-sm btn-inverted'
- if @deploy_keys.any?
.table-holder.deploy-keys-list
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
index d4f8e340b69..9a563a5bc78 100644
--- a/app/views/admin/deploy_keys/new.html.haml
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -6,5 +6,5 @@
= form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
- = f.submit 'Create', class: 'btn-create btn'
+ = f.submit 'Create', class: 'btn-success btn'
= link_to 'Cancel', admin_deploy_keys_path, class: 'btn btn-cancel'
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index a3773e90cfb..2a117c1414e 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -26,12 +26,12 @@
.alert.alert-info
= render 'shared/group_tips'
.form-actions
- = f.submit _('Create group'), class: "btn btn-create"
+ = f.submit _('Create group'), class: "btn btn-success"
= link_to _('Cancel'), admin_groups_path, class: "btn btn-cancel"
- else
.form-actions
- = f.submit _('Save changes'), class: "btn btn-save"
+ = f.submit _('Save changes'), class: "btn btn-success"
= link_to _('Cancel'), admin_group_path(@group), class: "btn btn-cancel"
= render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 6a9b85b4109..cb833ffd9ac 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -12,7 +12,7 @@
= search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name'
= icon("search", class: "search-icon")
= render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
- = link_to new_admin_group_path, class: "btn btn-new" do
+ = link_to new_admin_group_path, class: "btn btn-success" do
= _('New group')
%ul.content-list
= render @groups
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 72b068ea6b5..0c683f86252 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -111,7 +111,7 @@
.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
- = button_tag _('Add users to group'), class: "btn btn-create"
+ = button_tag _('Add users to group'), class: "btn btn-success"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
.card
diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml
index b9a650e1f1f..486d0477f20 100644
--- a/app/views/admin/hooks/edit.html.haml
+++ b/app/views/admin/hooks/edit.html.haml
@@ -12,7 +12,7 @@
= form_for @hook, as: :hook, url: admin_hook_path do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
- = f.submit 'Save changes', class: 'btn btn-create'
+ = f.submit 'Save changes', class: 'btn btn-success'
= render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: @hook
= link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: 'Are you sure?' }
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index 87f9b0e86a7..5d462d7b732 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -10,7 +10,7 @@
.col-lg-8.append-bottom-default
= form_for @hook, as: :hook, url: admin_hooks_path do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
- = f.submit 'Add system hook', class: 'btn btn-create'
+ = f.submit 'Add system hook', class: 'btn btn-success'
%hr
diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml
index 946d868da01..3ab7990d9e2 100644
--- a/app/views/admin/identities/_form.html.haml
+++ b/app/views/admin/identities/_form.html.haml
@@ -12,5 +12,5 @@
= f.text_field :extern_uid, class: 'form-control', required: true
.form-actions
- = f.submit _('Save changes'), class: "btn btn-save"
+ = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml
index df3df159947..9543bbcf977 100644
--- a/app/views/admin/identities/index.html.haml
+++ b/app/views/admin/identities/index.html.haml
@@ -3,7 +3,7 @@
- page_title _("Identities"), @user.name, _("Users")
= render 'admin/users/head'
-= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right btn btn-new'
+= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right btn btn-success'
- if @identities.present?
.table-holder
%table.table
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index ee2d4c8430a..5e7b4817461 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -27,5 +27,5 @@
&nbsp;
.form-actions
- = f.submit _('Save'), class: 'btn btn-save js-save-button'
+ = f.submit _('Save'), class: 'btn btn-success js-save-button'
= link_to _("Cancel"), admin_labels_path, class: 'btn btn-cancel'
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index f1b8658f84e..5a5b3d18c5f 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -1,7 +1,7 @@
- page_title _("Labels")
%div
- = link_to new_admin_label_path, class: "float-right btn btn-nr btn-new" do
+ = link_to new_admin_label_path, class: "float-right btn btn-nr btn-success" do
= _('New label')
%h3.page-title
= _('Labels')
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 57de792f92d..46bb57c78a8 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -21,7 +21,7 @@
= dropdown_content
= dropdown_loading
= render 'shared/projects/dropdown'
- = link_to new_project_path, class: 'btn btn-new' do
+ = link_to new_project_path, class: 'btn btn-success' do
New Project
= button_tag "Search", class: "btn btn-primary btn-search hide"
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index ccba1c461fc..fefb4c7455d 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,6 +1,8 @@
- add_to_breadcrumbs "Projects", admin_projects_path
- breadcrumb_title @project.full_name
- page_title @project.full_name, "Projects"
+- @content_class = "admin-projects"
+
%h3.page-title
Project: #{@project.full_name}
= link_to edit_project_path(@project), class: "btn btn-nr float-right" do
@@ -110,6 +112,8 @@
= visibility_level_icon(@project.visibility_level)
= visibility_level_label(@project.visibility_level)
+ = render_if_exists 'admin/projects/geo_status_widget', locals: { project: @project }
+
.card
.card-header
Transfer project
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 43937b01339..e4fc2985087 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -1,51 +1,78 @@
-%tr{ id: dom_id(runner) }
- %td
- - if runner.instance_type?
- %span.badge.badge-success shared
- - elsif runner.group_type?
- %span.badge.badge-success group
- - else
- %span.badge.badge-info specific
- - if runner.locked?
- %span.badge.badge-warning locked
- - unless runner.active?
- %span.badge.badge-danger paused
-
- %td
- = link_to admin_runner_path(runner) do
- = runner.short_sha
- %td
- = runner.description
- %td
- = runner.version
- %td
- = runner.ip_address
- %td
- - if runner.instance_type? || runner.group_type?
- n/a
- - else
- = runner.projects.count(:all)
- %td
- #{runner.builds.count(:all)}
- %td
- - runner.tag_list.sort.each do |tag|
- %span.badge.badge-primary
- = tag
- %td
- - if runner.contacted_at
- = time_ago_with_tooltip runner.contacted_at
- - else
- Never
- %td.admin-runner-btn-group-cell
- .float-right.btn-group
- = link_to admin_runner_path(runner), class: 'btn btn-sm btn-default has-tooltip', title: 'Edit', ref: 'tooltip', aria: { label: 'Edit' }, data: { placement: 'top', container: 'body'} do
- = icon('pencil')
- &nbsp;
- - if runner.active?
- = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-sm btn-default has-tooltip', title: 'Pause', ref: 'tooltip', aria: { label: 'Pause' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do
- = icon('pause')
+.gl-responsive-table-row{ id: dom_id(runner) }
+ .table-section.section-10.section-wrap
+ .table-mobile-header{ role: 'rowheader' }= _('Type')
+ .table-mobile-content
+ - if runner.instance_type?
+ %span.badge.badge-success shared
+ - elsif runner.group_type?
+ %span.badge.badge-success group
- else
- = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default btn-sm has-tooltip', title: 'Resume', ref: 'tooltip', aria: { label: 'Resume' }, data: { placement: 'top', container: 'body'} do
- = icon('play')
- = link_to [:admin, runner], method: :delete, class: 'btn btn-danger btn-sm has-tooltip', title: 'Remove', ref: 'tooltip', aria: { label: 'Remove' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do
- = icon('remove')
+ %span.badge.badge-info specific
+ - if runner.locked?
+ %span.badge.badge-warning locked
+ - unless runner.active?
+ %span.badge.badge-danger paused
+
+ .table-section.section-10
+ .table-mobile-header{ role: 'rowheader' }= _('Runner token')
+ .table-mobile-content
+ = link_to runner.short_sha, admin_runner_path(runner)
+
+ .table-section.section-15
+ .table-mobile-header{ role: 'rowheader' }= _('Description')
+ .table-mobile-content.str-truncated.has-tooltip{ title: runner.description }
+ = runner.description
+
+ .table-section.section-15
+ .table-mobile-header{ role: 'rowheader' }= _('Version')
+ .table-mobile-content.str-truncated.has-tooltip{ title: runner.version }
+ = runner.version
+
+ .table-section.section-10
+ .table-mobile-header{ role: 'rowheader' }= _('IP Address')
+ .table-mobile-content
+ = runner.ip_address
+
+ .table-section.section-5
+ .table-mobile-header{ role: 'rowheader' }= _('Projects')
+ .table-mobile-content
+ - if runner.instance_type? || runner.group_type?
+ = _('n/a')
+ - else
+ = runner.projects.count(:all)
+
+ .table-section.section-5
+ .table-mobile-header{ role: 'rowheader' }= _('Jobs')
+ .table-mobile-content
+ = runner.builds.count(:all)
+
+ .table-section.section-10.section-wrap
+ .table-mobile-header{ role: 'rowheader' }= _('Tags')
+ .table-mobile-content
+ - runner.tag_list.sort.each do |tag|
+ %span.badge.badge-primary
+ = tag
+
+ .table-section.section-10
+ .table-mobile-header{ role: 'rowheader' }= _('Last contact')
+ .table-mobile-content
+ - if runner.contacted_at
+ = time_ago_with_tooltip runner.contacted_at
+ - else
+ = _('Never')
+
+ .table-section.table-button-footer.section-10
+ .btn-group.table-action-buttons
+ .btn-group
+ = link_to admin_runner_path(runner), class: 'btn btn-default has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do
+ = icon('pencil')
+ .btn-group
+ - if runner.active?
+ = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-default has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
+ = icon('pause')
+ - else
+ = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default has-tooltip', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
+ = icon('play')
+ .btn-group
+ = link_to [:admin, runner], method: :delete, class: 'btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
+ = icon('remove')
diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/admin/runners/_sort_dropdown.html.haml
new file mode 100644
index 00000000000..b201e6bf10e
--- /dev/null
+++ b/app/views/admin/runners/_sort_dropdown.html.haml
@@ -0,0 +1,11 @@
+- sorted_by = sort_options_hash[@sort]
+
+.dropdown.inline.prepend-left-10
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
+ = sorted_by
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
+ %li
+ = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by)
+ = sortable_item(sort_title_contacted_date, page_filter_path(sort: sort_value_contacted_date, label: true), sorted_by)
+
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 9280ff4d478..e9e4e0847d3 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,77 +1,122 @@
-- breadcrumb_title "Runners"
+- breadcrumb_title _('Runners')
- @no_container = true
%div{ class: container_class }
- .bs-callout
- %p
- A 'Runner' is a process which runs a job.
- You can setup as many Runners as you need.
- %br
- Runners can be placed on separate users, servers, even on your local machine.
- %br
+ .row
+ .col-sm-6
+ .bs-callout
+ %p
+ = (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.")
+ %br
+ = _('Runners can be placed on separate users, servers, even on your local machine.')
+ %br
- %div
- %span Each Runner can be in one of the following states:
- %ul
- %li
- %span.badge.badge-success shared
- \- Runner runs jobs from all unassigned projects
- %li
- %span.badge.badge-success group
- \- Runner runs jobs from all unassigned projects in its group
- %li
- %span.badge.badge-info specific
- \- Runner runs jobs from assigned projects
- %li
- %span.badge.badge-warning locked
- \- Runner cannot be assigned to other projects
- %li
- %span.badge.badge-danger paused
- \- Runner will not receive any new jobs
+ %div
+ %span= _('Each Runner can be in one of the following states:')
+ %ul
+ %li
+ %span.badge.badge-success shared
+ \-
+ = _('Runner runs jobs from all unassigned projects')
+ %li
+ %span.badge.badge-success group
+ \-
+ = _('Runner runs jobs from all unassigned projects in its group')
+ %li
+ %span.badge.badge-info specific
+ \-
+ = _('Runner runs jobs from assigned projects')
+ %li
+ %span.badge.badge-warning locked
+ \-
+ = _('Runner cannot be assigned to other projects')
+ %li
+ %span.badge.badge-danger paused
+ \-
+ = _('Runner will not receive any new jobs')
- .bs-callout.clearfix
- .float-left
- %p
- You can reset runners registration token by pressing a button below.
- .prepend-top-10
- = button_to _("Reset runners registration token"), reset_runners_token_admin_application_settings_path,
- method: :put, class: 'btn btn-default',
- data: { confirm: _("Are you sure you want to reset registration token?") }
+ .col-sm-6
+ .bs-callout
+ = render partial: 'ci/runner/how_to_setup_runner',
+ locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token,
+ type: 'shared',
+ reset_token_url: reset_registration_token_admin_application_settings_path }
- = render partial: 'ci/runner/how_to_setup_shared_runner',
- locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token }
+ .row
+ .col-sm-9
+ = form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
+ .filtered-search-wrapper
+ .filtered-search-box
+ = dropdown_tag(custom_icon('icon_history'),
+ options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
+ toggle_class: 'filtered-search-history-dropdown-toggle-button',
+ dropdown_class: 'filtered-search-history-dropdown',
+ content_class: 'filtered-search-history-dropdown-content',
+ title: _('Recent searches') }) do
+ .js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
+ .filtered-search-box-input-container.droplab-dropdown
+ .scroll-container
+ %ul.tokens-container.list-unstyled
+ %li.input-token
+ %input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } }
+ #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { action: 'submit' } }
+ = button_tag class: %w[btn btn-link] do
+ = sprite_icon('search')
+ %span
+ = _('Press Enter or click to search')
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ = button_tag class: %w[btn btn-link] do
+ -# Encapsulate static class name `{{icon}}` inside #{} to bypass
+ -# haml lint's ClassAttributeWithStaticValue
+ %svg
+ %use{ 'xlink:href': "#{'{{icon}}'}" }
+ %span.js-filter-hint
+ {{hint}}
+ %span.js-filter-tag.dropdown-light-content
+ {{tag}}
- .append-bottom-20.clearfix
- .float-left
- = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
- .form-group
- = search_field_tag :search, params[:search], class: 'form-control input-short', placeholder: 'Runner description or token', spellcheck: false
- = submit_tag 'Search', class: 'btn'
+ #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ - Ci::Runner::AVAILABLE_STATUSES.each do |status|
+ %li.filter-dropdown-item{ data: { value: status } }
+ = button_tag class: %w[btn btn-link] do
+ = status.titleize
- .float-right.light
- Runners currently online: #{@active_runners_cnt}
+ #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
+ %li.filter-dropdown-item{ data: { value: runner_type } }
+ = button_tag class: %w[btn btn-link] do
+ = runner_type.titleize
- %br
+ = button_tag class: %w[clear-search hidden] do
+ = icon('times')
+ .filter-dropdown-container
+ = render 'sort_dropdown'
+
+ .col-sm-3.text-right-lg
+ = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
- if @runners.any?
- .runners-content
+ .runners-content.content-list
.table-holder
- %table.table
- %thead
- %tr
- %th Type
- %th Runner token
- %th Description
- %th Version
- %th IP Address
- %th Projects
- %th Jobs
- %th Tags
- %th= link_to 'Last contact', admin_runners_path(safe_params.slice(:search).merge(sort: 'contacted_asc'))
- %th
+ .gl-responsive-table-row.table-row-header{ role: 'row' }
+ .table-section.section-10{ role: 'rowheader' }= _('Type')
+ .table-section.section-10{ role: 'rowheader' }= _('Runner token')
+ .table-section.section-15{ role: 'rowheader' }= _('Description')
+ .table-section.section-15{ role: 'rowheader' }= _('Version')
+ .table-section.section-10{ role: 'rowheader' }= _('IP Address')
+ .table-section.section-5{ role: 'rowheader' }= _('Projects')
+ .table-section.section-5{ role: 'rowheader' }= _('Jobs')
+ .table-section.section-10{ role: 'rowheader' }= _('Tags')
+ .table-section.section-10{ role: 'rowheader' }= _('Last contact')
+ .table-section.section-10{ role: 'rowheader' }
- - @runners.each do |runner|
- = render "admin/runners/runner", runner: runner
- = paginate @runners, theme: "gitlab"
+ - @runners.each do |runner|
+ = render 'admin/runners/runner', runner: runner
+ = paginate @runners, theme: 'gitlab'
- else
- .nothing-here-block No runners found
+ .nothing-here-block= _('No runners found')
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index 993006e8745..1798b44bbb7 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -7,4 +7,4 @@
= render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block
- = form.submit 'Save', class: 'btn btn-save'
+ = form.submit 'Save', class: 'btn btn-success'
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 7f21bdb91c8..296ef073144 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -75,8 +75,8 @@
.form-actions
- if @user.new_record?
- = f.submit 'Create user', class: "btn btn-create"
+ = f.submit 'Create user', class: "btn btn-success"
= link_to 'Cancel', admin_users_path, class: "btn btn-cancel"
- else
- = f.submit 'Save changes', class: "btn btn-save"
+ = f.submit 'Save changes', class: "btn btn-success"
= link_to 'Cancel', admin_user_path(@user), class: "btn btn-cancel"
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index faeb82656ba..f910e90d6ca 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -31,7 +31,7 @@
= sort_title_recently_updated
= link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
= sort_title_oldest_updated
- = link_to 'New user', new_admin_user_path, class: 'btn btn-new btn-search'
+ = link_to 'New user', new_admin_user_path, class: 'btn btn-success btn-search'
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 3d39c1da408..e6da81831ab 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -7,7 +7,7 @@
.card
.card-header Group projects
%ul.hover-list
- - @user.group_members.includes(:source).each do |group_member|
+ - @user.group_members.includes(:source).each do |group_member| # rubocop: disable CodeReuse/ActiveRecord
- group = group_member.group
%li.group_member
%strong= link_to group.name, admin_group_path(group)
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 30d7b21b1b8..a758a63dfb3 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,5 +1,4 @@
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
-- user_authored = awardable.user_authored?(current_user)
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml
index c26eb873718..4307060d748 100644
--- a/app/views/ci/runner/_how_to_setup_runner.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -1,6 +1,6 @@
- link = link_to _("Install GitLab Runner"), 'https://docs.gitlab.com/runner/install/', target: '_blank'
.append-bottom-10
- %h4= _("Setup a %{type} Runner manually") % { type: type }
+ %h4= _("Set up a %{type} Runner manually") % { type: type }
%ol
%li
@@ -13,5 +13,9 @@
= _("Use the following registration token during setup:")
%code#registration_token= registration_token
= clipboard_button(target: '#registration_token', title: _("Copy token to clipboard"), class: "btn-transparent btn-clipboard")
+ .prepend-top-10.append-bottom-10
+ = button_to _("Reset runners registration token"), reset_token_url,
+ method: :put, class: 'btn btn-default',
+ data: { confirm: _("Are you sure you want to reset registration token?") }
%li
= _("Start the Runner!")
diff --git a/app/views/ci/runner/_how_to_setup_shared_runner.html.haml b/app/views/ci/runner/_how_to_setup_shared_runner.html.haml
deleted file mode 100644
index 2a190cb9250..00000000000
--- a/app/views/ci/runner/_how_to_setup_shared_runner.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-.bs-callout.help-callout
- = render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: registration_token, type: 'shared' }
diff --git a/app/views/ci/runner/_how_to_setup_specific_runner.html.haml b/app/views/ci/runner/_how_to_setup_specific_runner.html.haml
deleted file mode 100644
index e765a353fe4..00000000000
--- a/app/views/ci/runner/_how_to_setup_specific_runner.html.haml
+++ /dev/null
@@ -1,26 +0,0 @@
-.bs-callout.help-callout
- .append-bottom-10
- %h4= _('Setup a specific Runner automatically')
-
- %p
- - link_to_help_page = link_to(_('Learn more about Kubernetes'),
- help_page_path('user/project/clusters/index'),
- target: '_blank',
- rel: 'noopener noreferrer')
-
- = _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
-
- %ol
- %li
- = _('Click the button below to begin the install process by navigating to the Kubernetes page')
- %li
- = _('Select an existing Kubernetes cluster or create a new one')
- %li
- = _('From the Kubernetes cluster details view, install Runner from the applications list')
-
- = link_to _('Install Runner on Kubernetes'),
- project_clusters_path(@project),
- class: 'btn btn-info'
- %hr
- = render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: registration_token, type: 'specific' }
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index d8f1e50544c..727784141bb 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -10,4 +10,4 @@
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
- if current_user.can_create_group?
- = link_to _("New group"), new_group_path, class: "btn btn-new"
+ = link_to _("New group"), new_group_path, class: "btn btn-success"
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 9b1d9b659f9..69a2e408073 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -19,4 +19,4 @@
= render 'shared/projects/search_form'
= render 'shared/projects/dropdown'
- if current_user.can_create_project?
- = link_to "New project", new_project_path, class: "btn btn-new"
+ = link_to "New project", new_project_path, class: "btn btn-success"
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index e7e323a8683..4f38339b87a 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -9,4 +9,4 @@
- if current_user
.nav-controls.d-none.d-sm-block
- = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet"
+ = link_to "New snippet", new_snippet_path, class: "btn btn-success", title: "New snippet"
diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml
index 31d4b3da4f1..3cee5841bbc 100644
--- a/app/views/dashboard/activity.html.haml
+++ b/app/views/dashboard/activity.html.haml
@@ -4,6 +4,9 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
- page_title "Activity"
- header_title "Activity", activity_dashboard_path
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 50f39f93283..985928305a2 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -3,6 +3,9 @@
- header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head'
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
- if params[:filter].blank? && @groups.empty?
= render 'shared/groups/empty_state'
- else
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index d7b6fb9a4a1..6034389b897 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -1,3 +1,4 @@
+# rubocop: disable CodeReuse/ActiveRecord
xml.title "#{current_user.name} issues"
xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
@@ -5,3 +6,4 @@ xml.id issues_dashboard_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
+# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 86a21e24ac9..91f58ddcfcc 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -4,6 +4,9 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
.top-area
= render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
.nav-controls
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index 61aae31be60..27f53a8d1c6 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -2,6 +2,9 @@
- page_title _("Merge Requests")
- @breadcrumb_link = merge_requests_dashboard_path(assignee_id: current_user.id)
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
.top-area
= render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set
.nav-controls
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index deed774a4a5..f0d16936a51 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -4,6 +4,9 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 8933d9e31ff..42638b8528d 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -4,6 +4,9 @@
- page_title "Starred Projects"
- header_title "Projects", dashboard_projects_path
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
%div{ class: container_class }
= render "projects/last_push"
= render 'dashboard/projects_head'
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index 4391624196b..b11dc2c8e9b 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -7,7 +7,7 @@
.d-block.d-sm-none
&nbsp;
- = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
+ = link_to new_snippet_path, class: "btn btn-success btn-block", title: "New snippet" do
New snippet
= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 8b3974d97f8..bbfa4cc7413 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -2,6 +2,9 @@
- page_title "Todos"
- header_title "Todos", dashboard_todos_path
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
- if current_user.todos.any?
.top-area
%ul.nav-links.mobile-separator.nav.nav-tabs
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
index 35dafb3e980..4b8ad5acd5b 100644
--- a/app/views/devise/passwords/edit.html.haml
+++ b/app/views/devise/passwords/edit.html.haml
@@ -7,12 +7,12 @@
= f.hidden_field :reset_password_token
.form-group
= f.label 'New password', for: "user_password"
- = f.password_field :password, class: "form-control top", required: true, title: 'This field is required'
+ = f.password_field :password, class: "form-control top qa-password-field", required: true, title: 'This field is required'
.form-group
= f.label 'Confirm new password', for: "user_password_confirmation"
- = f.password_field :password_confirmation, class: "form-control bottom", title: 'This field is required', required: true
+ = f.password_field :password_confirmation, class: "form-control bottom qa-password-confirmation", title: 'This field is required', required: true
.clearfix
- = f.submit "Change your password", class: "btn btn-primary"
+ = f.submit "Change your password", class: "btn btn-primary qa-change-password-button"
.clearfix.prepend-top-20
%p
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 0ee563ac066..7dacd0b1d72 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,10 +1,10 @@
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
.form-group
- = f.label "Username or email", for: "user_login"
- = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
+ = f.label "Username or email", for: "user_login", class: 'label-bold'
+ = f.text_field :login, class: "form-control top qa-login-field", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
.form-group
- = f.label :password
- = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
+ = f.label :password, class: 'label-bold'
+ = f.password_field :password, class: "form-control bottom qa-password-field", required: true, title: "This field is required."
- if devise_mapping.rememberable?
.remember-me
%label{ for: "user_remember_me" }
@@ -17,4 +17,4 @@
= recaptcha_tags
.submit-container.move-submit-down
- = f.submit "Sign in", class: "btn btn-save"
+ = f.submit "Sign in", class: "btn btn-success qa-sign-in-button"
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index 36ff42090be..131544ac0c0 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -10,4 +10,4 @@
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
- = submit_tag "Sign in", class: "btn-save btn"
+ = submit_tag "Sign in", class: "btn-success btn"
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 6bf7349f602..796c0cadda8 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,13 +1,13 @@
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
.form-group
= label_tag :username, "#{server['label']} Username"
- = text_field_tag :username, nil, { class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
+ = text_field_tag :username, nil, { class: "form-control top qa-username-field", title: "This field is required.", autofocus: "autofocus", required: true }
.form-group
= label_tag :password
- = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
+ = password_field_tag :password, nil, { class: "form-control bottom qa-password-field", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
.remember-me
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
- = submit_tag "Sign in", class: "btn-save btn"
+ = submit_tag "Sign in", class: "btn-success btn qa-sign-in-button"
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index ba168c4eab8..fefdf5f9531 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -11,7 +11,7 @@
= f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
%p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20
- = f.submit "Verify code", class: "btn btn-save"
+ = f.submit "Verify code", class: "btn btn-success"
- if @user.two_factor_u2f_enabled?
= render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name }
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 3723814debe..269a3721e06 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,14 +1,17 @@
-.omniauth-container
- %p
- %span.light
- Sign in with &nbsp;
- - providers = enabled_button_based_providers
+.omniauth-container.prepend-top-15
+ %label.label-bold.d-block
+ Sign in with
+ - providers = enabled_button_based_providers
+ .d-flex.justify-content-between.flex-wrap
- providers.each do |provider|
- %span.light
- - has_icon = provider_has_icon?(provider)
- = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}"
- %fieldset.prepend-top-10.remember-me
- %label
- = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
+ - has_icon = provider_has_icon?(provider)
+ = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login', id: "oauth-login-#{provider}" do
+ - if has_icon
+ = provider_image_tag(provider)
%span
- Remember me
+ = label_for_provider(provider)
+ %fieldset.remember-me
+ %label
+ = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
+ %span
+ Remember me
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index ee7369f54a9..90ed20404c5 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -4,24 +4,24 @@
.devise-errors
= devise_error_messages!
.form-group
- = f.label :name, 'Full name'
+ = f.label :name, 'Full name', class: 'label-bold'
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group
- = f.label :username
+ = f.label :username, class: 'label-bold'
= f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
.form-group
- = f.label :email
+ = f.label :email, class: 'label-bold'
= f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address."
.form-group
- = f.label :email_confirmation
+ = f.label :email_confirmation, class: 'label-bold'
= f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address."
.form-group.append-bottom-20#password-strength
- = f.label :password
+ = f.label :password, class: 'label-bold'
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
- %p.gl-field-hint Minimum length is #{@minimum_password_length} characters
+ %p.gl-field-hint.text-secondary Minimum length is #{@minimum_password_length} characters
- if Gitlab::CurrentSettings.current_application_settings.enforce_terms?
.form-group
= check_box_tag :terms_opt_in, '1', false, required: true
@@ -34,8 +34,3 @@
= recaptcha_tags
.submit-container
= f.submit "Register", class: "btn-register btn"
-.clearfix.submit-container
- %p
- %span.light Didn't receive a confirmation email?
- = succeed '.' do
- = link_to "Request a new one", new_confirmation_path(:user)
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 58c585a29ff..7dced0942f5 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -4,10 +4,10 @@
= link_to "Crowd", "#crowd", class: 'nav-link active', 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
%li.nav-item
- = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && !crowd_enabled?)}", 'data-toggle' => 'tab'
+ = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && !crowd_enabled?)} qa-ldap-tab", 'data-toggle' => 'tab'
- if password_authentication_enabled_for_web?
%li.nav-item
- = link_to 'Standard', '#login-pane', class: 'nav-link', 'data-toggle' => 'tab'
+ = link_to 'Standard', '#login-pane', class: 'nav-link qa-standard-tab', 'data-toggle' => 'tab'
- if allow_signup?
%li.nav-item
- = link_to 'Register', '#register-pane', class: 'nav-link', 'data-toggle' => 'tab'
+ = link_to 'Register', '#register-pane', class: 'nav-link qa-register-tab', 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index 284d4fa1b89..8745a4e9d3e 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -1,6 +1,6 @@
%ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
- %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
+ %a.nav-link.qa-sign-in-tab.active{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if allow_signup?
%li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
+ %a.nav-link.qa-register-tab{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index 4b6c4581eb3..6b8dd156874 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -4,7 +4,6 @@
-# Text diff discussions
- expanded = local_assigns.fetch(:expanded, true)
%tr.notes_holder{ class: ('hide' unless expanded) }
- %td.notes_line{ colspan: 2 }
- %td.notes_content
+ %td.notes_content{ colspan: 3 }
.content{ class: ('hide' unless expanded) }
= render partial: "discussions/notes", collection: discussions, as: :discussion, locals: { disable_collapse_class: true }
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 646e89e9bd1..44c898e0fac 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -1,6 +1,4 @@
- diff_file = discussion.diff_file
-- blob = discussion.blob
-- discussions = { discussion.original_line_code => [discussion] }
- diff_file_class = diff_file.text? ? 'text-file' : 'js-image-file'
- diff_data = {}
- expanded = discussion.expanded? || local_assigns.fetch(:expanded, nil)
diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml
index 079d9083dff..2e621c4082d 100644
--- a/app/views/discussions/_parallel_diff_discussion.html.haml
+++ b/app/views/discussions/_parallel_diff_discussion.html.haml
@@ -1,21 +1,17 @@
- expanded = [*discussions_left, *discussions_right].any?(&:expanded?)
%tr.notes_holder{ class: ('hide' unless expanded) }
- if discussions_left
- %td.notes_line.old
- %td.notes_content.parallel.old
+ %td.notes_content.parallel.old{ colspan: 2 }
.content{ class: ('hide' unless discussions_left.any?(&:expanded?)) }
= render partial: "discussions/notes", collection: discussions_left, as: :discussion, line_type: 'old', locals: { disable_collapse_class: true }
- else
- %td.notes_line.old= ("")
- %td.notes_content.parallel.old
+ %td.notes_content.parallel.old{ colspan: 2 }
.content
- if discussions_right
- %td.notes_line.new
- %td.notes_content.parallel.new
+ %td.notes_content.parallel.new{ colspan: 2 }
.content{ class: ('hide' unless discussions_right.any?(&:expanded?)) }
= render partial: "discussions/notes", collection: discussions_right, as: :discussion, line_type: 'new', locals: { disable_collapse_class: true }
- else
- %td.notes_line.new= ("")
- %td.notes_content.parallel.new
+ %td.notes_content.parallel.new{ colspan: 2 }
.content
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index 0bc057a8864..78904f550c7 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -20,4 +20,4 @@
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
.prepend-top-default
- = f.submit _('Save application'), class: "btn btn-create"
+ = f.submit _('Save application'), class: "btn btn-success"
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index ab3a1b100ce..1f5c70a6c6e 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -16,6 +16,9 @@
= _('Add new application')
= render 'form', application: @application
%hr
+ - else
+ .bs-callout.bs-callout-disabled
+ = _('Adding new applications is disabled in your GitLab instance. Please contact your GitLab administrator to get the permission')
- if user_oauth_applications?
.oauth-applications
%h5
@@ -62,7 +65,7 @@
%th
%tbody
- @authorized_apps.each do |app|
- - token = app.authorized_tokens.order('created_at desc').first
+ - token = app.authorized_tokens.order('created_at desc').first # rubocop: disable CodeReuse/ActiveRecord
%tr{ id: "application_#{app.id}" }
%td= app.name
%td= token.created_at
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index bb76ac6d5f6..776bbc36ec2 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -10,18 +10,25 @@
%table.table
%tr
%td
- = _('Application Id')
+ = _('Application ID')
%td
- %code#application_id= @application.uid
+ .clipboard-group
+ .input-group
+ %input.label.label-monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
+ .input-group-append
+ = clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default")
%tr
%td
- = _('Secret:')
+ = _('Secret')
%td
- %code#secret= @application.secret
-
+ .clipboard-group
+ .input-group
+ %input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
+ .input-group-append
+ = clipboard_button(target: '#application_id', title: _("Copy secret to clipboard"), class: "btn btn btn-default")
%tr
%td
- = _('Callback url')
+ = _('Callback URL')
%td
- @application.redirect_uri.split.each do |uri|
%div
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index 08f2442f025..69cc510e9c1 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -1,4 +1,3 @@
-- submit_btn_css ||= 'btn btn-link btn-remove'
- if defined?(token)
- path = oauth_authorized_application_path(0, token_id: token)
- else
diff --git a/app/views/errors/precondition_failed.html.haml b/app/views/errors/precondition_failed.html.haml
new file mode 100644
index 00000000000..aa3869f33a9
--- /dev/null
+++ b/app/views/errors/precondition_failed.html.haml
@@ -0,0 +1,8 @@
+- content_for(:title, 'Encoding Error')
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
+ %h1
+ 412
+.container
+ %h3 Precondition failed
+ %hr
+ %p Page can't be loaded because of invalid parameters.
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 53a33adc14d..78a1d1a0553 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -11,3 +11,5 @@
= render "events/event/note", event: event
- else
= render "events/event/common", event: event
+- elsif @user&.include_private_contributions?
+ = render "events/event/private", event: event
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index bc1d32607e4..c5b033b1185 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -1,7 +1,7 @@
%div{ xmlns: "http://www.w3.org/1999/xhtml" }
%p
%strong= event.author_name
- = link_to "(#{truncate_sha(event.commit_id)})", project_commit_path(event.project, event.commit_id)
+ = link_to "(#{truncate_sha(event.commit_id)})", event_feed_url(event)
%i
at
= event.created_at.to_s(:short)
diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml
index 8f7da7d8c4f..98941722434 100644
--- a/app/views/events/_event_scope.html.haml
+++ b/app/views/events/_event_scope.html.haml
@@ -1,7 +1,7 @@
%span.event-scope
= event_preposition(event)
- if event.project
- = link_to_project event.project
+ = link_to_project(event.project)
- else
= event.project_name
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 01e72862114..829a3da1558 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -1,7 +1,7 @@
= icon_for_profile_event(event)
.event-title
- %span.author_name= link_to_author event
+ %span.author_name= link_to_author(event)
%span{ class: event.action_name }
- if event.target
= event.action_name
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index d8e59be57bb..6ad7e157131 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -1,11 +1,11 @@
= icon_for_profile_event(event)
.event-title
- %span.author_name= link_to_author event
+ %span.author_name= link_to_author(event)
%span{ class: event.action_name }
= event_action_name(event)
- if event.project
- = link_to_project event.project
+ = link_to_project(event.project)
- else
= event.project_name
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index de6383e4097..cdacd998a69 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -1,7 +1,7 @@
= icon_for_profile_event(event)
.event-title
- %span.author_name= link_to_author event
+ %span.author_name= link_to_author(event)
= event.action_name
= event_note_title_html(event)
diff --git a/app/views/events/event/_private.html.haml b/app/views/events/event/_private.html.haml
new file mode 100644
index 00000000000..ccd2aacb4ea
--- /dev/null
+++ b/app/views/events/event/_private.html.haml
@@ -0,0 +1,10 @@
+.event-inline.event-item
+ .event-item-timestamp
+ = time_ago_with_tooltip(event.created_at)
+
+ .system-note-image= sprite_icon('eye-slash', size: 16, css_class: 'icon')
+
+ .event-title
+ - author_name = capture do
+ %span.author_name= link_to_author(event)
+ = s_('Profiles|%{author_name} made a private contribution').html_safe % { author_name: author_name }
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 85f2d00bde3..5f0ee79cd9b 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -3,7 +3,7 @@
= icon_for_profile_event(event)
.event-title
- %span.author_name= link_to_author event
+ %span.author_name= link_to_author(event)
%span.pushed #{event.action_name} #{event.ref_type}
%strong
- commits_link = project_commits_path(project, event.ref_name)
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 387c37b7a91..1d8b9c5bc8f 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -2,6 +2,9 @@
- page_title _("Groups")
- header_title _("Groups"), dashboard_groups_path
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/groups_head'
- else
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index 452f390695c..16be5791f83 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -2,6 +2,9 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index 452f390695c..16be5791f83 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -2,6 +2,9 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index 452f390695c..16be5791f83 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -2,6 +2,9 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
+= content_for :above_breadcrumbs_content do
+ = render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/groups/_archived_projects.html.haml b/app/views/groups/_archived_projects.html.haml
new file mode 100644
index 00000000000..ed79f5790f0
--- /dev/null
+++ b/app/views/groups/_archived_projects.html.haml
@@ -0,0 +1,8 @@
+#js-groups-archived-tree
+ .empty-state.text-center.hidden
+ %p= _("There are no archived projects yet")
+
+ %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
+ .js-groups-list-holder
+ .loading-container.text-center
+ = icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
diff --git a/app/views/groups/_children.html.haml b/app/views/groups/_children.html.haml
deleted file mode 100644
index 742b40784d3..00000000000
--- a/app/views/groups/_children.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-.js-groups-list-holder
- #js-groups-tree{ data: { hide_projects: 'false', group_id: group.id, endpoint: group_children_path(group, format: :json), path: group_path(group), form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
- .loading-container.text-center
- = icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index f7cc62c6929..ff59013ed67 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -1,5 +1,5 @@
.form-group.row
- = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label col-sm-2'
+ = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label col-sm-2 pt-0'
.col-sm-10
.form-check
= f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'form-check-input'
@@ -11,13 +11,13 @@
%span.descr This setting can be overridden in each project.
.form-group.row
- = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2'
+ = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2 pt-0'
.col-sm-10
.form-check
= f.check_box :require_two_factor_authentication, class: 'form-check-input'
= f.label :require_two_factor_authentication, class: 'form-check-label' do
%strong
- Require all users in this group to setup Two-factor authentication
+ Require all users in this group to set up Two-factor authentication
= link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
.form-group.row
.offset-sm-2.col-sm-10
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
new file mode 100644
index 00000000000..4eb8367f633
--- /dev/null
+++ b/app/views/groups/_shared_projects.html.haml
@@ -0,0 +1,8 @@
+#js-groups-shared-tree
+ .empty-state.text-center.hidden
+ %p= _("There are no projects shared with this group yet")
+
+ %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
+ .js-groups-list-holder
+ .loading-container.text-center
+ = icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
diff --git a/app/views/groups/_subgroups_and_projects.html.haml b/app/views/groups/_subgroups_and_projects.html.haml
new file mode 100644
index 00000000000..d53c8026df8
--- /dev/null
+++ b/app/views/groups/_subgroups_and_projects.html.haml
@@ -0,0 +1,8 @@
+#js-groups-subgroups_and_projects-tree
+ .empty-state.hidden
+ = render "shared/groups/empty_state"
+
+ %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
+ .js-groups-list-holder
+ .loading-container.text-center
+ = icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index cae2df4699e..fc17dd2d310 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -25,6 +25,18 @@
.settings-content
= render 'groups/settings/permissions'
+%section.settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = s_('GroupSettings|Badges')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = s_('GroupSettings|Customize your group badges.')
+ = link_to s_('GroupSettings|Learn more about badges.'), help_page_path('user/project/badges')
+ .settings-content
+ = render 'shared/badges/badge_settings'
+
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index aa03f8365f9..04683ec5a9a 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -19,4 +19,4 @@
On this date, the member(s) will automatically lose access to this group and all of its projects.
.col-md-2
- = f.submit 'Add to group', class: "btn btn-create btn-block"
+ = f.submit 'Add to group', class: "btn btn-success btn-block"
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 2a385b661e5..2fd96c9d158 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -1,3 +1,4 @@
+# rubocop: disable CodeReuse/ActiveRecord
xml.title "#{@group.name} issues"
xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: issues_group_url, rel: "alternate", type: "text/html"
@@ -5,3 +6,4 @@ xml.id issues_group_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
+# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index db7eaff6658..5b78ce910b8 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,29 +1,36 @@
- @no_container = true
- page_title "Labels"
- can_admin_label = can?(current_user, :admin_label, @group)
-- hide_class = ''
-- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
- issuables = ['issues', 'merge requests']
+- search = params[:search]
+- subscribed = params[:subscribed]
+- labels_or_filters = @labels.exists? || search.present? || subscribed.present?
- if can_admin_label
- content_for(:header_content) do
.nav-controls
- = link_to _('New label'), new_group_label_path(@group), class: "btn btn-new"
+ = link_to _('New label'), new_group_label_path(@group), class: "btn btn-success"
-- if @labels.exists?
+- if labels_or_filters
#promote-label-modal
%div{ class: container_class }
- .top-area.adjust
- .nav-text
- = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
+ = render 'shared/labels/nav'
.labels-container.prepend-top-5
- .other-labels
- - if can_admin_label
- %h5{ class: ('hide' if hide) } Labels
- %ul.content-list.manage-labels-list.js-other-labels
- = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false }
- = paginate @labels, theme: 'gitlab'
+ - if @labels.any?
+ .text-muted
+ = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
+ .other-labels
+ %h5= _('Labels')
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false }
+ = paginate @labels, theme: 'gitlab'
+ - elsif search.present?
+ .nothing-here-block
+ = _('No labels with such name or description')
+ - elsif subscribed.present?
+ .nothing-here-block
+ = _('You do not have any subscriptions yet')
- else
= render 'shared/empty_states/labels'
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index 6d35457a0ec..39e3af5f6d2 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -19,9 +19,9 @@
.form-actions
- if @milestone.new_record?
- = f.submit 'Create milestone', class: "btn-create btn"
+ = f.submit 'Create milestone', class: "btn-success btn"
= link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
- else
- = f.submit 'Update milestone', class: "btn-create btn"
+ = f.submit 'Update milestone', class: "btn-success btn"
= link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn btn-cancel"
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index b6424df55cd..af4fe8f2ef8 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -6,7 +6,7 @@
.nav-controls
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @group)
- = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
+ = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-success"
.milestones
%ul.content-list
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 53f54db1ddf..683129fdf6e 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -36,5 +36,5 @@
= render 'shared/group_tips'
.form-actions
- = f.submit 'Create group', class: "btn btn-create"
+ = f.submit 'Create group', class: "btn btn-success"
= link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml
index e6c089c3494..bcfb6d99716 100644
--- a/app/views/groups/runners/_group_runners.html.haml
+++ b/app/views/groups/runners/_group_runners.html.haml
@@ -11,7 +11,9 @@
-# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
- if can?(current_user, :admin_pipeline, @group)
= render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: @group.runners_token, type: 'group' }
+ locals: { registration_token: @group.runners_token,
+ type: 'group',
+ reset_token_url: reset_registration_token_group_settings_ci_cd_path }
- if @group.runners.empty?
%h4.underlined-title
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
index b7c673db705..3814d45929d 100644
--- a/app/views/groups/settings/_advanced.html.haml
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -12,8 +12,8 @@
.group-root-path.input-group-prepend.has-tooltip{ title: group_path(@group), :'data-placement' => 'bottom' }
.input-group-text
%span>= root_url
- - if parent
- %strong= parent.full_path + '/'
+ - if @group.parent
+ %strong= @group.parent.full_path + '/'
= f.hidden_field :parent_id
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index ffce2d4b14f..8dc88ec446c 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -10,7 +10,7 @@
= render 'shared/allow_request_access', form: f
.form-group.row
- %label.col-form-label.col-sm-2
+ %label.col-form-label.col-sm-2.pt-0
= s_('GroupSettings|Share with group lock')
.col-sm-10
.form-check
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 5a88619f769..6a293daaf95 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -7,21 +7,20 @@
= render 'groups/home_panel'
-.groups-header{ class: container_class }
- .group-nav-container
- .nav-controls.clearfix
+.groups-listing{ class: container_class, data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
+ .top-area.group-nav-container
+ .group-search
= render "shared/groups/search_form"
- = render "shared/groups/dropdown", show_archive_options: true
- if can? current_user, :create_projects, @group
- new_project_label = _("New project")
- new_subgroup_label = _("New subgroup")
- if can_create_subgroups
- .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
- %input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } }
- %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
+ .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
+ %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } }
+ %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
= icon("caret-down", class: "dropdown-btn-icon")
%ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
- %li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } }
+ %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } }
.menu-item
.icon-container
= icon("check", class: "list-item-checkmark")
@@ -29,7 +28,7 @@
%strong= new_project_label
%span= s_("GroupsTree|Create a project in this group.")
%li.divider.droplap-item-ignore
- %li{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } }
+ %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } }
.menu-item
.icon-container
= icon("check", class: "list-item-checkmark")
@@ -39,7 +38,29 @@
- else
= link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success"
- - if params[:filter].blank? && !@has_children
- = render "shared/groups/empty_state"
- - else
- = render "children", children: @children, group: @group
+ .scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
+ %li.js-subgroups_and_projects-tab
+ = link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do
+ = _("Subgroups and projects")
+ %li.js-shared-tab
+ = link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do
+ = _("Shared projects")
+ %li.js-archived-tab
+ = link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do
+ = _("Archived projects")
+
+ .nav-controls
+ = render "shared/groups/dropdown"
+
+ .tab-content
+ #subgroups_and_projects.tab-pane
+ = render "subgroups_and_projects", group: @group
+
+ #shared.tab-pane
+ = render "shared_projects", group: @group
+
+ #archived.tab-pane
+ = render "archived_projects", group: @group
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 7a66bac09cb..dfa5d820ce9 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -7,8 +7,7 @@
GitLab
Community Edition
- if user_signed_in?
- %span= Gitlab::VERSION
- %small= link_to Gitlab.revision, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab.revision)
+ %span= link_to_version
= version_status_badge
%hr
diff --git a/app/views/help/instance_configuration/_gitlab_pages.html.haml b/app/views/help/instance_configuration/_gitlab_pages.html.haml
index bdd77730dcc..94c25edaf82 100644
--- a/app/views/help/instance_configuration/_gitlab_pages.html.haml
+++ b/app/views/help/instance_configuration/_gitlab_pages.html.haml
@@ -8,7 +8,7 @@
%p
Below are the settings for
- = succeed('.') { link_to('Gitlab Pages', gitlab_pages[:url], target: '_blank') }
+ = succeed('.') { link_to('GitLab Pages', gitlab_pages[:url], target: '_blank') }
.table-responsive
%table
%thead
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index b32b602ceb3..506f580b246 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -189,7 +189,7 @@
%li
= link_to 'Sort by date', '#'
- = link_to 'New issue', '#', class: 'btn btn-new btn-inverted'
+ = link_to 'New issue', '#', class: 'btn btn-success btn-inverted'
.lead
Only nav links without button and search
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index b54b1af1e0c..626080c284b 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -21,4 +21,4 @@
.col-md-4
= password_field_tag :password, nil, class: 'form-control'
.form-actions
- = submit_tag _('Continue to the next step'), class: 'btn btn-create'
+ = submit_tag _('Continue to the next step'), class: 'btn btn-success'
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index ff2f989c509..8ed9dc68bb3 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -39,4 +39,4 @@
scope: :all, email_user: true, selected: user[:gitlab_user])
.form-actions
- = submit_tag _('Continue to the next step'), class: 'btn btn-create'
+ = submit_tag _('Continue to the next step'), class: 'btn btn-success'
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index 2b3102f9af9..a88b04eccbb 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -19,4 +19,4 @@
.col-sm-4
= text_field_tag :personal_access_token, nil, class: 'form-control'
.form-actions
- = submit_tag _('List Your Gitea Repositories'), class: 'btn btn-create'
+ = submit_tag _('List Your Gitea Repositories'), class: 'btn btn-success'
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 4225ee19217..5e4595d930b 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -8,8 +8,11 @@
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
.row
+ .form-group.project-name.col-sm-12
+ = label_tag :name, _('Project name'), class: 'label-bold'
+ = text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control input-lg", autofocus: true
.form-group.col-12.col-sm-6
- = label_tag :namespace_id, 'Project path', class: 'label-bold'
+ = label_tag :namespace_id, _('Project URL'), class: 'label-bold'
.form-group
.input-group
- if current_user.can_select_namespace?
@@ -24,8 +27,8 @@
#{user_url(current_user.username)}/
= hidden_field_tag :namespace_id, value: current_user.namespace_id
.form-group.col-12.col-sm-6.project-path
- = label_tag :path, _('Project name'), class: 'label-bold'
- = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, autofocus: true, required: true
+ = label_tag :path, _('Project slug'), class: 'label-bold'
+ = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, required: true
.row
.form-group.col-md-12
@@ -38,5 +41,5 @@
= file_field_tag :file, class: ''
.row
.form-actions.col-sm-12
- = submit_tag _('Import project'), class: 'btn btn-create'
+ = submit_tag _('Import project'), class: 'btn btn-success'
= link_to _('Cancel'), new_project_path, class: 'btn btn-cancel'
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index fd6e4726fc5..7a6ad28f0aa 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -59,4 +59,4 @@
= _('Yes, let me map Google Code users to full names or GitLab users.')
%li
%p
- = submit_tag _('Continue to the next step'), class: "btn btn-create"
+ = submit_tag _('Continue to the next step'), class: "btn btn-success"
diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml
index baaaf6bdc63..f523b993aa7 100644
--- a/app/views/import/google_code/new_user_map.html.haml
+++ b/app/views/import/google_code/new_user_map.html.haml
@@ -33,4 +33,4 @@
= text_area_tag :user_map, JSON.pretty_generate(@user_map), class: 'form-control', rows: 15
.form-actions
- = submit_tag _('Continue to the next step'), class: "btn btn-create"
+ = submit_tag _('Continue to the next step'), class: "btn btn-success"
diff --git a/app/views/instance_statistics/cohorts/index.html.haml b/app/views/instance_statistics/cohorts/index.html.haml
index 5e9a8c083af..e135bab10d8 100644
--- a/app/views/instance_statistics/cohorts/index.html.haml
+++ b/app/views/instance_statistics/cohorts/index.html.haml
@@ -1,16 +1,16 @@
-- breadcrumb_title "Cohorts"
+- breadcrumb_title _("Cohorts")
- @no_container = true
%div{ class: container_class }
- if @cohorts
= render 'cohorts_table'
- = render 'usage_ping'
- else
.bs-callout.bs-callout-warning.clearfix
%p
- User cohorts are only shown when the
- = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping'), target: '_blank'
- is enabled. To enable it and see user cohorts,
- visit
- = succeed '.' do
- = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
+ - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
+ - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
+ = s_('User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
+ - if current_user.admin?
+ - application_settings_path = admin_application_settings_path(anchor: 'usage-statistics')
+ - application_settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: application_settings_path }
+ = s_('To enable it and see User Cohorts, visit %{application_settings_link_start}application settings%{application_settings_link_end}.').html_safe % { application_settings_link_start: application_settings_link_start, application_settings_link_end: '</a>'.html_safe }
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 0a741b50960..0a5717f75e1 100644
--- a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml
@@ -1,9 +1,14 @@
.container.convdev-empty
.col-sm-12.justify-content-center.text-center
= custom_icon('convdev_no_index')
- %h4 Usage ping is not enabled
- %p
- ConvDev is only shown when the
- = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics'), target: '_blank'
- is enabled. Enable usage ping to get an overview of how you are using GitLab from a feature perspective
- = link_to 'Enable usage ping', admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary'
+ %h4= _('Usage ping is not enabled')
+ - if !current_user.admin?
+ %p
+ - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
+ - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
+ = s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
+ - if current_user.admin?
+ %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'
diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/conversational_development_index/index.html.haml
index dd63b98376f..1e7db4982d6 100644
--- a/app/views/instance_statistics/conversational_development_index/index.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/index.html.haml
@@ -1,12 +1,13 @@
- @no_container = true
-- page_title 'ConvDev Index'
+- page_title _('ConvDev Index')
+- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
.container
- - if show_callout?('convdev_intro_callout_dismissed')
+ - if usage_ping_enabled && show_callout?('convdev_intro_callout_dismissed')
= render 'callout'
.prepend-top-default
- - if !Gitlab::CurrentSettings.usage_ping_enabled
+ - if !usage_ping_enabled
= render 'disabled'
- elsif @metric.blank?
= render 'no_data'
diff --git a/app/views/issues/_issues_calendar.ics.ruby b/app/views/issues/_issues_calendar.ics.ruby
index 3563635d33d..73ab8489e0c 100644
--- a/app/views/issues/_issues_calendar.ics.ruby
+++ b/app/views/issues/_issues_calendar.ics.ruby
@@ -2,6 +2,7 @@ cal = Icalendar::Calendar.new
cal.prodid = '-//GitLab//NONSGML GitLab//EN'
cal.x_wr_calname = 'GitLab Issues'
+# rubocop: disable CodeReuse/ActiveRecord
@issues.includes(project: :namespace).each do |issue|
cal.event do |event|
event.dtstart = Icalendar::Values::Date.new(issue.due_date)
@@ -11,5 +12,6 @@ cal.x_wr_calname = 'GitLab Issues'
event.transp = 'TRANSPARENT'
end
end
+# rubocop: enable CodeReuse/ActiveRecord
cal.to_ical
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index f67a8878c80..a41d30da450 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -8,6 +8,7 @@
= render "layouts/broadcast"
= render 'layouts/header/read_only_banner'
= yield :flash_message
+ = render "shared/ping_consent"
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"
= render "layouts/flash"
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 9a7a67cfa83..a86972d8cf3 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -1,7 +1,3 @@
-- if controller.controller_path =~ /^groups/ && @group.persisted?
- - label = _('This group')
-- if controller.controller_path =~ /^projects/ && @project.persisted?
- - label = _('This project')
- if @group && @group.persisted? && @group.path
- group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
- if @project && @project.persisted?
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 0ca34b276a7..1f4d24d996c 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -4,7 +4,7 @@
%body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
- = render "layouts/header/default"
+ = render partial: "layouts/header/default", locals: { project: @project, group: @group }
= render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body
diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml
index 2ab9e55441b..80bda34a3f5 100644
--- a/app/views/layouts/explore.html.haml
+++ b/app/views/layouts/explore.html.haml
@@ -1,5 +1,5 @@
-- page_title = _("Explore")
+- page_title _("Explore")
- unless current_user
- - header_title = _("Explore GitLab"), explore_root_path
+ - header_title _("Explore GitLab"), explore_root_path
= render template: "layouts/application"
diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml
index 95db8313821..e29f646ed4f 100644
--- a/app/views/layouts/fullscreen.html.haml
+++ b/app/views/layouts/fullscreen.html.haml
@@ -3,7 +3,7 @@
= render "layouts/head"
%body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
= render 'peek/bar'
- = render "layouts/header/default"
+ = render partial: "layouts/header/default", locals: { project: @project, group: @group }
= render 'shared/outdated_browser'
.mobile-overlay
.alert-wrapper
diff --git a/app/views/layouts/group_settings.html.haml b/app/views/layouts/group_settings.html.haml
index 14c5f0ce04c..9db78ec58e4 100644
--- a/app/views/layouts/group_settings.html.haml
+++ b/app/views/layouts/group_settings.html.haml
@@ -1,4 +1,4 @@
-- page_title = _("Settings")
-- nav "group"
+- page_title _("Settings")
+- nav "group"
= render template: "layouts/group"
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 9ed05d6e3d0..261d758622b 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -5,7 +5,14 @@
.user-name.bold
= current_user.name
= current_user.to_reference
+ - if current_user.status
+ .user-status-emoji.str-truncated.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
+ = emoji_icon current_user.status.emoji
+ = current_user.status.message_html.html_safe
%li.divider
+ - if can?(current_user, :update_user_status, current_user)
+ %li
+ .js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } }
- if current_user_menu?(:profile)
%li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index e8d31992149..39604611440 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,3 +1,10 @@
+- if project
+ - search_path_url = search_path(project_id: project.id)
+- elsif group
+ - search_path_url = search_path(group_id: group.id)
+- else
+ - search_path_url = search_path
+
%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
@@ -24,26 +31,25 @@
%li.nav-item.d-none.d-sm-none.d-md-block.m-auto
= render 'layouts/search' unless current_controller?(:search)
%li.nav-item.d-inline-block.d-sm-none.d-md-none
- = link_to search_path, title: _('Search'), aria: { label: _("Search") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to search_path_url, title: _('Search'), aria: { label: _('Search') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('search', size: 16)
-
- if header_link?(:issues)
= nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
- = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _("Issues") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues)
%span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
- = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _("Merge requests") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _('Merge requests') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('git-merge', size: 16)
- merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.badge-pill.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
- if header_link?(:todos)
= nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
- = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _("Todos") }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _('Todos') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('todo-done', size: 16)
%span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
@@ -56,7 +62,7 @@
= render 'layouts/header/current_user_dropdown'
- if header_link?(:admin_impersonation)
%li.nav-item.impersonation
- = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _("Stop impersonation"), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- if header_link?(:sign_in)
%li.nav-item
@@ -64,8 +70,10 @@
- sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in')
= link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
-
%button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
- %span.sr-only= _("Toggle navigation")
+ %span.sr-only= _('Toggle navigation')
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
+
+- if can?(current_user, :update_user_status, current_user)
+ .js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } }
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index f53bd2b5e4d..c35451827c8 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -1,6 +1,7 @@
- container = @no_breadcrumb_container ? 'container-fluid' : container_class
- hide_top_links = @hide_top_links || false
+= yield :above_breadcrumbs_content
%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] }
.breadcrumbs-container
- if defined?(@left_sidebar)
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index ff25b040913..5f15ba87729 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -1,4 +1,4 @@
-.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
+.nav-sidebar.qa-admin-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to admin_root_path, title: _('Admin Overview') do
@@ -7,14 +7,14 @@
.sidebar-context-title
= _('Admin Area')
%ul.sidebar-top-level-items
- = nav_link(controller: %w(dashboard admin projects users groups jobs runners gitaly_servers), html_options: {class: 'home'}) do
+ = nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers), html_options: {class: 'home'}) do
= link_to admin_root_path, class: 'shortcuts-tree' do
.nav-icon-container
= sprite_icon('overview')
%span.nav-item-name
= _('Overview')
%ul.sidebar-sub-level-items
- = nav_link(controller: %w(dashboard admin projects users groups jobs runners gitaly_servers), html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_root_path do
%strong.fly-out-top-item-name
= _('Overview')
@@ -23,7 +23,7 @@
= link_to admin_root_path, title: _('Overview') do
%span
= _('Dashboard')
- = nav_link(controller: [:admin, :projects]) do
+ = nav_link(controller: [:admin, 'admin/projects']) do
= link_to admin_projects_path, title: _('Projects') do
%span
= _('Projects')
@@ -197,12 +197,56 @@
= link_to admin_application_settings_path do
.nav-icon-container
= sprite_icon('settings')
- %span.nav-item-name
+ %span.nav-item-name.qa-admin-settings-item
= _('Settings')
- %ul.sidebar-sub-level-items.is-fly-out-only
+
+ %ul.sidebar-sub-level-items.qa-admin-sidebar-submenu
= nav_link(controller: :application_settings, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_application_settings_path do
%strong.fly-out-top-item-name
= _('Settings')
+ %li.divider.fly-out-top-item
+ = nav_link(path: 'application_settings#show') do
+ = link_to admin_application_settings_path, title: _('General') do
+ %span
+ = _('General')
+ = nav_link(path: 'application_settings#integrations') do
+ = link_to integrations_admin_application_settings_path, title: _('Integrations') do
+ %span
+ = _('Integrations')
+ = nav_link(path: 'application_settings#repository') do
+ = link_to repository_admin_application_settings_path, title: _('Repository'), class: 'qa-admin-settings-repository-item' do
+ %span
+ = _('Repository')
+ - if template_exists?('admin/application_settings/templates')
+ = nav_link(path: 'application_settings#templates') do
+ = link_to templates_admin_application_settings_path, title: _('Templates') do
+ %span
+ = _('Templates')
+ = nav_link(path: 'application_settings#ci_cd') do
+ = link_to ci_cd_admin_application_settings_path, title: _('CI/CD') do
+ %span
+ = _('CI/CD')
+ = nav_link(path: 'application_settings#reporting') do
+ = link_to reporting_admin_application_settings_path, title: _('Reporting') do
+ %span
+ = _('Reporting')
+ = nav_link(path: 'application_settings#metrics_and_profiling') do
+ = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling') do
+ %span
+ = _('Metrics and profiling')
+ = nav_link(path: 'application_settings#network') do
+ = link_to network_admin_application_settings_path, title: _('Network') do
+ %span
+ = _('Network')
+ - if template_exists?('admin/application_settings/geo')
+ = nav_link(path: 'application_settings#geo') do
+ = link_to geo_admin_application_settings_path, title: _('Geo') do
+ %span
+ = _('Geo')
+ = nav_link(path: 'application_settings#preferences') do
+ = link_to preferences_admin_application_settings_path, title: _('Preferences') do
+ %span
+ = _('Preferences')
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index d471dd84550..4aa22138498 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -10,7 +10,7 @@
= group_icon(@group, class: "avatar s40 avatar-tile")
.sidebar-context-title
= @group.name
- %ul.sidebar-top-level-items
+ %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
= link_to group_path(@group) do
@@ -109,9 +109,9 @@
= link_to edit_group_path(@group) do
.nav-icon-container
= sprite_icon('settings')
- %span.nav-item-name
+ %span.nav-item-name.qa-group-settings-item
= _('Settings')
- %ul.sidebar-sub-level-items
+ %ul.sidebar-sub-level-items.qa-group-sidebar-submenu
= nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show], html_options: { class: "fly-out-top-item" } ) do
= link_to edit_group_path(@group) do
%strong.fly-out-top-item-name
@@ -122,12 +122,6 @@
%span
= _('General')
- = nav_link(controller: :badges) do
- = link_to group_settings_badges_path(@group), title: _('Project Badges') do
- %span
- = _('Project Badges')
-
-
= nav_link(path: 'groups#projects') do
= link_to projects_group_path(@group), title: _('Projects') do
%span
diff --git a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
index b8ff448f261..57180f27146 100644
--- a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
+++ b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
@@ -18,16 +18,17 @@
%strong.fly-out-top-item-name
= _('ConvDev Index')
- = nav_link(controller: :cohorts) do
- = link_to instance_statistics_cohorts_path do
- .nav-icon-container
- = sprite_icon('users')
- %span.nav-item-name
- = _('Cohorts')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do
- = link_to instance_statistics_cohorts_path do
- %strong.fly-out-top-item-name
- = _('Cohorts')
+ - if Gitlab::CurrentSettings.usage_ping_enabled
+ = nav_link(controller: :cohorts) do
+ = link_to instance_statistics_cohorts_path do
+ .nav-icon-container
+ = sprite_icon('users')
+ %span.nav-item-name
+ = _('Cohorts')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do
+ = link_to instance_statistics_cohorts_path do
+ %strong.fly-out-top-item-name
+ = _('Cohorts')
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index d65f153b451..69167edb1df 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -28,18 +28,17 @@
= link_to profile_account_path do
%strong.fly-out-top-item-name
= _('Account')
- - if Gitlab::CurrentSettings.user_oauth_applications?
- = nav_link(controller: 'oauth/applications') do
- = link_to applications_profile_path do
- .nav-icon-container
- = sprite_icon('applications')
- %span.nav-item-name
- = _('Applications')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" } ) do
- = link_to applications_profile_path do
- %strong.fly-out-top-item-name
- = _('Applications')
+ = nav_link(controller: 'oauth/applications') do
+ = link_to applications_profile_path do
+ .nav-icon-container
+ = sprite_icon('applications')
+ %span.nav-item-name
+ = _('Applications')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" } ) do
+ = link_to applications_profile_path do
+ %strong.fly-out-top-item-name
+ = _('Applications')
= nav_link(controller: :chat_names) do
= link_to profile_chat_names_path do
.nav-icon-container
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 34f47806205..25cd53b378a 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -158,7 +158,7 @@
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do
- = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
+ = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines' do
.nav-icon-container
= sprite_icon('rocket')
%span.nav-item-name
@@ -245,7 +245,7 @@
= link_to _('Auto DevOps'), help_page_path('topics/autodevops/index.md')
%span= _('uses Kubernetes clusters to deploy your code!')
%hr
- %button.btn.btn-create.btn-sm.dismiss-feature-highlight{ type: 'button' }
+ %button.btn.btn-success.btn-sm.dismiss-feature-highlight{ type: 'button' }
%span= _("Got it!")
= sprite_icon('thumb-up')
@@ -313,11 +313,6 @@
%span
= _('Members')
- if can_edit
- = nav_link(controller: :badges) do
- = link_to project_settings_badges_path(@project), title: _('Badges') do
- %span
- = _('Badges')
- - if can_edit
= nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
= link_to project_settings_integrations_path(@project), title: _('Integrations') do
%span
diff --git a/app/views/notify/_failed_builds.html.haml b/app/views/notify/_failed_builds.html.haml
new file mode 100644
index 00000000000..7c563bb016c
--- /dev/null
+++ b/app/views/notify/_failed_builds.html.haml
@@ -0,0 +1,32 @@
+%tr
+ %td{ colspan: 2, style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 0 8px 16px; text-align: center;" }
+ had
+ = failed.size
+ failed
+ #{'build'.pluralize(failed.size)}.
+%tr.table-warning
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; border: 1px solid #ededed; border-bottom: 0; border-radius: 4px 4px 0 0; overflow: hidden; background-color: #fdf4f6; color: #d22852; font-size: 14px; line-height: 1.4; text-align: center; padding: 8px 16px;" }
+ Logs may contain sensitive data. Please consider before forwarding this email.
+%tr.section
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 16px; border: 1px solid #ededed; border-radius: 4px; overflow: hidden; border-top: 0; border-radius: 0 0 4px 4px;" }
+ %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width: 100%; border-collapse: collapse;" }
+ %tbody
+ - failed.each do |build|
+ %tr.build-state
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #d22f57; font-weight: 500; font-size: 16px; vertical-align: middle; padding-right: 8px; line-height: 10px" }
+ %img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display: block;", width: "10" }/
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #8c8c8c; font-weight: 500; font-size: 14px; vertical-align: middle;" }
+ = build.stage
+ %td{ align: "right", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
+ = render "notify/links/#{build.to_partial_path}", pipeline: pipeline, build: build
+ %tr.build-log
+ - if build.has_trace?
+ %td{ colspan: "2", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 0 16px;" }
+ %pre{ style: "font-family: Monaco,'Lucida Console','Courier New',Courier,monospace; background-color: #fafafa; border-radius: 4px; overflow: hidden; white-space: pre-wrap; word-break: break-all; font-size:13px; line-height: 1.4; padding: 16px 8px; color: #333333; margin: 0;" }
+ = build.trace.html(last_lines: 10).html_safe
+ - else
+ %td{ colspan: "2" }
diff --git a/app/views/notify/autodevops_disabled_email.html.haml b/app/views/notify/autodevops_disabled_email.html.haml
new file mode 100644
index 00000000000..65a2f75a3e2
--- /dev/null
+++ b/app/views/notify/autodevops_disabled_email.html.haml
@@ -0,0 +1,49 @@
+%tr.alert
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 8px 16px; border-radius: 4px; font-size: 14px; line-height: 1.3; text-align: center; overflow: hidden; background-color: #d22f57; color: #ffffff;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse; margin: 0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; vertical-align: middle; color: #ffffff; text-align: center;" }
+ Auto DevOps pipeline was disabled for #{@project.name}
+
+%tr.pre-section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.7; padding: 16px 8px 0;" }
+ The Auto DevOps pipeline failed for pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration:none;" }
+ = "\##{@pipeline.iid}"
+ and has been disabled for
+ %a{ href: project_url(@project), style: "color: #1b69b6; text-decoration: none;" }
+ = @project.name + "."
+ In order to use the Auto DevOps pipeline with your project, please review the
+ %a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: "color:#1b69b6;text-decoration:none;" } currently supported languages,
+ adjust your project accordingly, and turn on the Auto DevOps pipeline within your
+ %a{ href: project_settings_ci_cd_url(@project), style: "color: #1b69b6; text-decoration: none;" }
+ CI/CD project settings.
+
+%tr.pre-section
+ %td{ style: 'text-align: center;border-bottom:1px solid #ededed' }
+ %a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/', style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %button{ type: 'button', style: 'border-color: #dfdfdf; border-style: solid; border-width: 1px; border-radius: 4px; font-size: 14px; padding: 8px 16px; background-color:#fff; margin: 8px 0; cursor: pointer;' }
+ Learn more about Auto DevOps
+
+%tr.pre-section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 16px 8px; text-align: center;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size:14px; font-weight:500;line-height: 1.4; vertical-align: baseline;" }
+ Pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration: none;" }
+ = "\##{@pipeline.id}"
+ triggered by
+ - if @pipeline.user
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 15px; line-height: 1.4; vertical-align: middle; padding-right: 8px; padding-left:8px", width: "24" }
+ %img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display: block; border-radius: 12px; margin: -2px 0;", width: "24", alt: "" }/
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 14px; font-weight: 500; line-height: 1.4; vertical-align: baseline;" }
+ %a.muted{ href: user_url(@pipeline.user), style: "color: #333333; text-decoration: none;" }
+ = @pipeline.user.name
+ - else
+ %td{ style: "font-family: 'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace; font-size: 14px; line-height: 1.4; vertical-align: baseline; padding:0 8px;" }
+ API
+
+= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb
new file mode 100644
index 00000000000..695780c3145
--- /dev/null
+++ b/app/views/notify/autodevops_disabled_email.text.erb
@@ -0,0 +1,20 @@
+Auto DevOps pipeline was disabled for <%= @project.name %>
+
+The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>).
+
+<% if @pipeline.user -%>
+ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+<% else -%>
+ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
+<% end -%>
+<% failed = @pipeline.statuses.latest.failed -%>
+had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
+
+<% failed.each do |build| -%>
+ <%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
+ Stage: <%= build.stage %>
+ Name: <%= build.name %>
+ <% if build.has_trace? -%>
+ Trace: <%= build.trace.raw(last_lines: 10) %>
+ <% end -%>
+<% end -%>
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index dd6a84e503d..5acd45b74a7 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -9,7 +9,7 @@
%p
Assignee: #{@merge_request.assignee_name}
-= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request
+= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter
- if @merge_request.description
%div
diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index d5b8f8d764f..754f4bca1cd 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -5,6 +5,6 @@ New Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= @merge_request.author_name %>
Assignee: <%= @merge_request.assignee_name %>
-<%= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request %>
+<%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %>
<%= @merge_request.description %>
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index baafaa6e3a0..86dcca4a447 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -107,36 +107,5 @@
- else
%td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
API
-- failed = @pipeline.statuses.latest.failed
-%tr
- %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
- had
- = failed.size
- failed
- #{'build'.pluralize(failed.size)}.
-%tr.table-warning
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
- Logs may contain sensitive data. Please consider before forwarding this email.
-%tr.section
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
- %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
- %tbody
- - failed.each do |build|
- %tr.build-state
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#d22f57;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;line-height:10px" }
- %img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
- = build.stage
- %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
- = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
- %tr.build-log
- - if build.has_trace?
- %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" }
- %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" }
- = build.trace.html(last_lines: 10).html_safe
- - else
- %td{ colspan: "2" }
+
+= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 04a19ab14dd..1823f191fb3 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -15,14 +15,16 @@
= f.label :email, class: 'label-bold'
= f.text_field :email, class: 'form-control'
.prepend-top-default
- = f.submit 'Add email address', class: 'btn btn-create'
+ = f.submit 'Add email address', class: 'btn btn-success'
%hr
%h4.prepend-top-0
Linked emails (#{@emails.count + 1})
.account-well.append-bottom-default
%ul
%li
- Your Primary Email will be used for avatar detection and web based operations, such as edits and merges.
+ Your Primary Email will be used for avatar detection.
+ %li
+ Your Commit Email will be used for web based operations, such as edits and merges.
%li
Your Notification Email will be used for account notifications.
%li
@@ -34,6 +36,8 @@
= render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? }
%span.float-right
%span.badge.badge-success Primary email
+ - if @primary_email === current_user.commit_email
+ %span.badge.badge-info Commit email
- if @primary_email === current_user.public_email
%span.badge.badge-info Public email
- if @primary_email === current_user.notification_email
@@ -42,6 +46,8 @@
%li
= render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
%span.float-right
+ - if email.email === current_user.commit_email
+ %span.badge.badge-info Commit email
- if email.email === current_user.public_email
%span.badge.badge-info Public email
- if email.email === current_user.notification_email
diff --git a/app/views/profiles/gpg_keys/_form.html.haml b/app/views/profiles/gpg_keys/_form.html.haml
index aa9b0aad034..6c4cb614a2b 100644
--- a/app/views/profiles/gpg_keys/_form.html.haml
+++ b/app/views/profiles/gpg_keys/_form.html.haml
@@ -7,4 +7,4 @@
= f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'."
.prepend-top-default
- = f.submit 'Add key', class: "btn btn-create"
+ = f.submit 'Add key', class: "btn btn-success"
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 5207921d6fe..21eef08983c 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -5,10 +5,10 @@
.form-group
= f.label :key, class: 'label-bold'
%p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.")
- = f.text_area :key, class: "form-control js-add-ssh-key-validation-input", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"')
+ = f.text_area :key, class: "form-control js-add-ssh-key-validation-input qa-key-public-key-field", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"')
.form-group
= f.label :title, class: 'label-bold'
- = f.text_field :title, class: "form-control input-lg", required: true, placeholder: s_('Profiles|e.g. My MacBook key')
+ = f.text_field :title, class: "form-control input-lg qa-key-title-field", required: true, placeholder: s_('Profiles|e.g. My MacBook key')
%p.form-text.text-muted= _('Name your individual key via a title')
.js-add-ssh-key-validation-warning.hide
@@ -16,7 +16,7 @@
%strong= _('Oops, are you sure?')
%p= s_("Profiles|This doesn't look like a public SSH key, are you sure you want to add it?")
- %button.btn.btn-create.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")
+ %button.btn.btn-success.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")
.prepend-top-default
- = f.submit s_('Profiles|Add key'), class: "btn btn-create js-add-ssh-key-validation-original-submit"
+ = f.submit s_('Profiles|Add key'), class: "btn btn-success js-add-ssh-key-validation-original-submit qa-add-key-button"
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 2ac514d3f6f..88473c7f72d 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -24,4 +24,4 @@
= @key.key
.col-md-12
.float-right
- = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key"
+ = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button"
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index 9c8cc9c059b..0b4b9841ea1 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -29,6 +29,6 @@
= f.label :password_confirmation, class: 'label-bold'
= f.password_field :password_confirmation, required: true, class: 'form-control'
.prepend-top-default.append-bottom-default
- = f.submit 'Save password', class: "btn btn-create append-right-10"
+ = f.submit 'Save password', class: "btn btn-success append-right-10"
- unless @user.password_automatically_set?
= link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link"
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index 2176d7f8a31..d265f3c44ba 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -1,6 +1,6 @@
- page_title "New Password"
- header_title "New Password"
-%h3.page-title Setup new password
+%h3.page-title Set up new password
%hr
= form_for @user, url: profile_password_path, method: :post do |f|
%p.slead
@@ -22,4 +22,4 @@
.col-sm-10
= f.password_field :password_confirmation, required: true, class: 'form-control'
.form-actions
- = f.submit 'Set new password', class: "btn btn-create"
+ = f.submit 'Set new password', class: "btn btn-success"
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index fd6dd74e1c5..156c0d05b02 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -58,4 +58,4 @@
.form-text.text-muted
Choose what content you want to see on a project’s overview page
.form-group
- = f.submit 'Save changes', class: 'btn btn-save'
+ = f.submit 'Save changes', class: 'btn btn-success'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 6f08a294c5d..ea215e3e718 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,5 +1,6 @@
-- breadcrumb_title "Edit Profile"
+- breadcrumb_title s_("Profiles|Edit Profile")
- @content_class = "limit-container-width" unless fluid_layout
+- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f|
= form_errors(@user)
@@ -7,34 +8,36 @@
.row
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- Public Avatar
+ = s_("Profiles|Public Avatar")
%p
- if @user.avatar?
- You can change your avatar here
- if gravatar_enabled?
- or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host}
+ = s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
+ - else
+ = s_("Profiles|You can change your avatar here")
- else
- You can upload an avatar here
- if gravatar_enabled?
- or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host}
+ = s_("Profiles|You can upload your avatar here or change it at %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
+ - else
+ = s_("Profiles|You can upload your avatar here")
.col-lg-8
.clearfix.avatar-image.append-bottom-default
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon_for_user(@user, 160), alt: '', class: 'avatar s160'
- %h5.prepend-top-0= _("Upload new avatar")
+ %h5.prepend-top-0= s_("Profiles|Upload new avatar")
.prepend-top-5.append-bottom-10
- %button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...")
- %span.avatar-file-name.prepend-left-default.js-avatar-filename= _("No file chosen")
+ %button.btn.js-choose-user-avatar-button{ type: 'button' }= s_("Profiles|Choose file...")
+ %span.avatar-file-name.prepend-left-default.js-avatar-filename= s_("Profiles|No file chosen")
= f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
- .form-text.text-muted= _("The maximum file size allowed is 200KB.")
+ .form-text.text-muted= s_("Profiles|The maximum file size allowed is 200KB.")
- if @user.avatar?
%hr
- = link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted'
+ = link_to s_("Profiles|Remove avatar"), profile_avatar_path, data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") }, method: :delete, class: 'btn btn-danger btn-inverted'
%hr
.row
.col-lg-4.profile-settings-sidebar
- %h4.prepend-top-0= s_("User|Current status")
+ %h4.prepend-top-0= s_("Profiles|Current status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.col-lg-8
= f.fields_for :status, @user.status do |status_form|
@@ -66,62 +69,70 @@
.row
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- Main settings
+ = s_("Profiles|Main settings")
%p
- This information will appear on your profile.
+ = s_("Profiles|This information will appear on your profile.")
- if current_user.ldap_user?
- Some options are unavailable for LDAP accounts
+ = s_("Profiles|Some options are unavailable for LDAP accounts")
.col-lg-8
.row
- if @user.read_only_attribute?(:name)
= f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' },
- help: "Your name was automatically set based on your #{ attribute_provider_label(:name) } account, so people you know can recognize you."
+ help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you.") % { provider_label: attribute_provider_label(:name) }
- else
- = f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you."
+ = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you."
= f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' }
- if @user.read_only_attribute?(:email)
- = f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{ attribute_provider_label(:email) } account."
+ = f.text_field :email, required: true, readonly: true, help: s_("Profiles|Your email address was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:email) }
- else
= f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?),
help: user_email_help_text(@user)
= f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email),
- { help: 'This email will be displayed on your public profile.', include_blank: 'Do not show on profile' },
+ { help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") },
+ control_class: 'select2'
+ = f.select :commit_email, options_for_select(@user.verified_emails, selected: @user.commit_email),
+ { help: 'This email will be used for web based operations, such as edits and merges.' },
control_class: 'select2'
= f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] },
- { help: 'This feature is experimental and translations are not complete yet.' },
+ { help: s_("Profiles|This feature is experimental and translations are not complete yet.") },
control_class: 'select2'
= f.text_field :skype
= f.text_field :linkedin
= f.text_field :twitter
- = f.text_field :website_url, label: 'Website'
+ = f.text_field :website_url, label: s_("Profiles|Website")
- if @user.read_only_attribute?(:location)
- = f.text_field :location, readonly: true, help: "Your location was automatically set based on your #{ attribute_provider_label(:location) } account."
+ = f.text_field :location, readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:location) }
- else
= f.text_field :location
= f.text_field :organization
- = f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.'
+ = f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters.")
%hr
- %h5 Private profile
- - private_profile_label = capture do
- Don't display activity-related personal information on your profile
+ %h5= ("Private profile")
+ .checkbox-icon-inline-wrapper
+ - private_profile_label = capture do
+ = s_("Profiles|Don't display activity-related personal information on your profiles")
+ = f.check_box :private_profile, label: private_profile_label
= link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile')
- = f.check_box :private_profile, label: private_profile_label
+ %h5= s_("Profiles|Private contributions")
+ = f.check_box :include_private_contributions, label: 'Include private contributions on my profile'
+ .help-block
+ = s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.")
.prepend-top-default.append-bottom-default
- = f.submit 'Update profile settings', class: 'btn btn-success'
- = link_to 'Cancel', user_path(current_user), class: 'btn btn-cancel'
+ = f.submit s_("Profiles|Update profile settings"), class: 'btn btn-success'
+ = link_to _("Cancel"), user_path(current_user), class: 'btn btn-cancel'
.modal.modal-profile-crop
.modal-dialog
.modal-content
.modal-header
%h4.modal-title
- Position and size your new avatar
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+ = s_("Profiles|Position and size your new avatar")
+ %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _("Close") }
%span{ "aria-hidden": true } &times;
.modal-body
.profile-crop-image-container
- %img.modal-profile-crop-image{ alt: 'Avatar cropper' }
+ %img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") }
.crop-controls
.btn-group
%button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } }
@@ -130,4 +141,4 @@
%span.fa.fa-search-minus
.modal-footer
%button.btn.btn-primary.js-upload-user-avatar{ type: 'button' }
- Set new profile picture
+ = s_("Profiles|Set new profile picture")
diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml
index 93722d7b034..759d39cf5f5 100644
--- a/app/views/profiles/two_factor_auths/_codes.html.haml
+++ b/app/views/profiles/two_factor_auths/_codes.html.haml
@@ -1,5 +1,5 @@
%p.slead
- Should you ever lose your phone, each of these recovery codes can be used one
+ Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one
time each to regain access to your account. Please save them in a safe place, or you
%b will
lose access to your account.
@@ -10,4 +10,6 @@
%li
%span.monospace= code
-= link_to 'Proceed', profile_account_path, class: 'btn btn-success'
+.d-flex
+ = link_to 'Proceed', profile_account_path, class: 'btn btn-success append-right-10'
+ = link_to 'Download codes', "data:text/plain;charset=utf-8,#{CGI.escape(@codes.join("\n"))}", download: "gitlab-recovery-codes.txt", class: 'btn btn-default'
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index cd10b8758f6..94ec0cc5db8 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -6,13 +6,13 @@
.row.prepend-top-default
.col-lg-4
%h4.prepend-top-0
- Register Two-Factor Authentication App
+ Register Two-Factor Authenticator
%p
- Use an app on your mobile device to enable two-factor authentication (2FA).
+ Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA).
.col-lg-8
- if current_user.two_factor_otp_enabled?
%p
- You've already enabled two-factor authentication using mobile authenticator applications. In order to register a different device, you must first disable two-factor authentication.
+ You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication.
%p
If you lose your recovery codes you can generate new ones, invalidating all previous codes.
%div
diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml
index b387e38c1a6..1e27c71d20d 100644
--- a/app/views/projects/_commit_button.html.haml
+++ b/app/views/projects/_commit_button.html.haml
@@ -1,5 +1,5 @@
.form-actions
- = button_tag 'Commit changes', class: 'btn commit-btn js-commit-button btn-create'
+ = button_tag 'Commit changes', class: 'btn commit-btn js-commit-button btn-success'
= link_to 'Cancel', cancel_path,
class: 'btn btn-cancel', data: {confirm: leave_edit_message}
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index 0175b519867..7a5fff96676 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -5,3 +5,4 @@
- if current_user && can?(current_user, :download_code, project)
= render 'shared/no_ssh'
= render 'shared/no_password'
+ = render 'shared/auto_devops_implicitly_enabled_banner', project: project
diff --git a/app/views/projects/_fork_suggestion.html.haml b/app/views/projects/_fork_suggestion.html.haml
index c855bfaf067..0b616a0c1ce 100644
--- a/app/views/projects/_fork_suggestion.html.haml
+++ b/app/views/projects/_fork_suggestion.html.haml
@@ -6,6 +6,6 @@
edit
files in this project directly. Please fork this project,
make your changes there, and submit a merge request.
- = link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-new'
+ = link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-success'
%button.js-cancel-fork-suggestion-button.btn.btn-grouped{ type: 'button' }
Cancel
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index fbe88ec9618..ced6a2a0399 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,17 +1,35 @@
- empty_repo = @project.empty_repo?
-- fork_network = @project.fork_network
-.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
+- license = @project.license_anchor_data
+.project-home-panel{ class: ("empty-project" if empty_repo) }
.limit-container-width{ class: container_class }
- .avatar-container.s70.project-avatar
- = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile', width: 70, height: 70)
- %h1.project-title.qa-project-name
- = @project.name
- %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
- = visibility_level_icon(@project.visibility_level, fw: false)
+ .project-header.d-flex.flex-row.flex-wrap.align-items-center.append-bottom-8
+ .project-title-row.d-flex.align-items-center
+ .avatar-container.project-avatar.float-none
+ = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile')
+ %h1.project-title.d-flex.align-items-baseline.qa-project-name
+ = @project.name
+ .project-metadata.d-flex.flex-row.flex-wrap.align-items-baseline
+ .project-visibility.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
+ = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
+ = visibility_level_label(@project.visibility_level)
+ - if license.present?
+ .project-license.d-inline-flex.align-items-baseline
+ = link_to_if license.link, sprite_icon('scale', size: 16, css_class: 'icon') + license.label, license.link, class: license.enabled ? 'btn btn-link btn-secondary-hover-link' : 'btn btn-link'
+ - if @project.tag_list.present?
+ .project-tag-list.d-inline-flex.align-items-baseline.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil }
+ = sprite_icon('tag', size: 16, css_class: 'icon')
+ = @project.tags_to_show
+ - if @project.has_extra_tags?
+ = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown }
.project-home-desc
- if @project.description.present?
- = markdown_field(@project, :description)
+ .project-description
+ .project-description-markdown.read-more-container
+ = markdown_field(@project, :description)
+ %button.btn.btn-blank.btn-link.text-secondary.js-read-more-trigger.text-secondary.d-lg-none{ type: "button" }
+ = _("Read more")
+
- if can?(current_user, :read_project, @project)
.text-secondary.prepend-top-8
= s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
@@ -26,34 +44,42 @@
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_source_name(@project) }
- .project-badges.prepend-top-default.append-bottom-default
- - @project.badges.each do |badge|
- %a.append-right-8{ href: badge.rendered_link_url(@project),
- target: '_blank',
- rel: 'noopener noreferrer' }>
- %img.project-badge{ src: badge.rendered_image_url(@project),
- 'aria-hidden': true,
- alt: '' }>
-
- .project-repo-buttons
- .count-buttons
+ - if @project.badges.present?
+ .project-badges.prepend-top-default.append-bottom-default
+ - @project.badges.each do |badge|
+ %a.append-right-8{ href: badge.rendered_link_url(@project),
+ target: '_blank',
+ rel: 'noopener noreferrer' }>
+ %img.project-badge{ src: badge.rendered_image_url(@project),
+ 'aria-hidden': true,
+ alt: 'Project badge' }>
+
+ .project-repo-buttons.d-inline-flex.flex-wrap
+ .count-buttons.d-inline-flex
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- %span.d-none.d-sm-inline
- - if can?(current_user, :download_code, @project)
- .project-clone-holder
- = render "shared/clone_panel"
+ - if can?(current_user, :download_code, @project)
+ .project-clone-holder.d-inline-flex.d-sm-none
+ = render "shared/mobile_clone_panel"
- - if show_xcode_link?(@project)
- .project-action-button.project-xcode.inline
- = render "projects/buttons/xcode_link"
+ .project-clone-holder.d-none.d-sm-inline-flex
+ = render "shared/clone_panel"
- - if current_user
- - if can?(current_user, :download_code, @project)
+ - if show_xcode_link?(@project)
+ .project-action-button.project-xcode.inline
+ = render "projects/buttons/xcode_link"
+
+ - if current_user
+ - if can?(current_user, :download_code, @project)
+ .d-none.d-sm-inline-flex
= render 'projects/buttons/download', project: @project, ref: @ref
+ .d-none.d-sm-inline-flex
= render 'projects/buttons/dropdown'
+ .d-none.d-sm-inline-flex
= render 'projects/buttons/koding'
+ .d-none.d-sm-inline-flex
= render 'shared/notifications/button', notification_setting: @notification_setting
+ .d-none.d-sm-inline-flex
= render 'shared/members/access_request_buttons', source: @project
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 70e1c557547..2b425f18389 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -1,4 +1,5 @@
- active_tab = local_assigns.fetch(:active_tab, 'blank')
+- track_label = local_assigns.fetch(:track_label, 'import_project')
.project-import
.form-group.import-btn-container.clearfix
@@ -7,60 +8,63 @@
.import-buttons
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
+ = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_export" } do
= icon('gitlab', text: 'GitLab export')
- if github_import_enabled?
%div
- = link_to new_import_github_path, class: 'btn js-import-github' do
+ = link_to new_import_github_path, class: 'btn js-import-github', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "github" } do
= icon('github', text: 'GitHub')
- if bitbucket_import_enabled?
%div
- = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}",
+ data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_cloud" } do
= icon('bitbucket', text: 'Bitbucket Cloud')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
- if bitbucket_server_import_enabled?
%div
- = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket" do
+ = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket",
+ data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_server" } do
= icon('bitbucket-square', text: 'Bitbucket Server')
%div
- if gitlab_import_enabled?
%div
- = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}",
+ data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_com" } do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
- if google_code_import_enabled?
%div
- = link_to new_import_google_code_path, class: 'btn import_google_code' do
+ = link_to new_import_google_code_path, class: 'btn import_google_code', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "google_code" } do
= icon('google', text: 'Google Code')
- if fogbugz_import_enabled?
%div
- = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "fogbugz" } do
= icon('bug', text: 'Fogbugz')
- if gitea_import_enabled?
%div
- = link_to new_import_gitea_path, class: 'btn import_gitea' do
+ = link_to new_import_gitea_path, class: 'btn import_gitea', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitea" } do
= custom_icon('go_logo')
Gitea
- if git_import_enabled?
%div
- %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
+ %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active', data: { toggle_open_class: 'active', track_label: "#{track_label}" , track_event: "click_button", track_property: "repo_url" } } }
= icon('git', text: 'Repo by URL')
- if manifest_import_enabled?
%div
- = link_to new_import_manifest_path, class: 'btn import_manifest' do
+ = link_to new_import_manifest_path, class: 'btn import_manifest', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "manifest_file" } do
= icon('file-text-o', text: 'Manifest file')
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
= form_for @project, html: { class: 'new_project' } do |f|
%hr
= render "shared/import_form", f: f
- = render 'new_project_fields', f: f, project_name_id: "import-url-name"
+ = render 'new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true, track_label: track_label
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 8fb6aa55436..c62328e3fb9 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -18,14 +18,17 @@
Preview
%li.md-header-toolbar.active
- = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" })
- = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" })
- = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
- = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
- = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
- = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
- = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
- %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: "Go full screen", data: { container: "body" } }
+ = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") })
+ = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: s_("MarkdownToolbar|Add italic text") })
+ = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: s_("MarkdownToolbar|Insert a quote") })
+ = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: s_("MarkdownToolbar|Insert code") })
+ = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: s_("MarkdownToolbar|Add a link") })
+ = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a bullet list") })
+ = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a numbered list") })
+ = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a task list") })
+ = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header 1 | header 2 |\n| -------- | -------- |\n| cell 1 | cell 2 |\n| cell 3 | cell 4 |", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a table") })
+ = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _('Add a table') })
+ %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: s_("MarkdownToolbar|Go full screen"), data: { container: "body" } }
= sprite_icon("screen-full")
.md-write-holder
diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml
index 540e996e4d8..935581643cd 100644
--- a/app/views/projects/_merge_request_merge_method_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_method_settings.html.haml
@@ -1,5 +1,4 @@
- form = local_assigns.fetch(:form)
-- project = local_assigns.fetch(:project)
.form-group
= label_tag :merge_method_merge, class: 'label-bold' do
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index ad8c7911fad..e1f28364e19 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -1,12 +1,17 @@
- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
+- hide_init_with_readme = local_assigns.fetch(:hide_init_with_readme, false)
+- track_label = local_assigns.fetch(:track_label, 'blank_project')
.row{ id: project_name_id }
= f.hidden_field :ci_cd_only, value: ci_cd_only
+ .form-group.project-name.col-sm-12
+ = f.label :name, class: 'label-bold' do
+ %span= _("Project name")
+ = f.text_field :name, placeholder: "My awesome project", class: "form-control input-lg", autofocus: true, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" }
.form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-bold' do
- %span
- Project path
+ %span= s_("Project URL")
.input-group
- if current_user.can_select_namespace?
.input-group-prepend.has-tooltip{ title: root_url }
@@ -18,7 +23,7 @@
display_path: true,
extra_group: namespace_id),
{},
- { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1})
+ { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }})
- else
.input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
@@ -27,34 +32,34 @@
= f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.project-path.col-sm-6
= f.label :path, class: 'label-bold' do
- %span
- Project name
- = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
+ %span= _("Project slug")
+ = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, required: true
- if current_user.can_create_group?
.form-text.text-muted
Want to house several dependent projects under the same namespace?
- = link_to "Create a group", new_group_path
+ = link_to "Create a group.", new_group_path
.form-group
= f.label :description, class: 'label-bold' do
Project description
%span (optional)
- = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250
+ = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" }
= f.label :visibility_level, class: 'label-bold' do
Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer'
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
-.form-group.row.initialize-with-readme-setting
- %div{ :class => "col-sm-12" }
- .form-check
- = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input'
- = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
- .option-title
- %strong Initialize repository with a README
- .option-description
- Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.
+- if !hide_init_with_readme
+ .form-group.row.initialize-with-readme-setting
+ %div{ :class => "col-sm-12" }
+ .form-check
+ = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" }
+ = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
+ .option-title
+ %strong Initialize repository with a README
+ .option-description
+ Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.
-= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
-= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
+= f.submit 'Create project', class: "btn btn-success project-submit", tabindex: 4, data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
+= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" }
diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml
index e90a6355214..98fdb1d7a0b 100644
--- a/app/views/projects/_project_templates.html.haml
+++ b/app/views/projects/_project_templates.html.haml
@@ -5,4 +5,4 @@
.project-fields-form
= render 'projects/project_templates/project_fields_form'
- = render 'projects/new_project_fields', f: f, project_name_id: "template-project-name"
+ = render 'projects/new_project_fields', f: f, project_name_id: "template-project-name", hide_init_with_readme: true, track_label: "create_from_template"
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index 705338c083e..32624ac225b 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -20,4 +20,4 @@
distributed with computer software, forming part of its documentation.
GitLab will render it here instead of this message.
%p
- = link_to "Add Readme", @project.add_readme_path, class: 'btn btn-new'
+ = link_to "Add Readme", @project.add_readme_path, class: 'btn btn-success'
diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml
index 15ec58289e3..4cf49f3cf62 100644
--- a/app/views/projects/_stat_anchor_list.html.haml
+++ b/app/views/projects/_stat_anchor_list.html.haml
@@ -1,7 +1,7 @@
- anchors = local_assigns.fetch(:anchors, [])
- return unless anchors.any?
-%ul.nav.justify-content-center
+%ul.nav
- anchors.each do |anchor|
%li.nav-item
= link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do
diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml
index 5646dc464f8..5adca007f7e 100644
--- a/app/views/projects/_wiki.html.haml
+++ b/app/views/projects/_wiki.html.haml
@@ -2,7 +2,7 @@
%div{ class: container_class }
.prepend-top-default.append-bottom-default
.wiki
- = render_wiki_content(@wiki_home)
+ = render_wiki_content(@wiki_home, legacy_render_context(params))
- else
- can_create_wiki = can?(current_user, :create_wiki, @project)
.project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index a4b1b496b69..cf273aab108 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -5,6 +5,7 @@
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref
+ = render_if_exists 'projects/blob/owners', blob: blob
= render "projects/blob/auxiliary_viewer", blob: blob
#blob-content-holder.blob-content-holder
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index 6f3a691518b..e9010dc63fc 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -15,7 +15,7 @@
= render 'shared/new_commit_form', placeholder: _("Add new directory")
.form-actions
- = submit_tag _("Create directory"), class: 'btn btn-create'
+ = submit_tag _("Create directory"), class: 'btn btn-success'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
= render 'shared/projects/edit_information'
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 0a5c73c9037..d2b3c8ef96b 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -20,7 +20,7 @@
= render 'shared/new_commit_form', placeholder: placeholder
.form-actions
- = button_tag class: 'btn btn-create btn-upload-file', id: 'submit-all', type: 'button' do
+ = button_tag class: 'btn btn-success btn-upload-file', id: 'submit-all', type: 'button' do
= icon('spin spinner', class: 'js-loading-icon hidden' )
= button_title
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 27cf040da7c..fdab8a53b41 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -21,7 +21,7 @@
Write
%li
- = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do
+ = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id, legacy_render: params[:legacy_render]) do
= editing_preview_title(@blob.name)
= form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml
index da2cef17e8a..eb65cd90ea8 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -2,7 +2,7 @@
.diff-content
- if markup?(@blob.name)
.file-content.wiki
- = markup(@blob.name, @content)
+ = markup(@blob.name, @content, legacy_render_context(params))
- else
.file-content.code.js-syntax-highlight
- unless @diff_lines.empty?
diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
index 28c5be6ebf3..5be7cc7f25a 100644
--- a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
+++ b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
@@ -1,9 +1,9 @@
-- if viewer.valid?
+- if viewer.valid?(@project, @commit.sha)
= icon('check fw')
This GitLab CI configuration is valid.
- else
= icon('warning fw')
This GitLab CI configuration is invalid:
- = viewer.validation_message
+ = viewer.validation_message(@project, @commit.sha)
= link_to 'Learn more', help_page_path('ci/yaml/README')
diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml
index 230305b488d..bd12cadf240 100644
--- a/app/views/projects/blob/viewers/_markup.html.haml
+++ b/app/views/projects/blob/viewers/_markup.html.haml
@@ -1,4 +1,6 @@
- blob = viewer.blob
-- rendered_markup = blob.rendered_markup if blob.respond_to?(:rendered_markup)
+- context = legacy_render_context(params)
+- unless context[:markdown_engine] == :redcarpet
+ - context[:rendered] = blob.rendered_markup if blob.respond_to?(:rendered_markup)
.file-content.wiki
- = markup(blob.name, blob.data, rendered: rendered_markup)
+ = markup(blob.name, blob.data, context)
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index d6568c9f64a..ca867961f6b 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -41,7 +41,7 @@
data: { confirm: s_('Branches|Deleting the merged branches cannot be undone. Are you sure?'),
container: 'body' } do
= s_('Branches|Delete merged branches')
- = link_to new_project_branch_path(@project), class: 'btn btn-create' do
+ = link_to new_project_branch_path(@project), class: 'btn btn-success' do
= s_('Branches|New branch')
- if can?(current_user, :admin_project, @project)
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 65b414c8af2..500536a5dbc 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -26,7 +26,7 @@
= render 'shared/ref_dropdown', dropdown_class: 'wide'
.form-text.text-muted Existing branch name, tag, or commit SHA
.form-actions
- = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
+ = button_tag 'Create branch', class: 'btn btn-success', tabindex: 3
= link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index f880556a9f7..8da27ca7cb3 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -1,17 +1,17 @@
- unless @project.empty_repo?
- if current_user && can?(current_user, :fork_project, @project)
- - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
- = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do
- = custom_icon('icon_fork')
- %span= s_('GoToYourFork|Fork')
- - else
- - can_create_fork = current_user.can?(:create_fork)
- = link_to new_project_fork_path(@project),
- class: "btn btn-default #{'has-tooltip disabled' unless can_create_fork}",
- title: (_('You have reached your project limit') unless can_create_fork) do
- = custom_icon('icon_fork')
- %span= s_('CreateNewFork|Fork')
- .count-with-arrow
- %span.arrow
- = link_to project_forks_path(@project), title: n_('Fork', 'Forks', @project.forks_count), class: 'count' do
- = @project.forks_count
+ .count-badge.d-inline-flex.align-item-stretch.append-right-8
+ %span.fork-count.count-badge-count.d-flex.align-items-center
+ = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do
+ = @project.forks_count
+ - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do
+ = sprite_icon('fork', { css_class: 'icon' })
+ %span= s_('ProjectOverview|Fork')
+ - else
+ - can_create_fork = current_user.can?(:create_fork)
+ = link_to new_project_fork_path(@project),
+ class: "btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}",
+ title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do
+ = sprite_icon('fork', { css_class: 'icon' })
+ %span= s_('ProjectOverview|Fork')
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index a2dc2730ecc..0d04ecb3a58 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,21 +1,19 @@
- if current_user
- %button.btn.btn-default.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }>
- - if current_user.starred?(@project)
- = sprite_icon('star')
- %span.starred= _('Unstar')
- - else
- = sprite_icon('star-o')
- %span= s_('StarProject|Star')
- .count-with-arrow
- %span.arrow
- %span.count.star-count
+ .count-badge.d-inline-flex.align-item-stretch.append-right-8
+ %span.star-count.count-badge-count.d-flex.align-items-center
= @project.star_count
+ %button.count-badge-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }
+ - if current_user.starred?(@project)
+ = sprite_icon('star', { css_class: 'icon' })
+ %span.starred= s_('ProjectOverview|Unstar')
+ - else
+ = sprite_icon('star-o', { css_class: 'icon' })
+ %span= s_('ProjectOverview|Star')
- else
- = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: _('You must sign in to star a project') do
- = sprite_icon('star')
- #{ s_('StarProject|Star') }
- .count-with-arrow
- %span.arrow
- %span.count
+ .count-badge.d-inline-flex.align-item-stretch.append-right-8
+ %span.star-count.count-badge-count.d-flex.align-items-center
= @project.star_count
+ = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
+ = sprite_icon('star-o', { css_class: 'icon' })
+ %span= s_('ProjectOverview|Star')
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 44c1453e239..59c297c46a5 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -47,7 +47,9 @@
%span.badge.badge-info triggered
- if job.try(:allow_failure)
%span.badge.badge-danger allowed to fail
- - if job.action?
+ - if job.schedulable?
+ %span.badge.badge-info= s_('DelayedJobs|scheduled')
+ - elsif job.action?
%span.badge.badge-info manual
- if pipeline_link
@@ -101,6 +103,24 @@
- if job.active?
= link_to cancel_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred')
+ - elsif job.scheduled?
+ .btn-group
+ .btn.btn-default.has-tooltip{ disabled: true,
+ title: job.scheduled_at }
+ = sprite_icon('planning')
+ = duration_in_numbers(job.execute_in, true)
+ - confirmation_message = s_("DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes.") % { job_name: job.name }
+ = link_to play_project_job_path(job.project, job, return_to: request.original_url),
+ method: :post,
+ title: s_('DelayedJobs|Start now'),
+ class: 'btn btn-default btn-build has-tooltip',
+ data: { confirm: confirmation_message } do
+ = sprite_icon('play')
+ = link_to unschedule_project_job_path(job.project, job, return_to: request.original_url),
+ method: :post,
+ title: s_('DelayedJobs|Unschedule'),
+ class: 'btn btn-default btn-build has-tooltip' do
+ = sprite_icon('time-out')
- elsif allow_retry
- if job.playable? && !admin && can?(current_user, :update_build, job)
= link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml
index f18caa3f4ac..141314b4e4e 100644
--- a/app/views/projects/clusters/_banner.html.haml
+++ b/app/views/projects/clusters/_banner.html.haml
@@ -1,14 +1,15 @@
-%h4= s_('ClusterIntegration|Kubernetes cluster integration')
+.hidden.js-cluster-error.bs-callout.bs-callout-danger{ role: 'alert' }
+ = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine')
+ %p.js-error-reason
-.settings-content
- .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine')
- %p.js-error-reason
+.hidden.js-cluster-creating.bs-callout.bs-callout-info{ role: 'alert' }
+ = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...')
- .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...')
+.hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' }
+ = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details")
- .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
- = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details")
-
- %p= s_('ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab')
+- if show_cluster_security_warning?
+ .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning{ data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } }
+ %button.close.js-close{ type: "button" } &times;
+ = s_("ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application.")
+ = link_to s_("More information"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
index 73b11d509d3..85d1002243b 100644
--- a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
@@ -1,6 +1,6 @@
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
-.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' }
- %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } &times;
+.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } }
+ %button.close.js-close{ type: "button" } &times;
.gcp-signup-offer--content
.gcp-signup-offer--icon.append-right-8
= sprite_icon("information", size: 16)
diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml
index b46b45fea49..d0a553e3414 100644
--- a/app/views/projects/clusters/_integration_form.html.haml
+++ b/app/views/projects/clusters/_integration_form.html.haml
@@ -2,14 +2,6 @@
= form_errors(@cluster)
.form-group
%h5= s_('ClusterIntegration|Integration status')
- %p
- - if @cluster.enabled?
- - if can?(current_user, :update_cluster, @cluster)
- = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab\'s connection to it.')
- - else
- = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.')
- - else
- = s_('ClusterIntegration|Kubernetes cluster integration is disabled for this project.')
%label.append-bottom-0.js-cluster-enable-toggle-area
%button{ type: 'button',
class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
@@ -19,14 +11,13 @@
%span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
+ .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.')
- if has_multiple_clusters?(@project)
.form-group
%h5= s_('ClusterIntegration|Environment scope')
- %p
- = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
- = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
- = field.text_field :environment_scope, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
+ = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
+ .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
- if can?(current_user, :update_cluster, @cluster)
.form-group
@@ -38,8 +29,3 @@
%code *
is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster.
= link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope')
-
- %h5= s_('ClusterIntegration|Security')
- %p
- = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.")
- = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index 9133de6559d..eaf3a93bd15 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -62,4 +62,13 @@
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
.form-group
+ .form-check
+ = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
+ = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+ = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
+
+ .form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml
index 877e0cc876c..779c9c245c1 100644
--- a/app/views/projects/clusters/gcp/_show.html.haml
+++ b/app/views/projects/clusters/gcp/_show.html.haml
@@ -38,4 +38,12 @@
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
+ .form-check
+ = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+
+ .form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index 08d2deff6f8..eddd3613c5f 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -23,7 +23,8 @@
.js-cluster-application-notice
.flash-container
- %section.settings.no-animate.expanded#cluster-integration
+ %section#cluster-integration
+ %h4= @cluster.name
= render 'banner'
= render 'integration_form'
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index e8ef0008802..56551ed4d65 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -26,4 +26,13 @@
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
+ .form-check
+ = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+ = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
+
+ .form-group
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
index 20a07d6695e..5b57f7ceb7d 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -27,4 +27,12 @@
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
+ .form-check
+ = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+
+ .form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index afd70ef5774..e71615dd1c5 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -33,7 +33,7 @@
- else
= hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
- = submit_tag label, class: 'btn btn-create'
+ = submit_tag label, class: 'btn btn-success'
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
= render 'shared/projects/edit_information'
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 7951a5ddc9e..c6789e32dbe 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -1,3 +1,7 @@
+-#-----------------------------------------------------------------
+ WARNING: Please keep changes up-to-date with the following files:
+ - `assets/javascripts/diffs/components/commit_item.vue`
+-#-----------------------------------------------------------------
- view_details = local_assigns.fetch(:view_details, false)
- merge_request = local_assigns.fetch(:merge_request, nil)
- project = local_assigns.fetch(:project) { merge_request&.project }
@@ -37,7 +41,7 @@
%button.text-expander.js-toggle-button
= sprite_icon('ellipsis_h', size: 12)
- .commiter
+ .committer
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom')
- commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 07112c98804..d24ee4a3251 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -22,7 +22,7 @@
.dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag")
= render 'shared/ref_dropdown'
&nbsp;
- = button_tag s_("CompareBranches|Compare"), class: "btn btn-create commits-compare-btn"
+ = button_tag s_("CompareBranches|Compare"), class: "btn btn-success commits-compare-btn"
- if @merge_request.present?
= link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'prepend-left-10 btn'
- elsif create_mr_button?
diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml
new file mode 100644
index 00000000000..ff6a9d49a61
--- /dev/null
+++ b/app/views/projects/default_branch/_show.html.haml
@@ -0,0 +1,21 @@
+- expanded = Rails.env.test?
+
+%section.settings.no-animate#default-branch-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4= _('Default Branch')
+ %button.btn.js-settings-toggle
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Select the branch you want to set as the default for this project. All merge requests and commits will automatically be made against this branch unless you specify a different one.')
+
+ .settings-content
+ - if @project.empty_repo?
+ .text-secondary
+ = _('A default branch cannot be chosen for an empty project.')
+ - else
+ = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f|
+ %fieldset
+ .form-group
+ = f.label :default_branch, "Default Branch", class: 'label-bold'
+ = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index f8ab0c1ec54..568930595a2 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -21,4 +21,4 @@
Allow this key to push to repository as well? (Default only allows pull access.)
.form-group.row
- = f.submit "Add key", class: "btn-create btn"
+ = f.submit "Add key", class: "btn-success btn"
diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml
index e009b6fef0e..3e7872ebc1c 100644
--- a/app/views/projects/deploy_keys/edit.html.haml
+++ b/app/views/projects/deploy_keys/edit.html.haml
@@ -6,5 +6,5 @@
= form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'js-requires-input' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
- = f.submit 'Save changes', class: 'btn-save btn'
+ = f.submit 'Save changes', class: 'btn-success btn'
= link_to 'Cancel', project_settings_repository_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index e47361354f3..4b1d4b3ea17 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -5,7 +5,6 @@
- diff_file.parallel_diff_lines.each do |line|
- left = line[:left]
- right = line[:right]
- - last_line = right.new_pos if right
- discussions_left, discussions_right = parallel_diff_discussions(left, right, diff_file)
%tr.line_holder.parallel
- if left
diff --git a/app/views/projects/diffs/_single_image_diff.html.haml b/app/views/projects/diffs/_single_image_diff.html.haml
index 12be8beab39..454f814795a 100644
--- a/app/views/projects/diffs/_single_image_diff.html.haml
+++ b/app/views/projects/diffs/_single_image_diff.html.haml
@@ -1,7 +1,5 @@
- blob = diff_file.blob
-- old_blob = diff_file.old_blob
- blob_raw_url = diff_file_blob_raw_url(diff_file)
-- old_blob_raw_url = diff_file_old_blob_raw_url(diff_file)
- click_to_comment = local_assigns.fetch(:click_to_comment, true)
- diff_view_data = local_assigns.fetch(:diff_view_data, '')
- class_name = ''
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index aa1112c3313..229a4574eeb 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -1,5 +1,5 @@
-- sum_added_lines = diff_files.sum(&:added_lines)
-- sum_removed_lines = diff_files.sum(&:removed_lines)
+- sum_added_lines = diff_files.sum(&:added_lines) # rubocop: disable CodeReuse/ActiveRecord
+- sum_removed_lines = diff_files.sum(&:removed_lines) # rubocop: disable CodeReuse/ActiveRecord
.commit-stat-summary.dropdown
Showing
%button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown", display: "static" } }<
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index e37a444c1c9..07fc9e1c682 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -36,23 +36,18 @@
= render_if_exists 'projects/classification_policy_settings', f: f
- - unless @project.empty_repo?
- .form-group
- = f.label :default_branch, "Default Branch", class: 'label-bold'
- = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
-
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project
.form-group
= f.label :tag_list, "Tags", class: 'label-bold'
- = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control"
+ = f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control"
%p.form-text.text-muted Separate tags with commas.
%fieldset.features
%h5.prepend-top-0= _("Project avatar")
.form-group
- if @project.avatar?
.avatar-container.s160.append-bottom-15
- = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160', width: 160, height: 160)
+ = project_icon(@project, alt: '', class: 'avatar project-avatar s160', width: 160, height: 160)
- if @project.avatar_in_git
%p.light
= _("Project avatar in repository: %{link}").html_safe % { link: @project.avatar_in_git }
@@ -64,7 +59,7 @@
- if @project.avatar?
%hr
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted"
- = f.submit 'Save changes', class: "btn btn-success js-btn-save-general-project-settings"
+ = f.submit 'Save changes', class: "btn btn-success js-btn-success-general-project-settings"
%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded) }
.settings-header
@@ -80,7 +75,7 @@
-# haml-lint:disable InlineJavaScript
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project)
.js-project-permissions-form
- = f.submit 'Save changes', class: "btn btn-save"
+ = f.submit 'Save changes', class: "btn btn-success"
= render_if_exists 'projects/issues_settings'
@@ -98,10 +93,22 @@
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
= render 'projects/merge_request_settings', form: f
- = f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes"
+ = f.submit 'Save changes', class: "btn btn-success qa-save-merge-request-changes"
= render_if_exists 'projects/service_desk_settings'
+ %section.settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = s_('ProjectSettings|Badges')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = s_('ProjectSettings|Customize your project badges.')
+ = link_to s_('ProjectSettings|Learn more about badges.'), help_page_path('user/project/badges')
+ .settings-content
+ = render 'shared/badges/badge_settings'
+
= render 'export', project: @project
%section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) }
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index d47dc3d8143..75f35360e5e 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -9,7 +9,7 @@
.project-empty-note-panel
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
.prepend-top-20
- %h4
+ %h4.append-bottom-20
= _('The repository for this project is empty')
- if @project.can_current_user_push_code?
@@ -32,9 +32,13 @@
= _('Otherwise it is recommended you start with one of the options below.')
.prepend-top-20
-%nav.project-stats{ class: container_class }
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
+%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
+ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ .nav-links.scrolling-tabs
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
- if can?(current_user, :push_code, @project)
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
@@ -42,7 +46,7 @@
.empty_wrapper
%h3#repo-command-line-instructions.page-title-empty
Command line instructions
- .git-empty
+ .git-empty.js-git-empty
%fieldset
%h5 Git global setup
%pre.bg-light
@@ -54,7 +58,7 @@
%h5 Create a new repository
%pre.bg-light
:preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
cd #{h @project.path}
touch README.md
git add README.md
@@ -69,7 +73,7 @@
:preserve
cd existing_folder
git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
git add .
git commit -m "Initial commit"
- if @project.can_current_user_push_to_default_branch?
@@ -82,7 +86,7 @@
:preserve
cd existing_repo
git remote rename origin old-origin
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- if @project.can_current_user_push_to_default_branch?
%span><
git push -u origin --all
diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml
index 0586dbdf0e2..f942b936037 100644
--- a/app/views/projects/environments/_form.html.haml
+++ b/app/views/projects/environments/_form.html.haml
@@ -18,5 +18,5 @@
= f.url_field :external_url, class: 'form-control'
.form-actions
- = f.submit 'Save', class: 'btn btn-save'
+ = f.submit 'Save', class: 'btn btn-success'
= link_to 'Cancel', project_environments_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/environments/empty.html.haml b/app/views/projects/environments/empty.html.haml
index 1413930ebdb..129dbbf4e56 100644
--- a/app/views/projects/environments/empty.html.haml
+++ b/app/views/projects/environments/empty.html.haml
@@ -1,14 +1,14 @@
- page_title _("Metrics")
-.row
+.row.empty-state
.col-sm-12
.svg-content
= image_tag 'illustrations/operations_metrics_empty.svg'
-.row.empty-environments
- .col-sm-12.text-center
- %h4
- = s_('Metrics|No deployed environments')
- .state-description
- = s_('Metrics|Check out the CI/CD documentation on deploying to an environment')
- .prepend-top-10
- = link_to s_("Metrics|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success'
+ .col-12
+ .text-content
+ %h4.text-center
+ = s_('Metrics|No deployed environments')
+ %p.state-description
+ = s_('Metrics|Check out the CI/CD documentation on deploying to an environment')
+ .text-center
+ = link_to s_("Metrics|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success'
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index c7890b37381..8c5b6e089ea 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -49,15 +49,16 @@
.environments-container
- if @deployments.blank?
- .blank-state-row
- .blank-state-center
- %h2.blank-state-title
+ .empty-state
+ .text-content
+ %h4.state-title
You don't have any deployments right now.
%p.blank-state-text
Define environments in the deploy stage(s) in
%code .gitlab-ci.yml
to track deployments here.
- = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
+ .text-center
+ = link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index 5b680189bc8..e40d631a1a1 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -2,7 +2,7 @@
- page_title "Terminal for environment", @environment.name
- content_for :page_specific_javascripts do
- = stylesheet_link_tag "xterm/xterm"
+ = stylesheet_link_tag "xterm.css"
%div{ class: container_class }
.top-area
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index a966bfb2dd9..996c7b1b960 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -13,6 +13,6 @@
.tree-content-holder
.table-holder
- %table.table.files-slider{ class: "table_#{@hex_path} tree-table table-striped" }
+ %table.table.files-slider{ class: "table_#{@hex_path} tree-table" }
%tbody
= spinner nil, true
diff --git a/app/views/projects/forks/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml
index 12cf40bb65f..a69146513d8 100644
--- a/app/views/projects/forks/_fork_button.html.haml
+++ b/app/views/projects/forks/_fork_button.html.haml
@@ -5,7 +5,7 @@
.bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked
= link_to project_path(forked_project) do
- if /no_((\w*)_)*avatar/.match(avatar)
- = project_icon(namespace, class: "avatar s100 identicon")
+ = group_icon(namespace, class: "avatar s100 identicon")
- else
.avatar-container.s100
= image_tag(avatar, class: "avatar s100")
@@ -18,7 +18,7 @@
class: ("disabled has-tooltip" unless can_create_project),
title: (_('You have reached your project limit') unless can_create_project) do
- if /no_((\w*)_)*avatar/.match(avatar)
- = project_icon(namespace, class: "avatar s100 identicon")
+ = group_icon(namespace, class: "avatar s100 identicon")
- else
.avatar-container.s100
= image_tag(avatar, class: "avatar s100")
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 57afc7ac9c3..b44ea89510b 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -30,11 +30,11 @@
- if current_user && can?(current_user, :fork_project, @project)
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
- = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-success' do
= sprite_icon('fork', size: 12)
%span Fork
- else
- = link_to new_project_fork_path(@project), title: "Fork project", class: 'btn btn-new' do
+ = link_to new_project_fork_path(@project), title: "Fork project", class: 'btn btn-success' do
= sprite_icon('fork', size: 12)
%span Fork
diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml
index 5990582fd55..0ab7863b77c 100644
--- a/app/views/projects/hooks/_index.html.haml
+++ b/app/views/projects/hooks/_index.html.haml
@@ -9,7 +9,7 @@
.col-lg-8.append-bottom-default
= form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
- = f.submit 'Add webhook', class: 'btn btn-create'
+ = f.submit 'Add webhook', class: 'btn btn-success'
%hr
%h5.prepend-top-default
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index c31aef60453..57311284e11 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -11,7 +11,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: project_hook_path(@project, @hook) do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
- = f.submit 'Save changes', class: 'btn btn-create'
+ = f.submit 'Save changes', class: 'btn btn-success'
= render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: @hook
= link_to 'Remove', project_hook_path(@project, @hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: 'Are you sure?' }
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index 8ce822c43b7..1c50cfbde85 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -16,4 +16,4 @@
= render "shared/import_form", f: f
.form-actions
- = f.submit 'Start import', class: "btn btn-create", tabindex: 4
+ = f.submit 'Start import', class: "btn btn-success", tabindex: 4
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 665968a64e1..28998acdc13 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -6,7 +6,7 @@
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%section.js-vue-notes-event
- #js-vue-notes{ data: { notes_data: notes_data(@issue),
+ #js-vue-notes{ data: { notes_data: notes_data(@issue).to_json,
noteable_data: serialize_issuable(@issue),
noteable_type: 'Issue',
target_type: 'issue',
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 8a14146cb87..31c72f2f759 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -2,9 +2,9 @@
.issue-box
- if @can_bulk_update
.issue-check.hidden
- = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
- .issue-info-container
- .issue-main-info
+ = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected-issuable"
+ .issuable-info-container
+ .issuable-main-info
.issue-title.title
%span.issue-title-text
- if issue.confidential?
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index 0dd2d2e6c5d..e4a0d4b8479 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -6,6 +6,6 @@
= link_to "New issue", new_project_issue_path(@project,
issue: { assignee_id: finder.assignee.try(:id),
milestone_id: finder.milestones.first.try(:id) }),
- class: "btn btn-new",
+ class: "btn btn-success",
title: "New issue",
id: "new_issue_link"
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index 6330245954e..6566866be82 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -1,3 +1,4 @@
+# rubocop: disable CodeReuse/ActiveRecord
xml.title "#{@project.name} issues"
xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: project_issues_url(@project), rel: "alternate", type: "text/html"
@@ -5,3 +6,4 @@ xml.id project_issues_url(@project)
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
+# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index 60fe442014f..9a081a42b6f 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -1,4 +1,5 @@
-- breadcrumb_title "Issues"
+- add_to_breadcrumbs "Issues", project_issues_path(@project)
+- breadcrumb_title "New"
- page_title "New Issue"
%h3.page-title
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 2d036bd4e3e..c39fd0063be 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -6,6 +6,7 @@
- page_card_attributes @issue.card_attributes
- can_update_issue = can?(current_user, :update_issue, @issue)
+- can_reopen_issue = can?(current_user, :reopen_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project)
@@ -40,6 +41,7 @@
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ - if can_reopen_issue
%li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
@@ -48,12 +50,12 @@
%li.divider
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
+ = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
- = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
+ = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped new-issue-link btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
.issue-details.issuable-details
diff --git a/app/views/projects/jobs/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml
deleted file mode 100644
index ea552c73c92..00000000000
--- a/app/views/projects/jobs/_empty_state.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- illustration = local_assigns.fetch(:illustration)
-- illustration_size = local_assigns.fetch(:illustration_size)
-- title = local_assigns.fetch(:title)
-- content = local_assigns.fetch(:content, nil)
-- action = local_assigns.fetch(:action, nil)
-
-.row.empty-state
- .col-12
- .svg-content{ class: illustration_size }
- = image_tag illustration
- .col-12
- .text-content
- %h4.text-center= title
- - if content
- %p= content
- - if action
- .text-center
- = action
diff --git a/app/views/projects/jobs/_empty_states.html.haml b/app/views/projects/jobs/_empty_states.html.haml
deleted file mode 100644
index e5198d047df..00000000000
--- a/app/views/projects/jobs/_empty_states.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- detailed_status = @build.detailed_status(current_user)
-- illustration = detailed_status.illustration
-
-= render 'empty_state',
- illustration: illustration[:image],
- illustration_size: illustration[:size],
- title: illustration[:title],
- content: illustration[:content],
- action: detailed_status.has_action? ? link_to(detailed_status.action_button_title, detailed_status.action_path, method: detailed_status.action_method, class: 'btn btn-primary', title: detailed_status.action_button_title) : nil
diff --git a/app/views/projects/jobs/_header.html.haml b/app/views/projects/jobs/_header.html.haml
index b83e8dddccb..e7245622b80 100644
--- a/app/views/projects/jobs/_header.html.haml
+++ b/app/views/projects/jobs/_header.html.haml
@@ -24,7 +24,7 @@
- if show_controls
.nav-controls
- if can?(current_user, :create_issue, @project) && @build.failed?
- = link_to "New issue", new_project_issue_path(@project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
+ = link_to "New issue", new_project_issue_path(@project, issue: build_failed_issue_options), class: 'btn btn-success btn-inverted'
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_project_job_path(@project, @build), class: 'btn btn-inverted-secondary', method: :post
%button.btn.btn-default.float-right.d-block.d-sm-none.d-md-none.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
deleted file mode 100644
index acc1e17b811..00000000000
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ /dev/null
@@ -1,95 +0,0 @@
-%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
- .sidebar-container
- .blocks-container
- #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
-
- - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
- .block
- .title
- Job artifacts
- - if @build.artifacts_expired?
- %p.build-detail-row
- The artifacts were removed
- #{time_ago_with_tooltip(@build.artifacts_expire_at)}
- - elsif @build.has_expiring_artifacts?
- %p.build-detail-row
- The artifacts will be removed
- #{time_ago_with_tooltip(@build.artifacts_expire_at)}
-
- - if @build.artifacts?
- .btn-group.d-flex{ role: :group }
- - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
- = link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do
- Keep
-
- = link_to download_project_job_artifacts_path(@project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
- Download
-
- - if @build.browsable_artifacts?
- = link_to browse_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default' do
- Browse
-
- - if @build.trigger_request
- .build-widget.block
- %h4.title
- Trigger
-
- - if @build.trigger_request&.trigger&.short_token
- %p
- %span.build-light-text Token:
- #{@build.trigger_request.trigger.short_token}
-
- - if @build.trigger_variables.any?
- %p
- %button.btn.group.js-reveal-variables Reveal Variables
-
- %dl.js-build-variables.trigger-build-variables.hide
- - @build.trigger_variables.each do |trigger_variable|
- %dt.js-build-variable.trigger-build-variable= trigger_variable[:key]
- %dd.js-build-value.trigger-build-value= trigger_variable[:value]
-
- %div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") }
- %p
- Commit
- = link_to @build.pipeline.short_sha, project_commit_path(@project, @build.pipeline.sha), class: 'commit-sha link-commit'
- = clipboard_button(text: @build.pipeline.short_sha, title: "Copy commit SHA to clipboard")
- - if @build.merge_request
- in
- = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'link-commit'
-
- %p.build-light-text.append-bottom-0
- #{@build.pipeline.git_commit_title}
-
- - if @build.pipeline.stages_count > 1
- .block-last.dropdown.build-dropdown
- %div
- %span{ class: "ci-status-icon-#{@build.pipeline.status}" }
- = ci_icon_for_status(@build.pipeline.status)
- Pipeline
- = link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit'
- from
- = link_to "#{@build.pipeline.ref}", project_ref_path(@project, @build.pipeline.ref), class: 'link-commit ref-name'
- %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.stage-selection More
- = icon('chevron-down')
- %ul.dropdown-menu
- - @build.pipeline.legacy_stages.each do |stage|
- %li
- %a.stage-item= stage.name
-
- .builds-container
- - HasStatus::ORDERED_STATUSES.each do |build_status|
- - builds.select{|build| build.status == build_status}.each do |build|
- .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
- - tooltip = sanitize(build.tooltip_message.dup)
- = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', title: tooltip, container: 'body' }) do
- = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
- %span{ class: "ci-status-icon-#{build.status}" }
- = ci_icon_for_status(build.status)
- %span
- - if build.name
- = build.name
- - else
- = build.id
- - if build.retried?
- = sprite_icon('retry', size:16, css_class: 'icon-retry')
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index fe1c338b634..59592abcf6a 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -8,7 +8,7 @@
.nav-controls
- if can?(current_user, :update_build, @project)
- - if @all_builds.running_or_pending.limit(1).any?
+ - if @all_builds.running_or_pending.limit(1).any? # rubocop: disable CodeReuse/ActiveRecord
= link_to 'Cancel running', cancel_all_project_jobs_path(@project),
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 078f40c4477..a5f814b722d 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -3,57 +3,12 @@
- breadcrumb_title "##{@build.id}"
- page_title "#{@build.name} (##{@build.id})", "Jobs"
+- content_for :page_specific_javascripts do
+ = stylesheet_link_tag 'page_bundles/xterm'
+
%div{ class: container_class }
.build-page.js-build-page
#js-build-header-vue
- - if @build.stuck?
- - unless @build.any_runners_online?
- .bs-callout.bs-callout-warning.js-build-stuck
- %p
- - if no_runners_for_project?(@build.project)
- This job is stuck, because the project doesn't have any runners online assigned to it.
- - elsif @build.tags.any?
- This job is stuck, because you don't have any active runners online with any of these tags assigned to them:
- - @build.tags.each do |tag|
- %span.badge.badge-primary
- = tag
- - else
- This job is stuck, because you don't have any active runners that can run this job.
-
- %br
- Go to
- = link_to project_runners_path(@build.project, anchor: 'js-runners-settings') do
- Runners page
-
- - if @build.starts_environment?
- .prepend-top-default.js-environment-container
- .environment-information
- - if @build.outdated_deployment?
- = ci_icon_for_status('success_with_warnings')
- - else
- = ci_icon_for_status(@build.status)
-
- - environment = environment_for_build(@build.project, @build)
- - if @build.success? && @build.last_deployment.present?
- - if @build.last_deployment.last?
- This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
- - else
- This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
- View the most recent deployment #{deployment_link(environment.last_deployment)}.
- - elsif @build.complete? && !@build.success?
- The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed.
- - else
- This job is creating a deployment to #{environment_link_for_build(@build.project, @build)}
- - if environment.try(:last_deployment)
- and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
-
- - if @build.erased?
- .prepend-top-default.js-build-erased
- .erased.alert.alert-warning
- - if @build.erased_by_user?
- Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- - else
- Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default
@@ -87,10 +42,8 @@
= custom_icon('scroll_down')
= render 'shared/builds/build_output'
- - else
- = render "empty_states"
- = render "sidebar", builds: @builds
+ #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
.js-build-options{ data: javascript_build_options }
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 768ce9bd103..11a05eada30 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,33 +1,26 @@
- @no_container = true
- page_title "Labels"
- can_admin_label = can?(current_user, :admin_label, @project)
-- hide_class = ''
- search = params[:search]
+- subscribed = params[:subscribed]
+- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
- if can_admin_label
- content_for(:header_content) do
.nav-controls
- = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new"
+ = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success"
-- if @labels.exists? || @prioritized_labels.exists? || search.present?
+- if labels_or_filters
#promote-label-modal
%div{ class: container_class }
- .top-area.adjust
- .nav-text
- = _('Labels can be applied to issues and merge requests.')
-
- .nav-controls
- = form_tag project_labels_path(@project), method: :get do
- .input-group
- = search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
- %span.input-group-append
- %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
- = icon("search")
+ = render 'shared/labels/nav'
.labels-container.prepend-top-10
- if can_admin_label
- if search.blank?
%p.text-muted
+ = _('Labels can be applied to issues and merge requests.')
+ %br
= _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
-# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
@@ -59,7 +52,9 @@
- else
.nothing-here-block
= _('No labels with such name or description')
-
+ - elsif subscribed.present?
+ .nothing-here-block
+ = _('You do not have any subscriptions yet')
- else
= render 'shared/empty_states/labels'
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
index 37c09f12f63..d0a7f89df31 100644
--- a/app/views/projects/mattermosts/_team_selection.html.haml
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -43,4 +43,4 @@
.clearfix
.float-right
= link_to 'Cancel', edit_project_service_path(@project, @service), class: 'btn btn-lg'
- = f.submit 'Install', class: 'btn btn-save btn-lg'
+ = f.submit 'Install', class: 'btn btn-success btn-lg'
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 5a59f956cb5..13b967beba1 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,4 +1,4 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request],
html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' },
data: { markdown_version: @merge_request.cached_markdown_version } do |f|
- = render 'shared/issuable/form', f: f, issuable: @merge_request
+ = render 'shared/issuable/form', f: f, issuable: @merge_request, presenter: @mr_presenter
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index cd3d896fff2..faa070d0389 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,10 +1,10 @@
%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
- if @can_bulk_update
.issue-check.hidden
- = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
+ = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected-issuable"
- .issue-info-container
- .issue-main-info
+ .issuable-info-container
+ .issuable-main-info
.merge-request-title.title
%span.merge-request-title-text
= link_to merge_request.title, merge_request_path(merge_request)
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index a58179091ae..1bf42ded97a 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -39,4 +39,4 @@
- if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit"
- = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request
+ = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request, can_reopen: can_update_merge_request
diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml
index e73dab8ad4a..b7498216334 100644
--- a/app/views/projects/merge_requests/_nav_btns.html.haml
+++ b/app/views/projects/merge_requests/_nav_btns.html.haml
@@ -1,5 +1,5 @@
- if @can_bulk_update
= button_tag "Edit merge requests", class: "btn append-right-10 js-bulk-update-toggle"
- if merge_project
- = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do
+ = link_to new_merge_request_path, class: "btn btn-success", title: "New merge request" do
New merge request
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index afa7eb06cb4..1fd71a38472 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -61,4 +61,4 @@
- if @merge_request.errors.any?
= form_errors(@merge_request)
- = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
+ = f.submit 'Compare branches and continue', class: "btn btn-success mr-compare-btn"
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index d5c4134dee2..464f8fa65e9 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -11,7 +11,7 @@
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
- = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits
+ = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits, presenter: @mr_presenter
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
= f.hidden_field :target_project_id
diff --git a/app/views/projects/merge_requests/creations/new.html.haml b/app/views/projects/merge_requests/creations/new.html.haml
index 3220512d60d..0f618826305 100644
--- a/app/views/projects/merge_requests/creations/new.html.haml
+++ b/app/views/projects/merge_requests/creations/new.html.haml
@@ -1,4 +1,5 @@
-- breadcrumb_title "Merge Requests"
+- add_to_breadcrumbs "Merge Requests", project_merge_requests_path(@project)
+- breadcrumb_title "New"
- page_title "New Merge Request"
- if @merge_request.can_be_created && !params[:change_branches]
diff --git a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
index dab95b97346..066c8d5dba6 100644
--- a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
+++ b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
@@ -1,3 +1,7 @@
+-#-----------------------------------------------------------------
+ WARNING: Please keep changes up-to-date with the following files:
+ - `assets/javascripts/diffs/components/commit_widget.vue`
+-#-----------------------------------------------------------------
- if @commit
.info-well.d-none.d-sm-block.prepend-top-default
.well-segment
diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml
index bf3df0abf86..9ebd91dea0b 100644
--- a/app/views/projects/merge_requests/diffs/_diffs.html.haml
+++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml
@@ -14,7 +14,7 @@
%span.ref-name= @merge_request.source_branch
and
%span.ref-name= @merge_request.target_branch
- .text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
+ .text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-success'
- else
- diff_viewable = @merge_request_diff ? @merge_request_diff.viewable? : true
- if diff_viewable
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index b23baa22d8b..ef2fa8668c0 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -60,7 +60,7 @@
%section.col-md-12
%script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
.issuable-discussion.js-vue-notes-event
- #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
+ #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json,
noteable_data: serialize_issuable(@merge_request),
noteable_type: 'MergeRequest',
target_type: 'merge_request',
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 28f0a167128..ebd3229e42b 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -20,8 +20,8 @@
.form-actions
- if @milestone.new_record?
- = f.submit 'Create milestone', class: "btn-create btn qa-milestone-create-button"
+ = f.submit 'Create milestone', class: "btn-success btn qa-milestone-create-button"
= link_to "Cancel", project_milestones_path(@project), class: "btn btn-cancel"
- else
- = f.submit 'Save changes', class: "btn-save btn"
+ = f.submit 'Save changes', class: "btn-success btn"
= link_to "Cancel", project_milestone_path(@project, @milestone), class: "btn btn-cancel"
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 26d2ea8447b..57f3c640696 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -8,7 +8,7 @@
.nav-controls
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
- = link_to new_project_milestone_path(@project), class: "btn btn-new qa-new-project-milestone", title: 'New milestone' do
+ = link_to new_project_milestone_path(@project), class: "btn btn-success qa-new-project-milestone", title: 'New milestone' do
New milestone
.milestones
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index c6764c7607a..d523df1cd90 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -32,7 +32,7 @@
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
.panel-footer
- = f.submit _('Mirror repository'), class: 'btn btn-create', name: :update_remote_mirror
+ = f.submit _('Mirror repository'), class: 'btn btn-success', name: :update_remote_mirror
.panel.panel-default
.table-responsive
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 6c363345e38..d99b809c387 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -3,7 +3,6 @@
- @hide_top_links = true
- page_title 'New Project'
- header_title "Projects", dashboard_projects_path
-- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
- active_tab = local_assigns.fetch(:active_tab, 'blank')
.project-edit-container
@@ -30,15 +29,15 @@
.col-lg-9.js-toggle-container
%ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
- %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', track_label: 'blank_project', track_event: "click_tab" }, role: 'tab' }
%span.d-none.d-sm-block Blank project
%span.d-block.d-sm-none Blank
%li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', track_label: 'create_from_template', track_event: "click_tab" }, role: 'tab' }
%span.d-none.d-sm-block Create from template
%span.d-block.d-sm-none Template
%li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' }
%span.d-none.d-sm-block Import project
%span.d-block.d-sm-none Import
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index e9008d60098..eb6838cec8d 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -38,7 +38,6 @@
- if can?(current_user, :award_emoji, note)
- if note.emoji_awardable?
- - user_authored = note.user_authored?(current_user)
.note-actions-item
= button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do
= icon('spinner spin')
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index 7e1a3b9bea6..88ab486a248 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -4,7 +4,7 @@
Pages
- if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https)
- = link_to new_project_pages_domain_path(@project), class: 'btn btn-new float-right', title: 'New Domain' do
+ = link_to new_project_pages_domain_path(@project), class: 'btn btn-success float-right', title: 'New Domain' do
New Domain
%p.light
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
index ee70de22f13..342b1482df7 100644
--- a/app/views/projects/pages_domains/edit.html.haml
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -8,4 +8,4 @@
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
- = f.submit 'Save Changes', class: "btn btn-save"
+ = f.submit 'Save Changes', class: "btn btn-success"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index 376ce3f68aa..94ad1470052 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -7,6 +7,6 @@
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
- = f.submit 'Create New Domain', class: "btn btn-save"
+ = f.submit 'Create New Domain', class: "btn btn-success"
.float-right
= link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index 9a981d53ab6..259979417e0 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -39,5 +39,5 @@
= f.check_box :active, required: false, value: @schedule.active?
= _('Active')
.footer-block.row-content-block
- = f.submit _('Save pipeline schedule'), class: 'btn btn-create', tabindex: 3
+ = f.submit _('Save pipeline schedule'), class: 'btn btn-success', tabindex: 3
= link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 3677666070e..0580c15ad15 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -11,7 +11,7 @@
- if can?(current_user, :create_pipeline_schedule, @project)
.nav-controls
- = link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-create' do
+ = link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-success' do
%span= _('New schedule')
- if @schedules.present?
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index c13e3194340..5b6823da1f6 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,5 +1,5 @@
- breadcrumb_title "Pipelines"
-- page_title = s_("Pipeline|Run Pipeline")
+- page_title s_("Pipeline|Run Pipeline")
- settings_link = link_to _('CI/CD settings'), project_settings_ci_cd_path(@project)
%h3.page-title
diff --git a/app/views/projects/project_members/_new_shared_group.html.haml b/app/views/projects/project_members/_new_project_group.html.haml
index d7227c32833..74570769117 100644
--- a/app/views/projects/project_members/_new_shared_group.html.haml
+++ b/app/views/projects/project_members/_new_project_group.html.haml
@@ -2,19 +2,19 @@
.col-sm-12
= form_tag project_group_links_path(@project), class: 'js-requires-input', method: :post do
.form-group
- = label_tag :link_group_id, "Select a group to share with", class: "label-bold"
+ = label_tag :link_group_id, _("Select a group to invite"), class: "label-bold"
= groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, class: "input-clamp", required: true)
.form-group
- = label_tag :link_group_access, "Max access level", class: "label-bold"
+ = label_tag :link_group_access, _("Max access level"), class: "label-bold"
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
= icon('chevron-down')
.form-text.text-muted.append-bottom-10
- = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+ = link_to _("Read more"), help_page_path("user/permissions"), class: "vlink"
about role permissions
.form-group
- = label_tag :expires_at, 'Access expiration date', class: 'label-bold'
+ = label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
.clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Expiration date', id: 'expires_at_groups'
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: _('Expiration date'), id: 'expires_at_groups'
%i.clear-icon.js-clear-input
- = submit_tag "Share", class: "btn btn-create"
+ = submit_tag _("Invite"), class: "btn btn-success"
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 6272687be1c..517fd249f6e 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -17,5 +17,5 @@
= label_tag :expires_at, 'Access expiration date', class: 'label-bold'
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
- = f.submit "Add to project", class: "btn btn-create"
+ = f.submit "Add to project", class: "btn btn-success"
= link_to "Import", import_project_project_members_path(@project), class: "btn btn-default", title: "Import members from another project"
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index 6a52e72bfd8..8b93e81cd31 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -11,5 +11,5 @@
.col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
.form-actions
- = button_tag 'Import project members', class: "btn btn-create"
+ = button_tag 'Import project members', class: "btn btn-success"
= link_to "Cancel", project_project_members_path(@project), class: "btn btn-cancel"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9716322f8a1..14ed3345765 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -6,9 +6,9 @@
Project members
- if can?(current_user, :admin_project_member, @project)
%p
- You can add a new member to
+ You can invite a new member to
%strong= @project.name
- or share it with another group.
+ or invite another group.
- else
%p
Members can be added by project
@@ -19,16 +19,16 @@
- if can?(current_user, :admin_project_member, @project)
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
- %a.nav-link.active{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member
+ %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Invite member
- if @project.allowed_to_share_with_group?
%li.nav-tab{ role: 'presentation' }
- %a.nav-link{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group
+ %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group
.tab-content.gitlab-tab-content
- .tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' }
- = render 'projects/project_members/new_project_member', tab_title: 'Add member'
- .tab-pane{ id: 'share-with-group-pane', role: 'tabpanel' }
- = render 'projects/project_members/new_shared_group', tab_title: 'Share with group'
+ .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
+ = render 'projects/project_members/new_project_member', tab_title: 'Invite member'
+ .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
+ = render 'projects/project_members/new_project_group', tab_title: 'Invite group'
= render 'shared/members/requests', membership_source: @project, requesters: @requesters
.clearfix
diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml
index e7636099be6..233c3adba0e 100644
--- a/app/views/projects/project_templates/_built_in_templates.html.haml
+++ b/app/views/projects/project_templates/_built_in_templates.html.haml
@@ -10,8 +10,8 @@
= template.description
.controls.d-flex.align-items-center
%label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name }
- %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name }
+ %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
%span
= _("Use template")
- %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' }
+ %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
= _("Preview")
diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
index df2dcf19ed4..c3b8f2f8964 100644
--- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
@@ -30,4 +30,4 @@
= yield :push_access_levels
.card-footer
- = f.submit 'Protect', class: 'btn-create btn', disabled: true
+ = f.submit 'Protect', class: 'btn-success btn', disabled: true
diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
index f98781b77f4..b274c73d035 100644
--- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
@@ -26,4 +26,4 @@
= yield :create_access_levels
.card-footer
- = f.submit 'Protect', class: 'btn-create btn', disabled: true
+ = f.submit 'Protect', class: 'btn-success btn', disabled: true
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 8093cc2c2d7..52c6c7ec424 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -19,5 +19,5 @@
= render 'shared/notes/hints'
.error-alert
.prepend-top-default
- = f.submit 'Save changes', class: 'btn btn-save'
+ = f.submit 'Save changes', class: 'btn btn-success'
= link_to "Cancel", project_tag_path(@project, @tag.name), class: "btn btn-default btn-cancel"
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
index 86de71c732b..a6c16c70313 100644
--- a/app/views/projects/runners/_group_runners.html.haml
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -28,7 +28,7 @@
- group_link = link_to _('Group CI/CD settings'), group_settings_ci_cd_path(@project.group)
= _('Group maintainers can register group runners in the %{link}').html_safe % { link: group_link }
- else
- = _('Ask your group maintainer to setup a group Runner.')
+ = _('Ask your group maintainer to set up a group Runner.')
- else
%h4.underlined-title
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 6ee83fae25e..548977d6a80 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -24,7 +24,7 @@
- if runner.belongs_to_one_project?
= link_to _('Remove Runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- else
- - runner_project = @project.runner_projects.find_by(runner_id: runner)
+ - runner_project = @project.runner_projects.find_by(runner_id: runner) # rubocop: disable CodeReuse/ActiveRecord
= link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- elsif runner.project_type?
= form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 314af44490e..ec503cd8bef 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -1,8 +1,34 @@
%h3
= _('Specific Runners')
-= render partial: 'ci/runner/how_to_setup_specific_runner',
- locals: { registration_token: @project.runners_token }
+.bs-callout.help-callout
+ .append-bottom-10
+ %h4= _('Set up a specific Runner automatically')
+
+ %p
+ - link_to_help_page = link_to(_('Learn more about Kubernetes'),
+ help_page_path('user/project/clusters/index'),
+ target: '_blank',
+ rel: 'noopener noreferrer')
+
+ = _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
+
+ %ol
+ %li
+ = _('Click the button below to begin the install process by navigating to the Kubernetes page')
+ %li
+ = _('Select an existing Kubernetes cluster or create a new one')
+ %li
+ = _('From the Kubernetes cluster details view, install Runner from the applications list')
+
+ = link_to _('Install Runner on Kubernetes'),
+ project_clusters_path(@project),
+ class: 'btn btn-info'
+ %hr
+ = render partial: 'ci/runner/how_to_setup_runner',
+ locals: { registration_token: @project.runners_token,
+ type: 'specific',
+ reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path }
- if @project_runners.any?
%h4.underlined-title Runners activated for this project
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 9314804c5dd..9409418bbcc 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -1,6 +1,6 @@
- run_actions_text = "Perform common operations on GitLab project: #{@project.full_name}"
-%p To setup this service:
+%p To set up this service:
%ul.list-unstyled.indent-list
%li
1.
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index f25d2ecdfb1..9a7004f89c0 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -14,7 +14,7 @@
by entering
%kbd.inline /&lt;command&gt; help
- unless @service.template?
- %p To setup this service:
+ %p To set up this service:
%ul.list-unstyled.indent-list
%li
1.
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index ab9ba5c7569..5ec5a06396e 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -5,7 +5,6 @@
%fieldset.builds-feature.js-auto-devops-settings
.form-group
- message = auto_devops_warning_message(@project)
- - ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
- if message
%p.auto-devops-warning-message.settings-message.text-center
= message.html_safe
@@ -40,10 +39,17 @@
= form.label :deploy_strategy_continuous, class: 'form-check-label' do
= s_('CICD|Continuous deployment to production')
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-deploy'), target: '_blank'
+
+ .form-check
+ = form.radio_button :deploy_strategy, 'timed_incremental', class: 'form-check-input'
+ = form.label :deploy_strategy_timed_incremental, class: 'form-check-label' do
+ = s_('CICD|Continuous deployment to production using timed incremental rollout')
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank'
+
.form-check
= form.radio_button :deploy_strategy, 'manual', class: 'form-check-input'
= form.label :deploy_strategy_manual, class: 'form-check-label' do
= s_('CICD|Automatic deployment to staging, manual deployment to production')
- = link_to icon('question-circle'), help_page_path('ci/environments.md', anchor: 'manually-deploying-to-environments'), target: '_blank'
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'incremental-rollout-to-production'), target: '_blank'
= f.submit _('Save changes'), class: "btn btn-success prepend-top-15"
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 9134257b631..41afaa9ffc0 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -3,16 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings') do |f|
= form_errors(@project)
%fieldset.builds-feature
- .form-group.append-bottom-default.js-secret-runner-token
- = f.label :runners_token, _("Runner token"), class: 'label-bold'
- .form-control.js-secret-value-placeholder
- = '*' * 20
- = f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
- %p.form-text.text-muted= _("The secure token used by the Runner to checkout the project")
- %button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
- = _('Reveal value')
-
- %hr
.form-group
%h5.prepend-top-0
= _("Git strategy for pipelines")
@@ -121,7 +111,7 @@
go test -cover (Go)
%code coverage: \d+.\d+% of statements
- = f.submit _('Save changes'), class: "btn btn-save"
+ = f.submit _('Save changes'), class: "btn btn-success"
%hr
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 16961784e00..98e2829ba43 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -12,7 +12,7 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _("Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.")
+ = _("Customize your pipeline configuration, view your pipeline status and coverage report.")
.settings-content
= render 'form'
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 98c609d7bd4..a0bcaaf3c54 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -2,6 +2,7 @@
- page_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
+= render "projects/default_branch/show"
= render "projects/mirrors/show"
-# Protected branches & tags use a lot of nested partials.
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index df8a5742450..aba289c790f 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -19,8 +19,13 @@
- if can?(current_user, :download_code, @project)
%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
- = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
+ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ .nav-links.scrolling-tabs
+ = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
+ = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
+
= repository_languages_bar(@project.repository_languages)
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 4a3aa3dc626..ea963510a68 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -8,7 +8,7 @@
= link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-inverted btn-remove", title: _('Delete Snippet') do
= _('Delete')
- if can?(current_user, :create_project_snippet, @project)
- = link_to new_project_snippet_path(@project), class: 'btn btn-grouped btn-inverted btn-create', title: _("New snippet") do
+ = link_to new_project_snippet_path(@project), class: 'btn btn-grouped btn-inverted btn-success', title: _("New snippet") do
= _('New snippet')
- if @snippet.submittable_as_spam_by?(current_user)
= link_to _('Submit as spam'), mark_as_spam_project_snippet_path(@project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: _('Submit as spam')
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 1c4c73dc776..a4974d89c1a 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -7,6 +7,6 @@
.nav-controls
- if can?(current_user, :create_project_snippet, @project)
- = link_to _("New snippet"), new_project_snippet_path(@project), class: "btn btn-new", title: _("New snippet")
+ = link_to _("New snippet"), new_project_snippet_path(@project), class: "btn btn-success", title: _("New snippet")
= render 'snippets/snippets'
diff --git a/app/views/projects/tags/_tag.atom.builder b/app/views/projects/tags/_tag.atom.builder
new file mode 100644
index 00000000000..60d4b21b9d1
--- /dev/null
+++ b/app/views/projects/tags/_tag.atom.builder
@@ -0,0 +1,19 @@
+commit = @repository.commit(tag.dereferenced_target)
+release = @releases.find { |r| r.tag == tag.name }
+tag_url = project_tag_url(@project, tag.name)
+
+if commit
+ xml.entry do
+ xml.id tag_url
+ xml.link href: tag_url
+ xml.title truncate(tag.name, length: 80)
+ xml.summary strip_gpg_signature(tag.message)
+ xml.content markdown_field(release, :description), type: 'html'
+ xml.updated release.updated_at.xmlschema if release
+ xml.media :thumbnail, width: '40', height: '40', url: image_url(avatar_icon_for_email(commit.author_email))
+ xml.author do |author|
+ xml.name commit.author_name
+ xml.email commit.author_email
+ end
+ end
+end
diff --git a/app/views/projects/tags/index.atom.builder b/app/views/projects/tags/index.atom.builder
new file mode 100644
index 00000000000..b9b58b7beaa
--- /dev/null
+++ b/app/views/projects/tags/index.atom.builder
@@ -0,0 +1,7 @@
+xml.title "#{@project.name} tags"
+xml.link href: project_tags_url(@project, @ref, rss_url_options), rel: 'self', type: 'application/atom+xml'
+xml.link href: project_tags_url(@project, @ref), rel: 'alternate', type: 'text/html'
+xml.id project_tags_url(@project, @ref)
+xml.updated @releases.first.updated_at.xmlschema if @releases.any?
+
+xml << render(partial: 'tag', collection: @tags) if @tags.any?
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index dab95ba09f2..37535370940 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,6 +1,8 @@
- @no_container = true
- @sort ||= sort_value_recently_updated
- page_title s_('TagsPage|Tags')
+= content_for :meta_tags do
+ = auto_discovery_link_tag(:atom, project_tags_url(@project, rss_url_options), title: "#{@project.name} tags")
.flex-list{ class: container_class }
.top-area.adjust
@@ -23,8 +25,10 @@
%li
= link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value)
- if can?(current_user, :push_code, @project)
- = link_to new_project_tag_path(@project), class: 'btn btn-create new-tag-btn' do
+ = link_to new_project_tag_path(@project), class: 'btn btn-success new-tag-btn' do
= s_('TagsPage|New tag')
+ = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn rss-btn has-tooltip' do
+ = icon("rss")
= render_if_exists 'projects/commits/mirror_status'
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index da822ac5675..24724394259 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -41,7 +41,7 @@
.form-text.text-muted
= s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.')
.form-actions
- = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create'
+ = button_tag s_('TagsPage|Create tag'), class: 'btn btn-success'
= link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml
index abb3e918e87..406dccb74fb 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_markdown commit.full_title, project_commit_path(@project, commit.id), class: "tree-commit-link"
+ = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), class: 'tree-commit-link'
diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml
index 1a5fc56f429..a9abfac239c 100644
--- a/app/views/projects/triggers/_form.html.haml
+++ b/app/views/projects/triggers/_form.html.haml
@@ -8,4 +8,4 @@
.form-group
= f.label :key, "Description", class: "label-bold"
= f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description"
- = f.submit btn_text, class: "btn btn-save"
+ = f.submit btn_text, class: "btn btn-success"
diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml
index e8681da6528..70f1bf8ef46 100644
--- a/app/views/projects/update.js.haml
+++ b/app/views/projects/update.js.haml
@@ -7,4 +7,4 @@
$(".project-edit-errors").html("#{escape_javascript(render('errors'))}");
$('.save-project-loader').hide();
$('.project-edit-container').show();
- $('.edit-project .js-btn-save-general-project-settings').enable();
+ $('.edit-project .js-btn-success-general-project-settings').enable();
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index de692466fe5..7d8826e540c 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,9 +1,13 @@
- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
- commit_message = commit_message % { page_title: @page.title }
+- if params[:legacy_render] || !commonmark_for_repositories_enabled?
+ - markdown_version = CacheMarkdownField::CACHE_REDCARPET_VERSION
+- else
+ - markdown_version = 0
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post,
html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' },
- data: { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION } do |f|
+ data: { markdown_version: markdown_version, uploads_path: uploads_path } do |f|
= form_errors(@page)
- if @page.persisted?
@@ -47,10 +51,10 @@
.form-actions
- if @page && @page.persisted?
- = f.submit _("Save changes"), class: 'btn-save btn'
+ = f.submit _("Save changes"), class: 'btn-success btn'
.float-right
= link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped'
- else
- = f.submit s_("Wiki|Create page"), class: 'btn-create btn'
+ = f.submit s_("Wiki|Create page"), class: 'btn-success btn'
.float-right
= link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel'
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 8d91f411f89..643b51e01d1 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,6 +1,6 @@
- if (@page && @page.persisted?)
- if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do
= s_("Wiki|New page")
= link_to project_wiki_history_path(@project, @page), class: "btn" do
= s_("Wiki|Page history")
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index 38382aae67c..dc12e368b35 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -15,4 +15,4 @@
= icon('lightbulb-o')
= s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
.form-actions
- = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-create"
+ = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-success"
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 28353927135..02c5a6ea55c 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -12,7 +12,7 @@
.blocks-container
.block.block-first
- if @sidebar_page
- = render_wiki_content(@sidebar_page)
+ = render_wiki_content(@sidebar_page, legacy_render_context(params))
- else
%ul.wiki-pages
= render @sidebar_wiki_entries, context: 'sidebar'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index d80d2957466..80aa1500d53 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -22,22 +22,14 @@
.nav-controls
- if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do
= s_("Wiki|New page")
- if @page.persisted?
= link_to project_wiki_history_path(@project, @page), class: "btn" do
= s_("Wiki|Page history")
- if can?(current_user, :admin_wiki, @project)
- %button.btn.btn-danger{ data: { toggle: 'modal',
- target: '#delete-wiki-modal',
- delete_wiki_url: project_wiki_path(@project, @page),
- page_title: @page.title.capitalize },
- id: 'delete-wiki-button',
- type: 'button' }
- = _('Delete')
+ #delete-wiki-modal-wrapper{ data: { delete_wiki_url: project_wiki_path(@project, @page), page_title: @page.title.capitalize } }
-= render 'form'
+= render 'form', uploads_path: wiki_attachment_upload_url
= render 'sidebar'
-
-#delete-wiki-modal.modal.fade
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index a08973c7f32..19b9744b508 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -26,6 +26,6 @@
.prepend-top-default.append-bottom-default
.wiki
- = render_wiki_content(@page)
+ = render_wiki_content(@page, legacy_render_context(params))
= render 'sidebar'
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 57a0b64bfd5..8b95bdf9747 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -21,7 +21,7 @@
.file-content.wiki
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- = markup(snippet.file_name, chunk[:data])
+ = markup(snippet.file_name, chunk[:data], legacy_render_context(params))
- else
.file-content.code
.nothing-here-block Empty file
diff --git a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
new file mode 100644
index 00000000000..6c4607b2f16
--- /dev/null
+++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
@@ -0,0 +1,9 @@
+- if show_auto_devops_implicitly_enabled_banner?(project)
+ .auto-devops-implicitly-enabled-banner.alert.alert-warning
+ - more_information_link = link_to _('More information'), 'https://docs.gitlab.com/ee/topics/autodevops/', class: 'alert-link'
+ - auto_devops_message = s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}") % { more_information_link: more_information_link }
+ = auto_devops_message.html_safe
+ .alert-link-group
+ = link_to _('Settings'), project_settings_ci_cd_path(project), class: 'alert-link'
+ |
+ = link_to _('Dismiss'), '#', class: 'hide-auto-devops-implicitly-enabled-banner alert-link', data: { project_id: project.id }
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 3655c2a1d42..a2df0347fd6 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,14 +1,14 @@
- project = project || @project
-.git-clone-holder.input-group
+.git-clone-holder.js-git-clone-holder.input-group
.input-group-prepend
- if allowed_protocols_present?
.input-group-text.clone-dropdown-btn.btn
- %span
+ %span.js-clone-dropdown-label
= enabled_project_button(project, enabled_protocol)
- else
%a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
- %span
+ %span.js-clone-dropdown-label
= default_clone_protocol.upcase
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 7afb7b3a93b..6612497e7e2 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -2,13 +2,13 @@
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs
- = event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
+ = event_filter_link EventFilter::ALL, _('All'), s_('EventFilterBy|Filter by all')
- if event_filter_visible(:repository)
- = event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
+ = event_filter_link EventFilter::PUSH, _('Push events'), s_('EventFilterBy|Filter by push events')
- if event_filter_visible(:merge_requests)
- = event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events')
+ = event_filter_link EventFilter::MERGED, _('Merge events'), s_('EventFilterBy|Filter by merge events')
- if event_filter_visible(:issues)
- = event_filter_link EventFilter.issue, _('Issue events'), s_('EventFilterBy|Filter by issue events')
+ = event_filter_link EventFilter::ISSUE, _('Issue events'), s_('EventFilterBy|Filter by issue events')
- if comments_visible?
- = event_filter_link EventFilter.comments, _('Comments'), s_('EventFilterBy|Filter by comments')
- = event_filter_link EventFilter.team, _('Team'), s_('EventFilterBy|Filter by team')
+ = event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments')
+ = event_filter_link EventFilter::TEAM, _('Team'), s_('EventFilterBy|Filter by team')
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 2c3cbd0b986..71f34c0d85b 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -4,8 +4,6 @@
- use_label_priority = local_assigns.fetch(:use_label_priority, false)
- force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority.present? : false)
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
-- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
-- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
- tooltip_title = label_status_tooltip(label, status) if status
%li.label-list-item{ id: label_css_id, data: { id: label.id } }
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index ac2164a4a71..28b34e38b15 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -3,7 +3,6 @@
- if stage.status
- detailed_status = stage.detailed_status(current_user)
- icon_status = "#{detailed_status.icon}_borderless"
- - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
.stage-container.dropdown{ class: klass }
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml
new file mode 100644
index 00000000000..998985cabe1
--- /dev/null
+++ b/app/views/shared/_mobile_clone_panel.html.haml
@@ -0,0 +1,13 @@
+- project = project || @project
+- ssh_copy_label = _("Copy SSH clone URL")
+- http_copy_label = _("Copy HTTPS clone URL")
+
+.btn-group.mobile-git-clone.js-mobile-git-clone
+ = clipboard_button(button_text: default_clone_label, target: '#project_clone', hide_button_icon: true, class: "input-group-text clone-dropdown-btn js-clone-dropdown-label btn btn-default")
+ %button.btn.btn-default.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } }
+ = icon("caret-down", class: "dropdown-btn-icon")
+ %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } }
+ %li
+ = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' })
+ %li
+ = dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' })
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index d38d161047b..9bc67a7c715 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,7 +1,7 @@
- if any_projects?(@projects)
.project-item-select-holder.btn-group
- %a.btn.btn-new.new-project-item-link.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
+ %a.btn.btn-success.new-project-item-link.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
= icon('spinner spin')
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled]
- %button.btn.btn-new.new-project-item-select-button.qa-new-project-item-select-button
+ %button.btn.btn-success.new-project-item-select-button.qa-new-project-item-select-button
= icon('caret-down')
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index 58d310fac16..f4df7bdcd83 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -26,4 +26,4 @@
= render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes
.prepend-top-default
- = f.submit "Create #{type} token", class: "btn btn-create"
+ = f.submit "Create #{type} token", class: "btn btn-success"
diff --git a/app/views/shared/_ping_consent.html.haml b/app/views/shared/_ping_consent.html.haml
new file mode 100644
index 00000000000..f8eb2b2833b
--- /dev/null
+++ b/app/views/shared/_ping_consent.html.haml
@@ -0,0 +1,12 @@
+- if session[:ask_for_usage_stats_consent]
+ .ping-consent-message.alert.alert-warning.flex-alert
+ - settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="alert-link">'.html_safe % { url: admin_application_settings_path(anchor: 'js-usage-settings') }
+ - info_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="alert-link">'.html_safe % { url: help_page_path('user/admin_area/settings/usage_statistics.md') }
+ .alert-message
+ = s_('To help improve GitLab, we would like to periodically collect usage information. This can be changed at any time in %{settings_link_start}Settings%{link_end}. %{info_link_start}More Information%{link_end}').html_safe % { settings_link_start: settings_link_start, info_link_start: info_link_start, link_end: '</a>'.html_safe }
+ .alert-link-group
+ - send_usage_data_path = admin_application_settings_path(application_setting: { version_check_enabled: 1, usage_ping_enabled: 1 })
+ - not_now_path = admin_application_settings_path(application_setting: { version_check_enabled: 0, usage_ping_enabled: 0 })
+ = link_to _("Send usage data"), send_usage_data_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': true, 'data-ping-enabled': true, class: 'alert-link js-usage-consent-action'
+ |
+ = link_to _('Not now'), not_now_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': false, 'data-ping-enabled': false, class: 'hide-ping-consent-message alert-link js-usage-consent-action'
diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml
index a0ba1afc284..10f358402c1 100644
--- a/app/views/shared/_recaptcha_form.html.haml
+++ b/app/views/shared/_recaptcha_form.html.haml
@@ -17,4 +17,4 @@
- if has_submit
.row-content-block.footer-block
- = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
+ = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-success'
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 4e7061eef1c..7cbc5810c10 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,5 +1,3 @@
-- show_create = local_assigns.fetch(:show_create, false)
-
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index 01ce1225b8d..ba37b37a3b1 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -2,7 +2,7 @@
.form-group.row.visibility-level-setting
- if with_label
- = f.label :visibility_level, class: 'col-form-label col-sm-2' do
+ = f.label :visibility_level, class: 'col-form-label col-sm-2 pt-0' do
Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access")
%div{ :class => (with_label ? "col-sm-10" : "col-sm-12") }
diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml
index dd6b9cce58e..9fc46afe177 100644
--- a/app/views/shared/_visibility_radios.html.haml
+++ b/app/views/shared/_visibility_radios.html.haml
@@ -3,7 +3,7 @@
- restricted = restricted_visibility_levels.include?(level)
- disabled = disallowed || restricted
.form-check{ class: [('disabled' if disabled), ('restricted' if restricted)] }
- = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input'
+ = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input', data: { track_label: "blank_project", track_event: "activate_form_input", track_property: "#{model_method}", track_value: "#{level}" }
= form.label "#{model_method}_#{level}", class: 'form-check-label' do
= visibility_level_icon(level)
.option-title
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 28e6fe1b16d..0d2f6bb77d6 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -33,7 +33,7 @@
- if @project
%board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project),
"milestone-path" => milestones_filter_dropdown_path,
- "label-path" => labels_filter_path,
+ "label-path" => labels_filter_path_with_defaults,
"empty-state-svg" => image_path('illustrations/issues.svg'),
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 532045f3697..6138914206b 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -25,7 +25,7 @@
show_no: "true",
show_any: "true",
project_id: @project&.try(:id),
- labels: labels_filter_path(false),
+ labels: labels_filter_path_with_defaults,
namespace_path: @namespace_path,
project_path: @project.try(:path) } }
%span.dropdown-toggle-text
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index e8749ee3956..b629ceafeb3 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -7,5 +7,5 @@
%h4= _("Labels can be applied to issues and merge requests to categorize them.")
%p= _("You can also star a label to make it a priority label.")
- if can?(current_user, :admin_label, @project)
- = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-new', title: _('New label'), id: 'new_label_link'
+ = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link'
= link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link'
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 186139f3526..421a1b2415b 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -17,7 +17,7 @@
- if project_select_button
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests, with_feature_enabled: 'merge_requests'
- else
- = link_to _('New merge request'), button_path, class: 'btn btn-new', title: _('New merge request'), id: 'new_merge_request_link'
+ = link_to _('New merge request'), button_path, class: 'btn btn-success', title: _('New merge request'), id: 'new_merge_request_link'
- else
%h4.text-center
= _("There are no merge requests to show")
diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml
index f1a41074c28..5351c9ce6a4 100644
--- a/app/views/shared/empty_states/_wikis.html.haml
+++ b/app/views/shared/empty_states/_wikis.html.haml
@@ -2,7 +2,7 @@
- if can?(current_user, :create_wiki, @project)
- create_path = project_wiki_path(@project, params[:id], { view: 'create' })
- - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-new', title: s_('WikiEmpty|Create your first page')
+ - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success', title: s_('WikiEmpty|Create your first page')
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
%h4
@@ -13,7 +13,7 @@
- elsif can?(current_user, :read_issue, @project)
- issues_link = link_to s_('WikiEmptyIssueMessage|issue tracker'), project_issues_path(@project)
- - new_issue_link = link_to s_('WikiEmpty|Suggest wiki improvement'), new_project_issue_path(@project), class: 'btn btn-new', title: s_('WikiEmptyIssueMessage|Suggest wiki improvement')
+ - new_issue_link = link_to s_('WikiEmpty|Suggest wiki improvement'), new_project_issue_path(@project), class: 'btn btn-success', title: s_('WikiEmptyIssueMessage|Suggest wiki improvement')
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do
%h4
diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml
index 13bb4baee3f..f6b3a49eacb 100644
--- a/app/views/shared/groups/_empty_state.html.haml
+++ b/app/views/shared/groups/_empty_state.html.haml
@@ -1,7 +1,8 @@
-.groups-empty-state
- = custom_icon("icon_empty_groups")
+.group-empty-state.row.align-items-center.justify-content-center
+ .icon.text-center.order-md-2
+ = custom_icon("icon_empty_groups")
- .text-content
+ .text-content.m-0.order-md-1
%h4= s_("GroupsEmptyState|A group is a collection of several projects.")
%p= s_("GroupsEmptyState|If you organize your projects under a group, it works like a folder.")
%p= s_("GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group.")
diff --git a/app/views/shared/groups/_search_form.html.haml b/app/views/shared/groups/_search_form.html.haml
index 3f91263089a..49b812baefc 100644
--- a/app/views/shared/groups/_search_form.html.haml
+++ b/app/views/shared/groups/_search_form.html.haml
@@ -1,2 +1,2 @@
-= form_tag request.path, method: :get, class: 'group-filter-form append-right-10', id: 'group-filter-form' do |f|
- = search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Filter by name...'), class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
+= form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f|
+ = search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter qa-groups-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
diff --git a/app/views/shared/icons/_icon_status_scheduled.svg b/app/views/shared/icons/_icon_status_scheduled.svg
new file mode 100644
index 00000000000..ca6e4efce50
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_scheduled.svg
@@ -0,0 +1 @@
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><circle cx="7" cy="7" r="7"/><circle fill="#FFF" cx="7" cy="7" r="6"/><g transform="translate(2.75 2.75)" fill-rule="nonzero"><path d="M4.165 7.81a3.644 3.644 0 1 1 0-7.29 3.644 3.644 0 0 1 0 7.29zm0-1.042a2.603 2.603 0 1 0 0-5.206 2.603 2.603 0 0 0 0 5.206z"/><rect x="3.644" y="2.083" width="1.041" height="2.603" rx=".488"/><rect x="3.644" y="3.644" width="2.083" height="1.041" rx=".488"/></g></svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_icon_status_scheduled_borderless.svg b/app/views/shared/icons/_icon_status_scheduled_borderless.svg
new file mode 100644
index 00000000000..dc38c01d898
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_scheduled_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M6.16 11.55a5.39 5.39 0 1 1 0-10.78 5.39 5.39 0 0 1 0 10.78zm0-1.54a3.85 3.85 0 1 0 0-7.7 3.85 3.85 0 0 0 0 7.7z"/><rect x="5.39" y="3.08" width="1.54" height="3.85" rx=".767"/><rect x="5.39" y="5.39" width="3.08" height="1.54" rx=".767"/></svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
index fc86f855865..ef3d44a9241 100644
--- a/app/views/shared/issuable/_assignees.html.haml
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -3,7 +3,7 @@
- render_count = assignees_rendering_overflow ? max_render - 1 : max_render
- more_assignees_count = issue.assignees.size - render_count
-- issue.assignees.take(render_count).each do |assignee|
+- issue.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(@project, assignee, name: false, title: "Assigned to :name")
- if more_assignees_count.positive?
diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
index 23b2e1b91e5..4597d9439fa 100644
--- a/app/views/shared/issuable/_board_create_list_dropdown.html.haml
+++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
@@ -1,5 +1,5 @@
.dropdown.prepend-left-10#js-add-list
- %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
+ %button.btn.btn-success.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index 933d4b2ea65..4f6a71b6071 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -1,14 +1,18 @@
- is_current_user = issuable_author_is_current_user(issuable)
- display_issuable_type = issuable_display_type(issuable)
- button_method = issuable_close_reopen_button_method(issuable)
+- are_close_and_open_buttons_hidden = issuable_button_hidden?(issuable, true) && issuable_button_hidden?(issuable, false)
-- if can_update && is_current_user
- = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
- class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
- = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
- class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
-- elsif can_update && !is_current_user
- = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
+- if is_current_user
+ - if can_update
+ = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
+ - if can_reopen
+ = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- else
- = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
- class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse'
+ - if can_update && !are_close_and_open_buttons_hidden
+ = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
+ - else
+ = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
+ class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse'
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 1cd8ce0826c..c7037335866 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,5 +1,3 @@
-- boards_page = controller.controller_name == 'boards'
-
.issues-filters
.issues-details-filters.row-content-block.second-block
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index b49e47a7266..b33c758b464 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,6 +1,7 @@
- form = local_assigns.fetch(:f)
- commits = local_assigns[:commits]
- project = @target_project || @project
+- presenter = local_assigns.fetch(:presenter, nil)
= form_errors(issuable)
@@ -29,7 +30,7 @@
= render 'shared/issuable/form/metadata', issuable: issuable, form: form
-= render_if_exists 'shared/issuable/approvals', issuable: issuable, form: form
+= render_if_exists 'shared/issuable/approvals', issuable: issuable, presenter: presenter, form: form
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
@@ -69,9 +70,9 @@
%span.append-right-10
- if issuable.new_record?
- = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create qa-issuable-create-button'
+ = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-success qa-issuable-create-button'
- else
- = form.submit 'Save changes', class: 'btn btn-save'
+ = form.submit 'Save changes', class: 'btn btn-success'
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = issuable.project.present.contribution_guide_path)
.inline.prepend-top-10
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 34911fd2712..6eb1f8f0853 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -7,9 +7,8 @@
- data_options = local_assigns.fetch(:data_options, {})
- classes = local_assigns.fetch(:classes, [])
- selected = local_assigns.fetch(:selected, nil)
-- selected_toggle = local_assigns.fetch(:selected_toggle, nil)
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
-- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
+- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path_with_defaults, default_label: "Labels"}
- dropdown_data.merge!(data_options)
- label_name = local_assigns.fetch(:label_name, "Labels")
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 9ce7f6fe269..c4d177361e7 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -33,16 +33,17 @@
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
- %button.btn.btn-link
- = icon('search')
+ %button.btn.btn-link{ type: 'button' }
+ = sprite_icon('search')
%span
Press Enter or click to search
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
- %button.btn.btn-link
+ %button.btn.btn-link{ type: 'button' }
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
- %i.fa{ class: "#{'{{icon}}'}" }
+ %svg
+ %use{ 'xlink:href': "#{'{{icon}}'}" }
%span.js-filter-hint
{{hint}}
%span.js-filter-tag.dropdown-light-content
@@ -59,7 +60,7 @@
#js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
- %button.btn.btn-link
+ %button.btn.btn-link{ type: 'button' }
No Assignee
%li.divider.droplab-item-ignore
- if current_user
@@ -72,38 +73,46 @@
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
- %button.btn.btn-link
+ %button.btn.btn-link{ type: 'button' }
No Milestone
%li.filter-dropdown-item{ data: { value: 'upcoming' } }
- %button.btn.btn-link
+ %button.btn.btn-link{ type: 'button' }
Upcoming
%li.filter-dropdown-item{ 'data-value' => 'started' }
- %button.btn.btn-link
+ %button.btn.btn-link{ type: 'button' }
Started
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
- %button.btn.btn-link.js-data-value
+ %button.btn.btn-link.js-data-value{ type: 'button' }
{{title}}
#js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
- %button.btn.btn-link
+ %button.btn.btn-link{ type: 'button' }
No Label
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
- %button.btn.btn-link
+ %button.btn.btn-link{ type: 'button' }
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
#js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
- %button.btn.btn-link
+ %button.btn.btn-link{ type: 'button' }
%gl-emoji
%span.js-data-value.prepend-left-10
{{name}}
+ #js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul.filter-dropdown{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Yes')
+ %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('No')
= render_if_exists 'shared/issuable/filter_weight', type: type
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 0ca35ea1298..aa136af1955 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -109,7 +109,7 @@
- selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project), display: 'static' } }
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path_with_defaults if @project), display: 'static' } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
@@ -159,7 +159,7 @@
= dropdown_content
= dropdown_loading
= dropdown_footer add_content_class: true do
- %button.btn.btn-new.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true }
+ %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true }
= _('Move')
= icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index 3b017c62a80..ac8d58c0bfe 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -3,7 +3,6 @@
- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
- has_due_date = issuable.has_attribute?(:due_date)
-- has_labels = @labels && @labels.any?
- form = local_assigns.fetch(:form)
%hr
@@ -35,4 +34,4 @@
= form.label :due_date, "Due date", class: "col-form-label col-md-2 col-lg-4"
.col-8
.issuable-form-select-holder
- = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
+ = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date", autocomplete: 'off'
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index e49bdec386a..56c4b021eab 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -9,7 +9,7 @@
autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title')
- if issuable.respond_to?(:work_in_progress?)
- %p.form-text.text-muted
+ .form-text.text-muted
.js-wip-explanation
%a.js-toggle-wip{ href: '', tabindex: -1 }
Remove the
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index 2bf5efae1e6..335c34a4632 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -28,7 +28,7 @@
.form-actions
- if @label.persisted?
- = f.submit 'Save changes', class: 'btn btn-save js-save-button'
+ = f.submit 'Save changes', class: 'btn btn-success js-save-button'
- else
- = f.submit 'Create label', class: 'btn btn-create js-save-button'
+ = f.submit 'Create label', class: 'btn btn-success js-save-button'
= link_to 'Cancel', back_path, class: 'btn btn-cancel'
diff --git a/app/views/shared/labels/_nav.html.haml b/app/views/shared/labels/_nav.html.haml
new file mode 100644
index 00000000000..98572db738b
--- /dev/null
+++ b/app/views/shared/labels/_nav.html.haml
@@ -0,0 +1,20 @@
+- subscribed = params[:subscribed]
+
+.top-area.adjust
+ %ul.nav-links.nav.nav-tabs
+ %li{ class: active_when(subscribed != 'true') }>
+ = link_to labels_filter_path do
+ = _('All')
+ - if current_user
+ %li{ class: active_when(subscribed == 'true') }>
+ = link_to labels_filter_path(subscribed: 'true') do
+ = _('Subscribed')
+ .nav-controls
+ = form_tag labels_filter_path, method: :get do
+ = hidden_field_tag :subscribed, params[:subscribed]
+ .input-group
+ = search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
+ %span.input-group-append
+ %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
+ = icon("search")
+ = render 'shared/labels/sort_dropdown'
diff --git a/app/views/shared/labels/_sort_dropdown.html.haml b/app/views/shared/labels/_sort_dropdown.html.haml
new file mode 100644
index 00000000000..8a7d037e15b
--- /dev/null
+++ b/app/views/shared/labels/_sort_dropdown.html.haml
@@ -0,0 +1,9 @@
+- sort_title = label_sort_options_hash[@sort] || sort_title_name_desc
+.dropdown.inline
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ = sort_title
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
+ %li
+ - label_sort_options_hash.each do |value, title|
+ = sortable_item(title, page_filter_path(sort: value, label: true, subscribed: params[:subscribed]), sort_title)
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
index 40224cec9e8..ebae58f28ba 100644
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -1,13 +1,13 @@
- model_name = source.model_name.to_s.downcase
-- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
+- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) # rubocop: disable CodeReuse/ActiveRecord
.project-action-button.inline
- link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
= link_to link_text, polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(source) },
class: 'btn'
-- elsif requester = source.requesters.find_by(user_id: current_user.id)
+- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
.project-action-button.inline
= link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
method: :delete,
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index af29c0fe59e..2682d92fc56 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -22,7 +22,7 @@
%strong Blocked
- if user.two_factor_enabled?
- %label.label.label-info
+ %label.badge.badge-info
2FA
- if source.instance_of?(Group) && source != @group
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index ed336df4e9d..0674c822d63 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -1,7 +1,7 @@
- noteable_name = @note.noteable.human_class_name
.float-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
- %input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
+ %input.btn.btn-nr.btn-success.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
- if @note.can_be_discussion_note?
= button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => 'Open comment type dropdown' do
diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml
index 71a5b94e958..fec966069b9 100644
--- a/app/views/shared/notes/_edit_form.html.haml
+++ b/app/views/shared/notes/_edit_form.html.haml
@@ -9,6 +9,6 @@
.note-form-actions.clearfix
.settings-message.note-edit-warning.js-finish-edit-warning
Finish editing this message first!
- = submit_tag 'Save comment', class: 'btn btn-nr btn-save js-comment-save-button'
+ = submit_tag 'Save comment', class: 'btn btn-nr btn-success js-comment-save-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
Cancel
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index be053d481e4..aba790e1217 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -1,9 +1,7 @@
- avatar = true unless local_assigns[:avatar] == false
- stars = true unless local_assigns[:stars] == false
- forks = false unless local_assigns[:forks] == true
-- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
-- user = local_assigns[:user]
- access = max_project_member_access(project)
- css_class = '' unless local_assigns[:css_class]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml
index fa93307be31..daf08d9bb2c 100644
--- a/app/views/shared/runners/_form.html.haml
+++ b/app/views/shared/runners/_form.html.haml
@@ -51,6 +51,6 @@
= _('Tags')
.col-sm-10
= f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control'
- .form-text.text-muted= _('You can setup jobs to only use Runners with specific tags. Separate tags with commas.')
+ .form-text.text-muted= _('You can set up jobs to only use Runners with specific tags. Separate tags with commas.')
.form-actions
= f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml
index da5c032add5..5935750ca06 100644
--- a/app/views/shared/runners/_runner_description.html.haml
+++ b/app/views/shared/runners/_runner_description.html.haml
@@ -1,6 +1,6 @@
.light.prepend-top-default
%p
- = _("A 'Runner' is a process which runs a job. You can setup as many Runners as you need.")
+ = _("A 'Runner' is a process which runs a job. You can set up as many Runners as you need.")
%br
= _('Runners can be placed on separate users, servers, and even on your local machine.')
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 5e5c050d5c3..1b66d3acd40 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -32,9 +32,9 @@
.form-actions
- if @snippet.new_record?
- = f.submit 'Create snippet', class: "btn-create btn"
+ = f.submit 'Create snippet', class: "btn-success btn"
- else
- = f.submit 'Save changes', class: "btn-save btn"
+ = f.submit 'Save changes', class: "btn-success btn"
- if @snippet.project_id
= link_to "Cancel", project_snippets_path(@project), class: "btn btn-cancel"
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 07ebb8680d2..9c5b9593bba 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -17,6 +17,7 @@
%strong Push events
%p.light.ml-1
This URL will be triggered by a push to the repository
+ = form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)'
%li
= form.check_box :tag_push_events, class: 'form-check-input'
= form.label :tag_push_events, class: 'list-label form-check-label ml-1' do
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index ae69d0d07c7..0ce13ee7a53 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -7,7 +7,7 @@
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
Delete
- = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
+ = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-success", title: "New snippet" do
New snippet
- if @snippet.submittable_as_spam_by?(current_user)
= link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml
index f01915107e3..c8a5e199674 100644
--- a/app/views/snippets/new.html.haml
+++ b/app/views/snippets/new.html.haml
@@ -1,5 +1,6 @@
- @hide_top_links = true
-- breadcrumb_title "Snippets"
+- add_to_breadcrumbs "Snippets", dashboard_snippets_path
+- breadcrumb_title "New"
- page_title "New Snippet"
%h3.page-title
New Snippet
diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml
index e1f7ee80ebb..220ba2b49e6 100644
--- a/app/views/snippets/notes/_actions.html.haml
+++ b/app/views/snippets/notes/_actions.html.haml
@@ -1,6 +1,5 @@
- if current_user
- if note.emoji_awardable?
- - user_authored = note.user_authored?(current_user)
.note-actions-item
= link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip", data: { position: 'right' } do
= icon('spinner spin')
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index cc0e93c0755..39d4d82a77d 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -8,13 +8,13 @@
- if current_user.two_factor_otp_enabled?
.row.append-bottom-10
.col-md-4
- %button#js-setup-u2f-device.btn.btn-info.btn-block Setup new U2F device
+ %button#js-setup-u2f-device.btn.btn-info.btn-block Set up new U2F device
.col-md-8
%p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
- else
.row.append-bottom-10
.col-md-4
- %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true } Setup new U2F device
+ %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true } Set up new U2F device
.col-md-8
%p.text-warning You need to register a two-factor authentication app before you can set up a U2F device.
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
new file mode 100644
index 00000000000..f8b3754840d
--- /dev/null
+++ b/app/views/users/_overview.html.haml
@@ -0,0 +1,32 @@
+.row
+ .col-md-12.col-lg-6
+ .calendar-block
+ .content-block.hide-bottom-border
+ %h4
+ = s_('UserProfile|Activity')
+ .user-calendar.d-none.d-sm-block.text-left{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
+ %h4.center.light
+ %i.fa.fa-spinner.fa-spin
+ .user-calendar-activities.d-none.d-sm-block
+
+ - if can?(current_user, :read_cross_project)
+ .activities-block
+ .content-block
+ %h5.prepend-top-10
+ = s_('UserProfile|Recent contributions')
+ .overview-content-list{ data: { href: user_path } }
+ .center.light.loading
+ %i.fa.fa-spinner.fa-spin
+ .prepend-top-10
+ = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
+
+ .col-md-12.col-lg-6
+ .projects-block
+ .content-block
+ %h4
+ = s_('UserProfile|Personal projects')
+ .overview-content-list{ data: { href: user_projects_path } }
+ .center.light.loading
+ %i.fa.fa-spinner.fa-spin
+ .prepend-top-10
+ = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 6b1d75c6e72..938cb579e9f 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -1,6 +1,5 @@
%h4.prepend-top-20
- Contributions for
- %strong= @calendar_date.to_s(:medium)
+ = _("Contributions for <strong>%{calendar_date}</strong>").html_safe % { calendar_date: @calendar_date.to_s(:medium) }
- if @events.any?
%ul.bordered-list
@@ -9,25 +8,28 @@
%span.light
%i.fa.fa-clock-o
= event.created_at.strftime('%-I:%M%P')
- - if event.push?
- #{event.action_name} #{event.ref_type}
+ - if event.visible_to_user?(current_user)
+ - if event.push?
+ #{event.action_name} #{event.ref_type}
+ %strong
+ - commits_path = project_commits_path(event.project, event.ref_name)
+ = link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path
+ - else
+ = event_action_name(event)
+ %strong
+ - if event.note?
+ = link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title
+ - elsif event.target
+ = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
+
+ at
%strong
- - commits_path = project_commits_path(event.project, event.ref_name)
- = link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path
+ - if event.project
+ = link_to_project(event.project)
+ - else
+ = event.project_name
- else
- = event_action_name(event)
- %strong
- - if event.note?
- = link_to event.note_target.to_reference, event_note_target_path(event), class: 'has-tooltip', title: event.target_title
- - elsif event.target
- = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
-
- at
- %strong
- - if event.project
- = link_to_project event.project
- - else
- = event.project_name
+ made a private contribution
- else
%p
- No contributions found for #{@calendar_date.to_s(:medium)}
+ = _('No contributions were found')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 7a38d290915..d6c8420b744 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -12,22 +12,22 @@
.cover-block.user-cover-block.top-area
.cover-controls
- if @user == current_user
- = link_to profile_path, class: 'btn btn-default has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
+ = link_to profile_path, class: 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do
= icon('pencil')
- elsif current_user
- if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
+ %button.btn.btn-danger{ title: s_('UserProfile|Already reported for abuse'),
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
= icon('exclamation-circle')
- else
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn',
- title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- if can?(current_user, :read_user_profile, @user)
- = link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
+ = link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
= icon('rss')
- if current_user && current_user.admin?
- = link_to [:admin, @user], class: 'btn btn-default', title: 'View user in admin area',
+ = link_to [:admin, @user], class: 'btn btn-default', title: s_('UserProfile|View user in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users')
@@ -51,7 +51,7 @@
@#{@user.username}
- if can?(current_user, :read_user_profile, @user)
%span.middle-dot-divider
- Member since #{@user.created_at.to_date.to_s(:long)}
+ = s_('Member since %{date}') % { date: @user.created_at.to_date.to_s(:long) }
.cover-desc
- unless @user.public_email.blank?
@@ -91,32 +91,40 @@
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs
+ - if profile_tab?(:overview)
+ %li.js-overview-tab
+ = link_to user_path, data: { target: 'div#js-overview', action: 'overview', toggle: 'tab' } do
+ = s_('UserProfile|Overview')
- if profile_tab?(:activity)
%li.js-activity-tab
- = link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
- Activity
+ = link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
+ = s_('UserProfile|Activity')
- if profile_tab?(:groups)
%li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
- Groups
+ = s_('UserProfile|Groups')
- if profile_tab?(:contributed)
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
- Contributed projects
+ = s_('UserProfile|Contributed projects')
- if profile_tab?(:projects)
%li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
- Personal projects
+ = s_('UserProfile|Personal projects')
- if profile_tab?(:snippets)
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
- Snippets
+ = s_('UserProfile|Snippets')
%div{ class: container_class }
.tab-content
+ - if profile_tab?(:overview)
+ #js-overview.tab-pane
+ = render "users/overview"
+
- if profile_tab?(:activity)
#activity.tab-pane
- .row-content-block.calender-block.white.second-block.d-none.d-sm-block
+ .row-content-block.calendar-block.white.second-block.d-none.d-sm-block
.user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
%h4.center.light
%i.fa.fa-spinner.fa-spin
@@ -124,7 +132,7 @@
- if can?(current_user, :read_cross_project)
%h4.prepend-top-20
- Most Recent Activity
+ = s_('UserProfile|Most Recent Activity')
.content_list{ data: { href: user_path } }
= spinner
@@ -155,4 +163,4 @@
.col-12.text-center
.text-content
%h4
- This user has a private profile
+ = s_('UserProfile|This user has a private profile')
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index 06324575ffc..f69e74b2674 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -10,10 +10,12 @@ class AdminEmailWorker
private
+ # rubocop: disable CodeReuse/ActiveRecord
def send_repository_check_mail
repository_check_failed_count = Project.where(last_repository_check_failed: true).count
return if repository_check_failed_count.zero?
RepositoryCheckMailer.notify(repository_check_failed_count).deliver_now
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f95df7ecf03..f21789de37d 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1,4 +1,6 @@
---
+- auto_devops:auto_devops_disable
+
- cronjob:admin_email
- cronjob:expire_build_artifacts
- cronjob:gitlab_usage_ping
@@ -68,6 +70,7 @@
- pipeline_processing:pipeline_update
- pipeline_processing:stage_update
- pipeline_processing:update_head_pipeline_for_merge_request
+- pipeline_processing:ci_build_schedule
- repository_check:repository_check_clear
- repository_check:repository_check_batch
@@ -85,6 +88,7 @@
- authorized_projects
- background_migration
- create_gpg_signature
+- delete_container_repository
- delete_merged_branches
- delete_user
- email_receiver
diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb
index c6f89a17729..c1283e9b2fc 100644
--- a/app/workers/archive_trace_worker.rb
+++ b/app/workers/archive_trace_worker.rb
@@ -4,9 +4,11 @@ class ArchiveTraceWorker
include ApplicationWorker
include PipelineBackgroundQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(job_id)
Ci::Build.without_archived_trace.find_by(id: job_id).try do |job|
job.trace.archive!
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index dd62bb0f33d..c9ddeb08613 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -12,9 +12,11 @@ class AuthorizedProjectsWorker
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(user_id)
user = User.find_by(id: user_id)
user&.refresh_authorized_projects
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/auto_devops/disable_worker.rb b/app/workers/auto_devops/disable_worker.rb
new file mode 100644
index 00000000000..73ddc591505
--- /dev/null
+++ b/app/workers/auto_devops/disable_worker.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module AutoDevops
+ class DisableWorker
+ include ApplicationWorker
+ include AutoDevopsQueue
+
+ def perform(pipeline_id)
+ pipeline = Ci::Pipeline.find(pipeline_id)
+ project = pipeline.project
+
+ send_notification_email(pipeline, project) if disable_service(project).execute
+ end
+
+ private
+
+ def disable_service(project)
+ Projects::AutoDevops::DisableService.new(project)
+ end
+
+ def send_notification_email(pipeline, project)
+ recipients = email_receivers_for(pipeline, project)
+
+ return unless recipients.any?
+
+ NotificationService.new.autodevops_disabled(pipeline, recipients)
+ end
+
+ def email_receivers_for(pipeline, project)
+ recipients = [pipeline.user&.email]
+ recipients << project.owner.email unless project.group
+ recipients.uniq.compact
+ end
+ end
+end
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
index 53d77dc4524..912c53e11f8 100644
--- a/app/workers/build_coverage_worker.rb
+++ b/app/workers/build_coverage_worker.rb
@@ -4,7 +4,9 @@ class BuildCoverageWorker
include ApplicationWorker
include PipelineQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.find_by(id: build_id)&.update_coverage
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 9dc2c7f3601..51cbbe8882e 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -6,6 +6,7 @@ class BuildFinishedWorker
queue_namespace :pipeline_processing
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
# We execute that in sync as this access the files in order to access local file, and reduce IO
@@ -17,4 +18,5 @@ class BuildFinishedWorker
ArchiveTraceWorker.perform_async(build.id)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index f1f71dc589c..b0c3676714c 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -6,8 +6,10 @@ class BuildHooksWorker
queue_namespace :pipeline_hooks
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.find_by(id: build_id)
.try(:execute_hooks)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index 1b3f1fd3c2a..67d5b0f5f5b 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -6,9 +6,11 @@ class BuildQueueWorker
queue_namespace :pipeline_processing
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
Ci::UpdateBuildQueueService.new.execute(build)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index e1c1cc24a94..c17608f7378 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -6,11 +6,13 @@ class BuildSuccessWorker
queue_namespace :pipeline_processing
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
create_deployment(build) if build.has_environment?
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/build_trace_sections_worker.rb b/app/workers/build_trace_sections_worker.rb
index f4114b3353c..0641130fd64 100644
--- a/app/workers/build_trace_sections_worker.rb
+++ b/app/workers/build_trace_sections_worker.rb
@@ -4,7 +4,9 @@ class BuildTraceSectionsWorker
include ApplicationWorker
include PipelineQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.find_by(id: build_id)&.parse_trace_sections!
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
index 7d4e9660a4e..7443aad1380 100644
--- a/app/workers/ci/archive_traces_cron_worker.rb
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -5,6 +5,7 @@ module Ci
include ApplicationWorker
include CronjobQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform
# Archive stale live traces which still resides in redis or database
# This could happen when ArchiveTraceWorker sidekiq jobs were lost by receiving SIGKILL
@@ -19,6 +20,7 @@ module Ci
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/ci/build_schedule_worker.rb b/app/workers/ci/build_schedule_worker.rb
new file mode 100644
index 00000000000..da219adffc6
--- /dev/null
+++ b/app/workers/ci/build_schedule_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildScheduleWorker
+ include ApplicationWorker
+ include PipelineQueue
+
+ queue_namespace :pipeline_processing
+
+ def perform(build_id)
+ ::Ci::Build.find_by_id(build_id).try do |build|
+ break unless build.scheduled?
+
+ Ci::RunScheduledBuildService
+ .new(build.project, build.user).execute(build)
+ end
+ end
+ end
+end
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
index 9dbf2e5e1ac..23a11c28f9b 100644
--- a/app/workers/ci/build_trace_chunk_flush_worker.rb
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -5,10 +5,12 @@ module Ci
include ApplicationWorker
include PipelineBackgroundQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(build_trace_chunk_id)
::Ci::BuildTraceChunk.find_by(id: build_trace_chunk_id).try do |build_trace_chunk|
build_trace_chunk.persist_data!
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/workers/concerns/auto_devops_queue.rb b/app/workers/concerns/auto_devops_queue.rb
new file mode 100644
index 00000000000..aba928ccaab
--- /dev/null
+++ b/app/workers/concerns/auto_devops_queue.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+#
+module AutoDevopsQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :auto_devops
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
index 692ca6b7f42..1c6413674a0 100644
--- a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
@@ -8,6 +8,7 @@ module Gitlab
# project_id - The ID of the GitLab project to import the note into.
# hash - A Hash containing the details of the GitHub object to imoprt.
# notify_key - The Redis key to notify upon completion, if any.
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, hash, notify_key = nil)
project = Project.find_by(id: project_id)
@@ -24,6 +25,7 @@ module Gitlab
.perform_in(client.rate_limit_resets_in, project.id, hash, notify_key)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
def try_import(*args)
import(*args)
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
index 147c8c8d683..59e6bc2c97d 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -20,11 +20,13 @@ module Gitlab
self.class.perform_in(client.rate_limit_resets_in, project.id)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def find_project(id)
# If the project has been marked as failed we want to bail out
# automatically.
Project.import_started.find_by(id: id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb
index 7735dec5e6b..a89451a4475 100644
--- a/app/workers/concerns/new_issuable.rb
+++ b/app/workers/concerns/new_issuable.rb
@@ -10,17 +10,21 @@ module NewIssuable
user && issuable
end
+ # rubocop: disable CodeReuse/ActiveRecord
def set_user(user_id)
@user = User.find_by(id: user_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
log_error(User, user_id) unless @user # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def set_issuable(issuable_id)
@issuable = issuable_class.find_by(id: issuable_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
log_error(issuable_class, issuable_id) unless @issuable # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+ # rubocop: enable CodeReuse/ActiveRecord
def log_error(record_class, record_id)
Rails.logger.error("#{self.class}: couldn't find #{record_class} with ID=#{record_id}, skipping job")
diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb
index a1aeeb7c4fc..49c7a403838 100644
--- a/app/workers/create_gpg_signature_worker.rb
+++ b/app/workers/create_gpg_signature_worker.rb
@@ -3,6 +3,7 @@
class CreateGpgSignatureWorker
include ApplicationWorker
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(commit_shas, project_id)
# Older versions of GitPushService may push a single commit ID on the stack.
# We need this to be backwards compatible.
@@ -26,4 +27,5 @@ class CreateGpgSignatureWorker
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
new file mode 100644
index 00000000000..e8fe9d82797
--- /dev/null
+++ b/app/workers/delete_container_repository_worker.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class DeleteContainerRepositoryWorker
+ include ApplicationWorker
+ include ExclusiveLeaseGuard
+
+ LEASE_TIMEOUT = 1.hour
+
+ attr_reader :container_repository
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(current_user_id, container_repository_id)
+ current_user = User.find_by(id: current_user_id)
+ @container_repository = ContainerRepository.find_by(id: container_repository_id)
+ project = container_repository&.project
+
+ return unless current_user && container_repository && project
+
+ # If a user accidentally attempts to delete the same container registry in quick succession,
+ # this can lead to orphaned tags.
+ try_obtain_lease do
+ Projects::ContainerRepository::DestroyService.new(project, current_user).execute(container_repository)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # For ExclusiveLeaseGuard concern
+ def lease_key
+ @lease_key ||= "container_repository:delete:#{container_repository.id}"
+ end
+
+ # For ExclusiveLeaseGuard concern
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+end
diff --git a/app/workers/delete_diff_files_worker.rb b/app/workers/delete_diff_files_worker.rb
index 0874a0b75e8..f518dfe871c 100644
--- a/app/workers/delete_diff_files_worker.rb
+++ b/app/workers/delete_diff_files_worker.rb
@@ -3,6 +3,7 @@
class DeleteDiffFilesWorker
include ApplicationWorker
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(merge_request_diff_id)
merge_request_diff = MergeRequestDiff.find(merge_request_diff_id)
@@ -16,4 +17,5 @@ class DeleteDiffFilesWorker
.delete_all
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb
index 854b74b884a..64bc9776d48 100644
--- a/app/workers/detect_repository_languages_worker.rb
+++ b/app/workers/detect_repository_languages_worker.rb
@@ -11,6 +11,7 @@ class DetectRepositoryLanguagesWorker
attr_reader :project
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, user_id)
@project = Project.find_by(id: project_id)
user = User.find_by(id: user_id)
@@ -20,6 +21,7 @@ class DetectRepositoryLanguagesWorker
::Projects::DetectRepositoryLanguagesService.new(project, user).execute
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index 5d3a9a39b93..dce812d1ae2 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -4,6 +4,7 @@ class ExpireBuildArtifactsWorker
include ApplicationWorker
include CronjobQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform
Rails.logger.info 'Scheduling removal of build artifacts'
@@ -12,4 +13,5 @@ class ExpireBuildArtifactsWorker
ExpireBuildInstanceArtifactsWorker.bulk_perform_async(build_ids)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index 3b57ecb36e3..94426dcf921 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -3,6 +3,7 @@
class ExpireBuildInstanceArtifactsWorker
include ApplicationWorker
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
build = Ci::Build
.with_expired_artifacts
@@ -12,6 +13,7 @@ class ExpireBuildInstanceArtifactsWorker
return unless build&.project && !build.project.pending_delete
Rails.logger.info "Removing artifacts for build #{build.id}..."
- build.erase_artifacts!
+ build.erase_erasable_artifacts!
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index 14a57b90114..b09d0a5d121 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -6,6 +6,7 @@ class ExpireJobCacheWorker
queue_namespace :pipeline_cache
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(job_id)
job = CommitStatus.joins(:pipeline, :project).find_by(id: job_id)
return unless job
@@ -18,6 +19,7 @@ class ExpireJobCacheWorker
store.touch(project_job_path(project, job))
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index 992fc63c451..c96e8a0379b 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -6,6 +6,7 @@ class ExpirePipelineCacheWorker
queue_namespace :pipeline_cache
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
return unless pipeline
@@ -23,6 +24,7 @@ class ExpirePipelineCacheWorker
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(pipeline)
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index be0b6c180b0..cd2ceb8dcdf 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -63,12 +63,14 @@ module Gitlab
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def find_project(id)
# TODO: Only select the JID
# This is due to the fact that the JID could be present in either the project record or
# its associated import_state record
Project.import_started.find_by(id: id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
index 68d2c5c4331..65473026b4c 100644
--- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -30,12 +30,14 @@ module Gitlab
# stage, if it died there's nothing we can do anyway.
end
+ # rubocop: disable CodeReuse/ActiveRecord
def find_project(id)
# TODO: Only select the JID
# This is due to the fact that the JID could be present in either the project record or
# its associated import_state record
Project.import_started.find_by(id: id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb
index 4724ab7ad98..fc8a731b427 100644
--- a/app/workers/invalid_gpg_signature_update_worker.rb
+++ b/app/workers/invalid_gpg_signature_update_worker.rb
@@ -3,6 +3,7 @@
class InvalidGpgSignatureUpdateWorker
include ApplicationWorker
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(gpg_key_id)
gpg_key = GpgKey.find_by(id: gpg_key_id)
@@ -10,4 +11,5 @@ class InvalidGpgSignatureUpdateWorker
Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb
index c04a2d75e0b..476cba47ad7 100644
--- a/app/workers/issue_due_scheduler_worker.rb
+++ b/app/workers/issue_due_scheduler_worker.rb
@@ -4,9 +4,11 @@ class IssueDueSchedulerWorker
include ApplicationWorker
include CronjobQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform
project_ids = Issue.opened.due_tomorrow.group(:project_id).pluck(:project_id).map { |id| [id] }
MailScheduler::IssueDueWorker.bulk_perform_async(project_ids)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
index 8794ad7a82c..1e1dde1e829 100644
--- a/app/workers/mail_scheduler/issue_due_worker.rb
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -5,10 +5,12 @@ module MailScheduler
include ApplicationWorker
include MailSchedulerQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id)
Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue|
notification_service.issue_due(issue)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index 5d8b8904502..fa48c1b29a8 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -9,6 +9,8 @@ class NewMergeRequestWorker
EventCreateService.new.open_mr(issuable, user)
NotificationService.new.new_merge_request(issuable, user)
+
+ issuable.diffs(include_stats: false).write_cache
issuable.create_cross_references!(user)
end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 74f34dcf9aa..42f5b945a75 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -5,6 +5,7 @@ class NewNoteWorker
# Keep extra parameter to preserve backwards compatibility with
# old `NewNoteWorker` jobs (can remove later)
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
NotificationService.new.new_note(note)
@@ -13,4 +14,5 @@ class NewNoteWorker
Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index 01d03ec7888..fe5d27b087d 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -57,11 +57,13 @@ module ObjectStorage
include Report
+ # rubocop: disable CodeReuse/ActiveRecord
def self.enqueue!(uploads, model_class, mounted_as, to_store)
sanity_check!(uploads, model_class, mounted_as)
perform_async(uploads.ids, model_class.to_s, mounted_as, to_store)
end
+ # rubocop: enable CodeReuse/ActiveRecord
# We need to be sure all the uploads are for the same uploader and model type
# and that the mount point exists if provided.
@@ -78,6 +80,7 @@ module ObjectStorage
raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount
end
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(*args)
args_check!(args)
@@ -97,6 +100,7 @@ module ObjectStorage
# do not retry: the job is insane
Rails.logger.warn "#{self.class}: Sanity check error (#{e.message})"
end
+ # rubocop: enable CodeReuse/ActiveRecord
def sanity_check!(uploads)
self.class.sanity_check!(uploads, @model_class, @mounted_as)
diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb
index 4610b688189..b3319ff5a13 100644
--- a/app/workers/pages_domain_verification_worker.rb
+++ b/app/workers/pages_domain_verification_worker.rb
@@ -3,6 +3,7 @@
class PagesDomainVerificationWorker
include ApplicationWorker
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(domain_id)
domain = PagesDomain.find_by(id: domain_id)
@@ -10,4 +11,5 @@ class PagesDomainVerificationWorker
VerifyPagesDomainService.new(domain).execute
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index 13a6576a301..fa0dfa2ff4b 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -9,6 +9,7 @@ class PagesWorker
send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
end
+ # rubocop: disable CodeReuse/ActiveRecord
def deploy(build_id)
build = Ci::Build.find_by(id: build_id)
result = Projects::UpdatePagesService.new(build.project, build).execute
@@ -18,6 +19,7 @@ class PagesWorker
result
end
+ # rubocop: enable CodeReuse/ActiveRecord
def remove(namespace_path, project_path)
full_path = File.join(Settings.pages.path, namespace_path, project_path)
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index 58023e0af1b..eae1115e60c 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -6,8 +6,10 @@ class PipelineHooksWorker
queue_namespace :pipeline_hooks
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
.try(:execute_hooks)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index a97019b100a..c2fbfd2b3a5 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -4,12 +4,14 @@ class PipelineMetricsWorker
include ApplicationWorker
include PipelineQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
update_metrics_for_active_pipeline(pipeline) if pipeline.active?
update_metrics_for_succeeded_pipeline(pipeline) if pipeline.success?
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
@@ -21,9 +23,11 @@ class PipelineMetricsWorker
metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at, pipeline_id: pipeline.id)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def metrics(pipeline)
MergeRequest::Metrics.where(merge_request_id: merge_requests(pipeline))
end
+ # rubocop: enable CodeReuse/ActiveRecord
def merge_requests(pipeline)
pipeline.merge_requests.map(&:id)
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
index 3a8846b3747..e4a18573d20 100644
--- a/app/workers/pipeline_notification_worker.rb
+++ b/app/workers/pipeline_notification_worker.rb
@@ -4,6 +4,7 @@ class PipelineNotificationWorker
include ApplicationWorker
include PipelineQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id, recipients = nil)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
@@ -11,4 +12,5 @@ class PipelineNotificationWorker
NotificationService.new.pipeline_finished(pipeline, recipients)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 83744c5338a..f2aa17acb51 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -6,8 +6,10 @@ class PipelineProcessWorker
queue_namespace :pipeline_processing
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
.try(:process!)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index a1815757735..85d1ffe0fa9 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -4,6 +4,7 @@ class PipelineScheduleWorker
include ApplicationWorker
include CronjobQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform
Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
.preload(:owner, :project).find_each do |schedule|
@@ -21,4 +22,5 @@ class PipelineScheduleWorker
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 68e9af6a619..4f349ed922c 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -6,6 +6,7 @@ class PipelineSuccessWorker
queue_namespace :pipeline_processing
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
MergeRequests::MergeWhenPipelineSucceedsService
@@ -13,4 +14,5 @@ class PipelineSuccessWorker
.trigger(pipeline)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index c33468c1f14..13a748e1551 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -6,8 +6,10 @@ class PipelineUpdateWorker
queue_namespace :pipeline_processing
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
.try(:update_status)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index c9f6df9b56d..7b167c95c29 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -14,6 +14,7 @@ class ProcessCommitWorker
# commit_hash - Hash containing commit details to use for constructing a
# Commit object without having to use the Git repository.
# default - The data was pushed to the default branch.
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, user_id, commit_hash, default = false)
project = Project.find_by(id: project_id)
@@ -30,6 +31,7 @@ class ProcessCommitWorker
process_commit_message(project, commit, user, author, default)
update_issue_metrics(commit, author)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def process_commit_message(project, commit, user, author, default = false)
# Ignore closing references from GitLab-generated commit messages.
@@ -50,6 +52,7 @@ class ProcessCommitWorker
end
end
+ # rubocop: disable CodeReuse/ActiveRecord
def update_issue_metrics(commit, author)
mentioned_issues = commit.all_references(author).issues
@@ -58,6 +61,7 @@ class ProcessCommitWorker
Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil)
.update_all(first_mentioned_in_commit_at: commit.committed_date)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def build_commit(project, hash)
date_suffix = '_date'
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index b0e1d8837d9..d27b5e62574 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -12,6 +12,7 @@ class ProjectCacheWorker
# CHANGELOG.
# statistics - An Array containing columns from ProjectStatistics to
# refresh, if empty all columns will be refreshed
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, files = [], statistics = [])
project = Project.find_by(id: project_id)
@@ -23,6 +24,7 @@ class ProjectCacheWorker
project.cleanup
end
+ # rubocop: enable CodeReuse/ActiveRecord
def update_statistics(project, statistics = [])
return unless try_obtain_lease_for(project.id, :update_statistics)
diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb
index ad0003e7bff..4c6339f7701 100644
--- a/app/workers/project_migrate_hashed_storage_worker.rb
+++ b/app/workers/project_migrate_hashed_storage_worker.rb
@@ -5,6 +5,7 @@ class ProjectMigrateHashedStorageWorker
LEASE_TIMEOUT = 30.seconds.to_i
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, old_disk_path = nil)
project = Project.find_by(id: project_id)
return if project.nil? || project.pending_delete?
@@ -19,6 +20,7 @@ class ProjectMigrateHashedStorageWorker
cancel_lease_for(project_id, uuid) if uuid
raise ex
end
+ # rubocop: enable CodeReuse/ActiveRecord
def lease_for(project_id)
Gitlab::ExclusiveLease.new(lease_key(project_id), timeout: LEASE_TIMEOUT)
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index a0bc9288cf0..25567cec08b 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -7,6 +7,10 @@ class ProjectServiceWorker
def perform(hook_id, data)
data = data.with_indifferent_access
- Service.find(hook_id).execute(data)
+ service = Service.find(hook_id)
+ service.execute(data)
+ rescue => error
+ service_class = service&.class&.name || "Not Found"
+ logger.error class: self.class.name, service_class: service_class, message: error.message
end
end
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
index c9da1cae255..3ccd7615697 100644
--- a/app/workers/propagate_service_template_worker.rb
+++ b/app/workers/propagate_service_template_worker.rb
@@ -6,11 +6,13 @@ class PropagateServiceTemplateWorker
LEASE_TIMEOUT = 4.hours.to_i
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(template_id)
return unless try_obtain_lease_for(template_id)
Projects::PropagateServiceTemplate.propagate(Service.find_by(id: template_id))
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
index c1d05ebbcfd..d44ad0d8030 100644
--- a/app/workers/prune_old_events_worker.rb
+++ b/app/workers/prune_old_events_worker.rb
@@ -4,6 +4,7 @@ class PruneOldEventsWorker
include ApplicationWorker
include CronjobQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform
# Contribution calendar shows maximum 12 months of events.
# Double nested query is used because MySQL doesn't allow DELETE subqueries
@@ -17,4 +18,5 @@ class PruneOldEventsWorker
.limit(10_000))
.delete_all
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/prune_web_hook_logs_worker.rb b/app/workers/prune_web_hook_logs_worker.rb
index 45c7d32f7eb..38054069f4e 100644
--- a/app/workers/prune_web_hook_logs_worker.rb
+++ b/app/workers/prune_web_hook_logs_worker.rb
@@ -9,6 +9,7 @@ class PruneWebHookLogsWorker
# The maximum number of rows to remove in a single job.
DELETE_LIMIT = 50_000
+ # rubocop: disable CodeReuse/ActiveRecord
def perform
# MySQL doesn't allow "DELETE FROM ... WHERE id IN ( ... )" if the inner
# query refers to the same table. To work around this we wrap the IN body in
@@ -23,4 +24,5 @@ class PruneWebHookLogsWorker
)
.delete_all
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index 9b331f15dc5..96ff8cd6222 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -3,6 +3,7 @@
class ReactiveCachingWorker
include ApplicationWorker
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(class_name, id, *args)
klass = begin
Kernel.const_get(class_name)
@@ -13,4 +14,5 @@ class ReactiveCachingWorker
klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index 07559ea479b..c1bb1adc9cc 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -59,22 +59,28 @@ module RepositoryCheck
never_checked_project_ids(BATCH_SIZE) + old_checked_project_ids(BATCH_SIZE)
end
+ # rubocop: disable CodeReuse/ActiveRecord
def never_checked_project_ids(batch_size)
projects_on_shard.where(last_repository_check_at: nil)
.where('created_at < ?', 24.hours.ago)
.limit(batch_size).pluck(:id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def old_checked_project_ids(batch_size)
projects_on_shard.where.not(last_repository_check_at: nil)
.where('last_repository_check_at < ?', 1.month.ago)
.reorder(last_repository_check_at: :asc)
.limit(batch_size).pluck(:id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def projects_on_shard
Project.where(repository_storage: shard_name)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def try_obtain_lease_for_project(id)
# Use a 24-hour timeout because on servers/projects where 'git fsck' is
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
index 81e1a4b63bb..01964c69fb2 100644
--- a/app/workers/repository_check/clear_worker.rb
+++ b/app/workers/repository_check/clear_worker.rb
@@ -5,6 +5,7 @@ module RepositoryCheck
include ApplicationWorker
include RepositoryCheckQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform
# Do small batched updates because these updates will be slow and locking
Project.select(:id).find_in_batches(batch_size: 100) do |batch|
@@ -14,5 +15,6 @@ module RepositoryCheck
)
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index f44e5693b25..a8097af321f 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -48,9 +48,11 @@ module RepositoryCheck
false
end
+ # rubocop: disable CodeReuse/ActiveRecord
def has_changes?(project)
Project.with_push.exists?(project.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def has_wiki_changes?(project)
return false unless project.wiki_enabled?
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index 1f6cb18c812..f72331c003a 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -6,6 +6,7 @@ class RunPipelineScheduleWorker
queue_namespace :pipeline_creation
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(schedule_id, user_id)
schedule = Ci::PipelineSchedule.find_by(id: schedule_id)
user = User.find_by(id: user_id)
@@ -14,6 +15,7 @@ class RunPipelineScheduleWorker
run_pipeline_schedule(schedule, user)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def run_pipeline_schedule(schedule, user)
Ci::CreatePipelineService.new(schedule.project,
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
index ec8c8e3689f..ea587789d03 100644
--- a/app/workers/stage_update_worker.rb
+++ b/app/workers/stage_update_worker.rb
@@ -6,9 +6,11 @@ class StageUpdateWorker
queue_namespace :pipeline_processing
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(stage_id)
Ci::Stage.find_by(id: stage_id).try do |stage|
stage.update_status
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index c78b7fac589..25809f68080 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -8,6 +8,7 @@ class StuckCiJobsWorker
BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour
BUILD_PENDING_OUTDATED_TIMEOUT = 1.day
+ BUILD_SCHEDULED_OUTDATED_TIMEOUT = 1.hour
BUILD_PENDING_STUCK_TIMEOUT = 1.hour
def perform
@@ -15,9 +16,10 @@ class StuckCiJobsWorker
Rails.logger.info "#{self.class}: Cleaning stuck builds"
- drop :running, BUILD_RUNNING_OUTDATED_TIMEOUT
- drop :pending, BUILD_PENDING_OUTDATED_TIMEOUT
- drop_stuck :pending, BUILD_PENDING_STUCK_TIMEOUT
+ drop :running, BUILD_RUNNING_OUTDATED_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure
+ drop :pending, BUILD_PENDING_OUTDATED_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure
+ drop :scheduled, BUILD_SCHEDULED_OUTDATED_TIMEOUT, 'scheduled_at IS NOT NULL AND scheduled_at < ?', :stale_schedule
+ drop_stuck :pending, BUILD_PENDING_STUCK_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure
remove_lease
end
@@ -32,24 +34,25 @@ class StuckCiJobsWorker
Gitlab::ExclusiveLease.cancel(EXCLUSIVE_LEASE_KEY, @uuid)
end
- def drop(status, timeout)
- search(status, timeout) do |build|
- drop_build :outdated, build, status, timeout
+ def drop(status, timeout, condition, reason)
+ search(status, timeout, condition) do |build|
+ drop_build :outdated, build, status, timeout, reason
end
end
- def drop_stuck(status, timeout)
- search(status, timeout) do |build|
+ def drop_stuck(status, timeout, condition, reason)
+ search(status, timeout, condition) do |build|
break unless build.stuck?
- drop_build :stuck, build, status, timeout
+ drop_build :stuck, build, status, timeout, reason
end
end
- def search(status, timeout)
+ # rubocop: disable CodeReuse/ActiveRecord
+ def search(status, timeout, condition)
loop do
jobs = Ci::Build.where(status: status)
- .where('ci_builds.updated_at < ?', timeout.ago)
+ .where(condition, timeout.ago)
.includes(:tags, :runner, project: :namespace)
.limit(100)
.to_a
@@ -60,11 +63,12 @@ class StuckCiJobsWorker
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
- def drop_build(type, build, status, timeout)
- Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})"
+ def drop_build(type, build, status, timeout, reason)
+ Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout}, reason: #{reason})"
Gitlab::OptimisticLocking.retry_lock(build, 3) do |b|
- b.drop(:stuck_or_timeout_failure)
+ b.drop(reason)
end
end
end
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index 79ce06dd66e..de92f3eca6a 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -23,6 +23,7 @@ class StuckImportJobsWorker
end.count
end
+ # rubocop: disable CodeReuse/ActiveRecord
def mark_projects_with_jid_as_failed!
# TODO: Rollback this change to use SQL through #pluck
jids_and_ids = enqueued_projects_with_jid.map { |project| [project.import_jid, project.id] }.to_h
@@ -43,18 +44,25 @@ class StuckImportJobsWorker
project.mark_import_as_failed(error_message)
end.count
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def enqueued_projects
Project.joins_import_state.where("(import_state.status = 'scheduled' OR import_state.status = 'started') OR (projects.import_status = 'scheduled' OR projects.import_status = 'started')")
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def enqueued_projects_with_jid
enqueued_projects.where.not("import_state.jid IS NULL AND projects.import_jid IS NULL")
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def enqueued_projects_without_jid
enqueued_projects.where("import_state.jid IS NULL AND projects.import_jid IS NULL")
end
+ # rubocop: enable CodeReuse/ActiveRecord
def error_message
"Import timed out. Import took longer than #{IMPORT_JOBS_EXPIRATION} seconds"
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index b0a62f76e94..98c81956cba 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -4,6 +4,7 @@ class StuckMergeJobsWorker
include ApplicationWorker
include CronjobQueue
+ # rubocop: disable CodeReuse/ActiveRecord
def perform
stuck_merge_requests.find_in_batches(batch_size: 100) do |group|
jids = group.map(&:merge_jid)
@@ -18,9 +19,11 @@ class StuckMergeJobsWorker
end
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
+ # rubocop: disable CodeReuse/ActiveRecord
def apply_current_state!(completed_jids, completed_ids)
merge_requests = MergeRequest.where(id: completed_ids)
@@ -34,8 +37,11 @@ class StuckMergeJobsWorker
Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def stuck_merge_requests
MergeRequest.select('id, merge_jid').with_state(:locked).where.not(merge_jid: nil).reorder(nil)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb
index 0487a393566..9ce51662969 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -6,6 +6,7 @@ class UpdateHeadPipelineForMergeRequestWorker
queue_namespace :pipeline_processing
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(merge_request_id)
merge_request = MergeRequest.find(merge_request_id)
pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last
@@ -20,6 +21,7 @@ class UpdateHeadPipelineForMergeRequestWorker
merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def log_error_message_for(merge_request)
Rails.logger.error(
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 742841219b3..c7213df652a 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -5,6 +5,7 @@ class UpdateMergeRequestsWorker
LOG_TIME_THRESHOLD = 90 # seconds
+ # rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id)
return unless project
@@ -28,4 +29,5 @@ class UpdateMergeRequestsWorker
Rails.logger.info("UpdateMergeRequestsWorker#perform #{args_log}") if time.real > LOG_TIME_THRESHOLD
end
+ # rubocop: enable CodeReuse/ActiveRecord
end