summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorMarcia Ramos <virtua.creative@gmail.com>2018-04-17 12:38:04 -0300
committerMarcia Ramos <virtua.creative@gmail.com>2018-04-17 12:38:04 -0300
commita9c32fe099db50a1ec147b84ee31dafb9321c3a4 (patch)
tree3b0be4dbd5ce89a3adeb51091808b4200688f99a /app
parentd1e0e6815fd5c39683660436aff0581e3ce9f54d (diff)
parent4355a13ad4e04b1ae82b60858376c426ae041699 (diff)
downloadgitlab-ce-a9c32fe099db50a1ec147b84ee31dafb9321c3a4.tar.gz
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into docs-add-badges
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/ext_snippet_icons/ext_snippet_icons.pngbin0 -> 1018 bytes
-rw-r--r--app/assets/images/ext_snippet_icons/logo.pngbin0 -> 494 bytes
-rw-r--r--app/assets/javascripts/awards_handler.js15
-rw-r--r--app/assets/javascripts/badges/components/badge.vue121
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue219
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue57
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue89
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue70
-rw-r--r--app/assets/javascripts/badges/constants.js2
-rw-r--r--app/assets/javascripts/badges/empty_badge.js7
-rw-r--r--app/assets/javascripts/badges/store/actions.js167
-rw-r--r--app/assets/javascripts/badges/store/index.js13
-rw-r--r--app/assets/javascripts/badges/store/mutation_types.js21
-rw-r--r--app/assets/javascripts/badges/store/mutations.js158
-rw-r--r--app/assets/javascripts/badges/store/state.js13
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js2
-rw-r--r--app/assets/javascripts/blob/file_template_selector.js4
-rw-r--r--app/assets/javascripts/boards/components/board.js4
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue (renamed from app/assets/javascripts/boards/components/board_blank_state.js)71
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js6
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js13
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js6
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js6
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js6
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js3
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js3
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js6
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js6
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js1
-rw-r--r--app/assets/javascripts/boards/index.js7
-rw-r--r--app/assets/javascripts/boards/mixins/modal_mixins.js4
-rw-r--r--app/assets/javascripts/boards/models/issue.js6
-rw-r--r--app/assets/javascripts/boards/services/board_service.js2
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js5
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js11
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue185
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue26
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js17
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js29
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js18
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js3
-rw-r--r--app/assets/javascripts/ide/components/ide.vue5
-rw-r--r--app/assets/javascripts/ide/components/ide_file_buttons.vue84
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue40
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue72
-rw-r--r--app/assets/javascripts/ide/components/repo_file_buttons.vue61
-rw-r--r--app/assets/javascripts/ide/components/resizable_panel.vue109
-rw-r--r--app/assets/javascripts/ide/ide_router.js4
-rw-r--r--app/assets/javascripts/ide/lib/editor.js20
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js70
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js7
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js8
-rw-r--r--app/assets/javascripts/ide/stores/utils.js9
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js23
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue2
-rw-r--r--app/assets/javascripts/labels_select.js3
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js7
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js20
-rw-r--r--app/assets/javascripts/merge_request_tabs.js49
-rw-r--r--app/assets/javascripts/milestone.js22
-rw-r--r--app/assets/javascripts/milestone_select.js11
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue24
-rw-r--r--app/assets/javascripts/monitoring/components/graph/axis.vue142
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue32
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue228
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_info.vue29
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue36
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js2
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js84
-rw-r--r--app/assets/javascripts/mr_notes/index.js5
-rw-r--r--app/assets/javascripts/notes.js46
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue11
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue1
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue1
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue11
-rw-r--r--app/assets/javascripts/notes/constants.js6
-rw-r--r--app/assets/javascripts/notes/mixins/noteable.js11
-rw-r--r--app/assets/javascripts/pages/dashboard/milestones/show/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js1
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js1
-rw-r--r--app/assets/javascripts/pages/groups/milestones/show/index.js7
-rw-r--r--app/assets/javascripts/pages/groups/settings/badges/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/badges/index/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js19
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js18
-rw-r--r--app/assets/javascripts/pages/projects/shared/project_new.js152
-rw-r--r--app/assets/javascripts/pages/projects/shared/save_project_loader.js12
-rw-r--r--app/assets/javascripts/pages/projects/snippets/show/index.js6
-rw-r--r--app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js2
-rw-r--r--app/assets/javascripts/pages/shared/mount_badge_settings.js24
-rw-r--r--app/assets/javascripts/pages/snippets/show/index.js10
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js30
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue96
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue84
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue181
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue78
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue64
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue21
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js39
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js23
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js13
-rw-r--r--app/assets/javascripts/pipelines/services/pipeline_service.js13
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js42
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue121
-rw-r--r--app/assets/javascripts/profile/account/index.js15
-rw-r--r--app/assets/javascripts/search_autocomplete.js8
-rw-r--r--app/assets/javascripts/shared/popover.js33
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js11
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js17
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue (renamed from app/assets/javascripts/sidebar/components/time_tracking/help_state.js)49
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue8
-rw-r--r--app/assets/javascripts/snippet/snippet_embed.js23
-rw-r--r--app/assets/javascripts/users_select.js3
-rw-r--r--app/assets/javascripts/visibility_select.js21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue107
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js)267
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue58
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js32
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue68
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal.vue77
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue2
-rw-r--r--app/assets/stylesheets/application.scss6
-rw-r--r--app/assets/stylesheets/framework/animations.scss80
-rw-r--r--app/assets/stylesheets/framework/banner.scss19
-rw-r--r--app/assets/stylesheets/framework/buttons.scss46
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss3
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss13
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss2
-rw-r--r--app/assets/stylesheets/framework/snippets.scss27
-rw-r--r--app/assets/stylesheets/framework/typography.scss5
-rw-r--r--app/assets/stylesheets/framework/variables.scss19
-rw-r--r--app/assets/stylesheets/highlight/embedded.scss3
-rw-r--r--app/assets/stylesheets/highlight/white.scss291
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss290
-rw-r--r--app/assets/stylesheets/pages/builds.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss65
-rw-r--r--app/assets/stylesheets/pages/diff.scss5
-rw-r--r--app/assets/stylesheets/pages/environments.scss46
-rw-r--r--app/assets/stylesheets/pages/login.scss20
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss17
-rw-r--r--app/assets/stylesheets/pages/milestone.scss35
-rw-r--r--app/assets/stylesheets/pages/pages.scss60
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss15
-rw-r--r--app/assets/stylesheets/pages/profile.scss7
-rw-r--r--app/assets/stylesheets/pages/projects.scss13
-rw-r--r--app/assets/stylesheets/pages/repo.scss70
-rw-r--r--app/assets/stylesheets/pages/settings.scss20
-rw-r--r--app/assets/stylesheets/snippets.scss156
-rw-r--r--app/controllers/admin/application_settings_controller.rb21
-rw-r--r--app/controllers/boards/issues_controller.rb3
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb2
-rw-r--r--app/controllers/concerns/checks_collaboration.rb21
-rw-r--r--app/controllers/concerns/notes_actions.rb4
-rw-r--r--app/controllers/concerns/renders_notes.rb2
-rw-r--r--app/controllers/concerns/snippets_actions.rb4
-rw-r--r--app/controllers/dashboard_controller.rb19
-rw-r--r--app/controllers/groups/milestones_controller.rb2
-rw-r--r--app/controllers/groups/settings/badges_controller.rb13
-rw-r--r--app/controllers/groups_controller.rb4
-rw-r--r--app/controllers/jwt_controller.rb3
-rw-r--r--app/controllers/profiles_controller.rb18
-rw-r--r--app/controllers/projects/application_controller.rb14
-rw-r--r--app/controllers/projects/commit_controller.rb1
-rw-r--r--app/controllers/projects/deploy_tokens_controller.rb10
-rw-r--r--app/controllers/projects/discussions_controller.rb4
-rw-r--r--app/controllers/projects/git_http_client_controller.rb1
-rw-r--r--app/controllers/projects/git_http_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/jobs_controller.rb10
-rw-r--r--app/controllers/projects/labels_controller.rb3
-rw-r--r--app/controllers/projects/lfs_api_controller.rb2
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb13
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb2
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb37
-rw-r--r--app/controllers/projects/refs_controller.rb2
-rw-r--r--app/controllers/projects/repositories_controller.rb22
-rw-r--r--app/controllers/projects/settings/badges_controller.rb13
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb50
-rw-r--r--app/controllers/projects/settings/repository_controller.rb30
-rw-r--r--app/controllers/projects/snippets_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb4
-rw-r--r--app/finders/issuable_finder.rb11
-rw-r--r--app/finders/labels_finder.rb35
-rw-r--r--app/finders/merge_request_target_project_finder.rb1
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb6
-rw-r--r--app/helpers/boards_helper.rb6
-rw-r--r--app/helpers/ci_status_helper.rb4
-rw-r--r--app/helpers/commits_helper.rb29
-rw-r--r--app/helpers/compare_helper.rb2
-rw-r--r--app/helpers/deploy_tokens_helper.rb12
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/helpers/icons_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb24
-rw-r--r--app/helpers/issues_helper.rb15
-rw-r--r--app/helpers/labels_helper.rb10
-rw-r--r--app/helpers/markup_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb12
-rw-r--r--app/helpers/notes_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb66
-rw-r--r--app/helpers/services_helper.rb25
-rw-r--r--app/helpers/snippets_helper.rb35
-rw-r--r--app/helpers/tree_helper.rb4
-rw-r--r--app/helpers/workhorse_helper.rb4
-rw-r--r--app/models/ability.rb4
-rw-r--r--app/models/appearance.rb2
-rw-r--r--app/models/broadcast_message.rb6
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/group_variable.rb2
-rw-r--r--app/models/ci/job_artifact.rb5
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb2
-rw-r--r--app/models/commit.rb8
-rw-r--r--app/models/concerns/awardable.rb14
-rw-r--r--app/models/concerns/chronic_duration_attribute.rb6
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/presentable.rb8
-rw-r--r--app/models/concerns/resolvable_discussion.rb2
-rw-r--r--app/models/deploy_key.rb2
-rw-r--r--app/models/deploy_token.rb61
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/event.rb3
-rw-r--r--app/models/fork_network.rb2
-rw-r--r--app/models/group.rb10
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/internal_id.rb24
-rw-r--r--app/models/issue.rb14
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/lfs_object.rb2
-rw-r--r--app/models/merge_request_diff_commit.rb2
-rw-r--r--app/models/milestone.rb6
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/note.rb8
-rw-r--r--app/models/project.rb60
-rw-r--r--app/models/project_deploy_token.rb8
-rw-r--r--app/models/project_services/chat_notification_service.rb14
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/repository.rb1
-rw-r--r--app/models/service.rb14
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb53
-rw-r--r--app/policies/ci/build_policy.rb4
-rw-r--r--app/policies/ci/pipeline_schedule_policy.rb14
-rw-r--r--app/policies/deploy_token_policy.rb11
-rw-r--r--app/policies/issuable_policy.rb16
-rw-r--r--app/policies/issue_policy.rb2
-rw-r--r--app/policies/merge_request_policy.rb1
-rw-r--r--app/policies/note_policy.rb11
-rw-r--r--app/policies/personal_snippet_policy.rb2
-rw-r--r--app/policies/project_policy.rb133
-rw-r--r--app/policies/project_policy/class_methods.rb19
-rw-r--r--app/presenters/ci/build_presenter.rb16
-rw-r--r--app/presenters/merge_request_presenter.rb11
-rw-r--r--app/serializers/build_metadata_entity.rb5
-rw-r--r--app/serializers/issue_entity.rb4
-rw-r--r--app/serializers/note_entity.rb6
-rw-r--r--app/serializers/status_entity.rb2
-rw-r--r--app/services/auth/container_registry_authentication_service.rb22
-rw-r--r--app/services/boards/issues/list_service.rb5
-rw-r--r--app/services/boards/issues/move_service.rb5
-rw-r--r--app/services/boards/lists/create_service.rb8
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb2
-rw-r--r--app/services/deploy_tokens/create_service.rb7
-rw-r--r--app/services/events/render_service.rb12
-rw-r--r--app/services/issuable/destroy_service.rb1
-rw-r--r--app/services/issuable_base_service.rb5
-rw-r--r--app/services/issues/update_service.rb17
-rw-r--r--app/services/merge_requests/create_service.rb4
-rw-r--r--app/services/notes/post_process_service.rb10
-rw-r--r--app/services/notes/render_service.rb13
-rw-r--r--app/services/notification_recipient_service.rb3
-rw-r--r--app/services/projects/autocomplete_service.rb3
-rw-r--r--app/services/projects/base_move_relations_service.rb22
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb28
-rw-r--r--app/services/projects/gitlab_projects_import_service.rb26
-rw-r--r--app/services/projects/import_export/export_service.rb6
-rw-r--r--app/services/projects/move_access_service.rb25
-rw-r--r--app/services/projects/move_deploy_keys_projects_service.rb31
-rw-r--r--app/services/projects/move_forks_service.rb42
-rw-r--r--app/services/projects/move_lfs_objects_projects_service.rb29
-rw-r--r--app/services/projects/move_notification_settings_service.rb38
-rw-r--r--app/services/projects/move_project_authorizations_service.rb40
-rw-r--r--app/services/projects/move_project_group_links_service.rb40
-rw-r--r--app/services/projects/move_project_members_service.rb40
-rw-r--r--app/services/projects/move_users_star_projects_service.rb20
-rw-r--r--app/services/projects/overwrite_project_service.rb69
-rw-r--r--app/services/projects/transfer_service.rb2
-rw-r--r--app/services/projects/update_pages_service.rb14
-rw-r--r--app/services/quick_actions/interpret_service.rb5
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/uploaders/job_artifact_uploader.rb4
-rw-r--r--app/uploaders/legacy_artifact_uploader.rb4
-rw-r--r--app/uploaders/object_storage.rb70
-rw-r--r--app/views/admin/application_settings/_email.html.haml26
-rw-r--r--app/views/admin/application_settings/_form.html.haml173
-rw-r--r--app/views/admin/application_settings/_gitaly.html.haml27
-rw-r--r--app/views/admin/application_settings/_koding.html.haml24
-rw-r--r--app/views/admin/application_settings/_performance.html.haml19
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml20
-rw-r--r--app/views/admin/application_settings/_realtime.html.haml19
-rw-r--r--app/views/admin/application_settings/_registry.html.haml10
-rw-r--r--app/views/admin/application_settings/_signin.html.haml1
-rw-r--r--app/views/admin/application_settings/_terminal.html.haml13
-rw-r--r--app/views/admin/application_settings/_usage.html.haml37
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml1
-rw-r--r--app/views/admin/application_settings/show.html.haml108
-rw-r--r--app/views/admin/dashboard/index.html.haml1
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/users/_user.html.haml2
-rw-r--r--app/views/award_emoji/_awards_block.html.haml4
-rw-r--r--app/views/ci/status/_badge.html.haml4
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml7
-rw-r--r--app/views/dashboard/issues.html.haml12
-rw-r--r--app/views/dashboard/merge_requests.html.haml12
-rw-r--r--app/views/devise/shared/_tab_single.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml7
-rw-r--r--app/views/email_rejection_mailer/rejection.text.haml3
-rw-r--r--app/views/groups/settings/badges/index.html.haml4
-rw-r--r--app/views/layouts/header/_new_dropdown.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml8
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml11
-rw-r--r--app/views/notify/push_to_merge_request_email.html.haml2
-rw-r--r--app/views/notify/push_to_merge_request_email.text.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml16
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml7
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml2
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml11
-rw-r--r--app/views/projects/_last_push.html.haml7
-rw-r--r--app/views/projects/_visibility_select.html.haml9
-rw-r--r--app/views/projects/blob/_viewer.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_highlight_embed.html.haml7
-rw-r--r--app/views/projects/branches/_branch.html.haml4
-rw-r--r--app/views/projects/buttons/_download.html.haml9
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml22
-rw-r--r--app/views/projects/clusters/_empty_state.html.haml5
-rw-r--r--app/views/projects/clusters/new.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml15
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/commits/_commit.atom.builder2
-rw-r--r--app/views/projects/commits/_commit.html.haml18
-rw-r--r--app/views/projects/deploy_tokens/_form.html.haml29
-rw-r--r--app/views/projects/deploy_tokens/_index.html.haml18
-rw-r--r--app/views/projects/deploy_tokens/_new_deploy_token.html.haml14
-rw-r--r--app/views/projects/deploy_tokens/_revoke_modal.html.haml17
-rw-r--r--app/views/projects/deploy_tokens/_table.html.haml31
-rw-r--r--app/views/projects/edit.html.haml11
-rw-r--r--app/views/projects/issues/_discussion.html.haml1
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml13
-rw-r--r--app/views/projects/issues/_new_branch.html.haml8
-rw-r--r--app/views/projects/issues/show.html.haml13
-rw-r--r--app/views/projects/jobs/_empty_state.html.haml5
-rw-r--r--app/views/projects/jobs/_empty_states.html.haml9
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml8
-rw-r--r--app/views/projects/jobs/show.html.haml25
-rw-r--r--app/views/projects/merge_requests/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml1
-rw-r--r--app/views/projects/notes/_actions.html.haml2
-rw-r--r--app/views/projects/pages/_list.html.haml37
-rw-r--r--app/views/projects/pages/show.html.haml3
-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/pages_domains/show.html.haml54
-rw-r--r--app/views/projects/pipelines/new.html.haml15
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_tags_list.html.haml2
-rw-r--r--app/views/projects/registry/repositories/index.html.haml4
-rw-r--r--app/views/projects/settings/badges/index.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/_badge.html.haml (renamed from app/views/projects/pipelines_settings/_badge.html.haml)0
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml (renamed from app/views/projects/pipelines_settings/_show.html.haml)14
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml5
-rw-r--r--app/views/projects/settings/repository/show.html.haml1
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml6
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml33
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml8
-rw-r--r--app/views/shared/_label.html.haml26
-rw-r--r--app/views/shared/_recaptcha_form.html.haml2
-rw-r--r--app/views/shared/badges/_badge_settings.html.haml4
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_assignee.html.haml3
-rw-r--r--app/views/shared/boards/components/sidebar/_due_date.html.haml3
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml3
-rw-r--r--app/views/shared/boards/components/sidebar/_milestone.html.haml3
-rw-r--r--app/views/shared/dashboard/_no_filter_selected.html.haml8
-rw-r--r--app/views/shared/issuable/_filter.html.haml9
-rw-r--r--app/views/shared/issuable/_nav.html.haml11
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/members/_group.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/milestones/_deprecation_message.html.haml14
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml13
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/snippets/_embed.html.haml24
-rw-r--r--app/views/shared/snippets/_header.html.haml25
-rw-r--r--app/views/shared/snippets/show.js.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml7
-rw-r--r--app/workers/authorized_projects_worker.rb8
-rw-r--r--app/workers/new_note_worker.rb2
428 files changed, 6387 insertions, 3054 deletions
diff --git a/app/assets/images/ext_snippet_icons/ext_snippet_icons.png b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png
new file mode 100644
index 00000000000..20380adc4e5
--- /dev/null
+++ b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png
Binary files differ
diff --git a/app/assets/images/ext_snippet_icons/logo.png b/app/assets/images/ext_snippet_icons/logo.png
new file mode 100644
index 00000000000..794c9cc2dbc
--- /dev/null
+++ b/app/assets/images/ext_snippet_icons/logo.png
Binary files differ
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 0e1ca7fe883..976d32abe9b 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -4,7 +4,8 @@ import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
import { __ } from './locale';
-import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
+import { updateTooltipTitle } from './lib/utils/common_utils';
+import { isInVueNoteablePage } from './lib/utils/dom_utils';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -243,7 +244,7 @@ class AwardsHandler {
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
- if (this.isInVueNoteablePage() && !isMainAwardsBlock) {
+ if (isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu'));
@@ -295,16 +296,8 @@ class AwardsHandler {
}
}
- isVueMRDiscussions() {
- return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
- }
-
- isInVueNoteablePage() {
- return isInIssuePage() || isInEpicPage() || this.isVueMRDiscussions();
- }
-
getVotesBlock() {
- if (this.isInVueNoteablePage()) {
+ if (isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
if ($el.length) {
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
new file mode 100644
index 00000000000..6e6cb31e3ac
--- /dev/null
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -0,0 +1,121 @@
+<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: {
+ Tooltip,
+ },
+ props: {
+ imageUrl: {
+ type: String,
+ required: true,
+ },
+ linkUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ hasError: false,
+ isLoading: true,
+ numRetries: 0,
+ };
+ },
+ computed: {
+ imageUrlWithRetries() {
+ if (this.numRetries === 0) {
+ return this.imageUrl;
+ }
+
+ return `${this.imageUrl}#retries=${this.numRetries}`;
+ },
+ },
+ watch: {
+ imageUrl() {
+ this.hasError = false;
+ this.isLoading = true;
+ this.numRetries = 0;
+ },
+ },
+ methods: {
+ onError() {
+ this.isLoading = false;
+ this.hasError = true;
+ },
+ onLoad() {
+ this.isLoading = false;
+ },
+ reloadImage() {
+ this.hasError = false;
+ this.isLoading = true;
+ this.numRetries += 1;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <a
+ v-show="!isLoading && !hasError"
+ :href="linkUrl"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <img
+ class="project-badge"
+ :src="imageUrlWithRetries"
+ @load="onLoad"
+ @error="onError"
+ aria-hidden="true"
+ />
+ </a>
+
+ <loading-icon
+ v-show="isLoading"
+ :inline="true"
+ />
+
+ <div
+ v-show="hasError"
+ class="btn-group"
+ >
+ <div class="btn btn-default btn-xs disabled">
+ <icon
+ class="prepend-left-8 append-right-8"
+ name="doc_image"
+ :size="16"
+ aria-hidden="true"
+ />
+ </div>
+ <div
+ class="btn btn-default btn-xs disabled"
+ >
+ <span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span>
+ </div>
+ </div>
+
+ <button
+ v-show="hasError"
+ class="btn btn-transparent btn-xs text-primary"
+ type="button"
+ v-tooltip
+ :title="s__('Badges|Reload badge image')"
+ @click="reloadImage"
+ >
+ <icon
+ name="retry"
+ :size="16"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
new file mode 100644
index 00000000000..ae942b2c1a7
--- /dev/null
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -0,0 +1,219 @@
+<script>
+import _ from 'underscore';
+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';
+
+const badgePreviewDelayInMilliseconds = 1500;
+
+export default {
+ name: 'BadgeForm',
+ components: {
+ Badge,
+ LoadingButton,
+ LoadingIcon,
+ },
+ props: {
+ isEditing: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'badgeInAddForm',
+ 'badgeInEditForm',
+ 'docsUrl',
+ 'isRendering',
+ 'isSaving',
+ 'renderedBadge',
+ ]),
+ badge() {
+ if (this.isEditing) {
+ return this.badgeInEditForm;
+ }
+
+ 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>`)
+ .join(', ');
+ return sprintf(
+ s__('Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}'),
+ {
+ docsLinkEnd: '</a>',
+ docsLinkStart: `<a href="${_.escape(this.docsUrl)}">`,
+ placeholders,
+ },
+ false,
+ );
+ },
+ renderedImageUrl() {
+ return this.renderedBadge ? this.renderedBadge.renderedImageUrl : '';
+ },
+ renderedLinkUrl() {
+ return this.renderedBadge ? this.renderedBadge.renderedLinkUrl : '';
+ },
+ imageUrl: {
+ get() {
+ return this.badge ? this.badge.imageUrl : '';
+ },
+ set(imageUrl) {
+ const badge = this.badge || createEmptyBadge();
+ this.updateBadgeInForm({
+ ...badge,
+ imageUrl,
+ });
+ },
+ },
+ linkUrl: {
+ get() {
+ return this.badge ? this.badge.linkUrl : '';
+ },
+ set(linkUrl) {
+ const badge = this.badge || createEmptyBadge();
+ this.updateBadgeInForm({
+ ...badge,
+ linkUrl,
+ });
+ },
+ },
+ submitButtonLabel() {
+ if (this.isEditing) {
+ return s__('Badges|Save changes');
+ }
+ return s__('Badges|Add badge');
+ },
+ },
+ methods: {
+ ...mapActions(['addBadge', 'renderBadge', 'saveBadge', 'stopEditing', 'updateBadgeInForm']),
+ debouncedPreview: _.debounce(function preview() {
+ this.renderBadge();
+ }, badgePreviewDelayInMilliseconds),
+ onCancel() {
+ this.stopEditing();
+ },
+ onSubmit() {
+ if (!this.canSubmit) {
+ return Promise.resolve();
+ }
+
+ if (this.isEditing) {
+ return this.saveBadge()
+ .then(() => {
+ createFlash(s__('Badges|The badge was saved.'), 'notice');
+ })
+ .catch(error => {
+ createFlash(
+ s__('Badges|Saving the badge failed, please check the entered URLs and try again.'),
+ );
+ throw error;
+ });
+ }
+
+ return this.addBadge()
+ .then(() => {
+ createFlash(s__('Badges|A new badge was added.'), 'notice');
+ })
+ .catch(error => {
+ createFlash(
+ s__('Badges|Adding the badge failed, please check the entered URLs and try again.'),
+ );
+ throw error;
+ });
+ },
+ },
+ 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"
+ @submit.prevent.stop="onSubmit"
+ >
+ <div class="form-group">
+ <label for="badge-link-url">{{ s__('Badges|Link') }}</label>
+ <input
+ id="badge-link-url"
+ type="text"
+ class="form-control"
+ v-model="linkUrl"
+ :placeholder="$options.badgeLinkUrlPlaceholder"
+ @input="debouncedPreview"
+ />
+ <span
+ class="help-block"
+ v-html="helpText"
+ ></span>
+ </div>
+
+ <div class="form-group">
+ <label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
+ <input
+ id="badge-image-url"
+ type="text"
+ class="form-control"
+ v-model="imageUrl"
+ :placeholder="$options.badgeImageUrlPlaceholder"
+ @input="debouncedPreview"
+ />
+ <span
+ class="help-block"
+ v-html="helpText"
+ ></span>
+ </div>
+
+ <div class="form-group">
+ <label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label>
+ <badge
+ id="badge-preview"
+ v-show="renderedBadge && !isRendering"
+ :image-url="renderedImageUrl"
+ :link-url="renderedLinkUrl"
+ />
+ <p v-show="isRendering">
+ <loading-icon
+ :inline="true"
+ />
+ </p>
+ <p
+ v-show="!renderedBadge && !isRendering"
+ class="disabled-content"
+ >{{ s__('Badges|No image to preview') }}</p>
+ </div>
+
+ <div class="row-content-block">
+ <loading-button
+ type="submit"
+ container-class="btn btn-success"
+ :disabled="!canSubmit"
+ :loading="isSaving"
+ :label="submitButtonLabel"
+ />
+ <button
+ class="btn btn-cancel"
+ type="button"
+ v-if="isEditing"
+ @click="onCancel"
+ >{{ __('Cancel') }}</button>
+ </div>
+ </form>
+</template>
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
new file mode 100644
index 00000000000..ca7197e1e0f
--- /dev/null
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -0,0 +1,57 @@
+<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';
+
+export default {
+ name: 'BadgeList',
+ components: {
+ BadgeListRow,
+ LoadingIcon,
+ },
+ computed: {
+ ...mapState(['badges', 'isLoading', 'kind']),
+ hasNoBadges() {
+ return !this.isLoading && (!this.badges || !this.badges.length);
+ },
+ isGroupBadge() {
+ return this.kind === GROUP_BADGE;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{ s__('Badges|Your badges') }}
+ <span
+ v-show="!isLoading"
+ class="badge"
+ >{{ badges.length }}</span>
+ </div>
+ <loading-icon
+ v-show="isLoading"
+ class="panel-body"
+ size="2"
+ />
+ <div
+ v-if="hasNoBadges"
+ class="panel-body"
+ >
+ <span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
+ <span v-else>{{ s__('Badges|This project has no badges') }}</span>
+ </div>
+ <div
+ v-else
+ class="panel-body"
+ >
+ <badge-list-row
+ v-for="badge in badges"
+ :key="badge.id"
+ :badge="badge"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
new file mode 100644
index 00000000000..af062bdf8c6
--- /dev/null
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -0,0 +1,89 @@
+<script>
+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';
+
+export default {
+ name: 'BadgeListRow',
+ components: {
+ Badge,
+ Icon,
+ LoadingIcon,
+ },
+ props: {
+ badge: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['kind']),
+ badgeKindText() {
+ if (this.badge.kind === PROJECT_BADGE) {
+ return s__('Badges|Project Badge');
+ }
+
+ return s__('Badges|Group Badge');
+ },
+ canEditBadge() {
+ return this.badge.kind === this.kind;
+ },
+ },
+ methods: {
+ ...mapActions(['editBadge', 'updateBadgeInModal']),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-responsive-table-row-layout gl-responsive-table-row">
+ <badge
+ class="table-section section-30"
+ :image-url="badge.renderedImageUrl"
+ :link-url="badge.renderedLinkUrl"
+ />
+ <span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
+ <div class="table-section section-10">
+ <span class="badge">{{ badgeKindText }}</span>
+ </div>
+ <div class="table-section section-10 table-button-footer">
+ <div
+ v-if="canEditBadge"
+ class="table-action-buttons">
+ <button
+ class="btn btn-default append-right-8"
+ type="button"
+ :disabled="badge.isDeleting"
+ @click="editBadge(badge)"
+ >
+ <icon
+ name="pencil"
+ :size="16"
+ :aria-label="__('Edit')"
+ />
+ </button>
+ <button
+ class="btn btn-danger"
+ type="button"
+ data-toggle="modal"
+ data-target="#delete-badge-modal"
+ :disabled="badge.isDeleting"
+ @click="updateBadgeInModal(badge)"
+ >
+ <icon
+ name="remove"
+ :size="16"
+ :aria-label="__('Delete')"
+ />
+ </button>
+ <loading-icon
+ v-show="badge.isDeleting"
+ :inline="true"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
new file mode 100644
index 00000000000..83f78394238
--- /dev/null
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -0,0 +1,70 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import Badge from './badge.vue';
+import BadgeForm from './badge_form.vue';
+import BadgeList from './badge_list.vue';
+
+export default {
+ name: 'BadgeSettings',
+ components: {
+ Badge,
+ BadgeForm,
+ BadgeList,
+ GlModal,
+ },
+ computed: {
+ ...mapState(['badgeInModal', 'isEditing']),
+ deleteModalText() {
+ return s__(
+ 'Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.',
+ );
+ },
+ },
+ methods: {
+ ...mapActions(['deleteBadge']),
+ onSubmitModal() {
+ this.deleteBadge(this.badgeInModal)
+ .then(() => {
+ createFlash(s__('Badges|The badge was deleted.'), 'notice');
+ })
+ .catch(error => {
+ createFlash(s__('Badges|Deleting the badge failed, please try again.'));
+ throw error;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="badge-settings">
+ <gl-modal
+ id="delete-badge-modal"
+ :header-title-text="s__('Badges|Delete badge?')"
+ footer-primary-button-variant="danger"
+ :footer-primary-button-text="s__('Badges|Delete badge')"
+ @submit="onSubmitModal">
+ <div class="well">
+ <badge
+ :image-url="badgeInModal ? badgeInModal.renderedImageUrl : ''"
+ :link-url="badgeInModal ? badgeInModal.renderedLinkUrl : ''"
+ />
+ </div>
+ <p v-html="deleteModalText"></p>
+ </gl-modal>
+
+ <badge-form
+ v-show="isEditing"
+ :is-editing="true"
+ />
+
+ <badge-form
+ v-show="!isEditing"
+ :is-editing="false"
+ />
+ <badge-list v-show="!isEditing" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/badges/constants.js b/app/assets/javascripts/badges/constants.js
new file mode 100644
index 00000000000..8fbe3db5ef1
--- /dev/null
+++ b/app/assets/javascripts/badges/constants.js
@@ -0,0 +1,2 @@
+export const GROUP_BADGE = 'group';
+export const PROJECT_BADGE = 'project';
diff --git a/app/assets/javascripts/badges/empty_badge.js b/app/assets/javascripts/badges/empty_badge.js
new file mode 100644
index 00000000000..49a9b5e1be8
--- /dev/null
+++ b/app/assets/javascripts/badges/empty_badge.js
@@ -0,0 +1,7 @@
+export default () => ({
+ imageUrl: '',
+ isDeleting: false,
+ linkUrl: '',
+ renderedImageUrl: '',
+ renderedLinkUrl: '',
+});
diff --git a/app/assets/javascripts/badges/store/actions.js b/app/assets/javascripts/badges/store/actions.js
new file mode 100644
index 00000000000..5542278b3e0
--- /dev/null
+++ b/app/assets/javascripts/badges/store/actions.js
@@ -0,0 +1,167 @@
+import axios from '~/lib/utils/axios_utils';
+import types from './mutation_types';
+
+export const transformBackendBadge = badge => ({
+ id: badge.id,
+ imageUrl: badge.image_url,
+ kind: badge.kind,
+ linkUrl: badge.link_url,
+ renderedImageUrl: badge.rendered_image_url,
+ renderedLinkUrl: badge.rendered_link_url,
+ isDeleting: false,
+});
+
+export default {
+ requestNewBadge({ commit }) {
+ commit(types.REQUEST_NEW_BADGE);
+ },
+ receiveNewBadge({ commit }, newBadge) {
+ commit(types.RECEIVE_NEW_BADGE, newBadge);
+ },
+ receiveNewBadgeError({ commit }) {
+ commit(types.RECEIVE_NEW_BADGE_ERROR);
+ },
+ addBadge({ dispatch, state }) {
+ const newBadge = state.badgeInAddForm;
+ const endpoint = state.apiEndpointUrl;
+ dispatch('requestNewBadge');
+ return axios
+ .post(endpoint, {
+ image_url: newBadge.imageUrl,
+ link_url: newBadge.linkUrl,
+ })
+ .catch(error => {
+ dispatch('receiveNewBadgeError');
+ throw error;
+ })
+ .then(res => {
+ dispatch('receiveNewBadge', transformBackendBadge(res.data));
+ });
+ },
+ requestDeleteBadge({ commit }, badgeId) {
+ commit(types.REQUEST_DELETE_BADGE, badgeId);
+ },
+ receiveDeleteBadge({ commit }, badgeId) {
+ commit(types.RECEIVE_DELETE_BADGE, badgeId);
+ },
+ receiveDeleteBadgeError({ commit }, badgeId) {
+ commit(types.RECEIVE_DELETE_BADGE_ERROR, badgeId);
+ },
+ deleteBadge({ dispatch, state }, badge) {
+ const badgeId = badge.id;
+ dispatch('requestDeleteBadge', badgeId);
+ const endpoint = `${state.apiEndpointUrl}/${badgeId}`;
+ return axios
+ .delete(endpoint)
+ .catch(error => {
+ dispatch('receiveDeleteBadgeError', badgeId);
+ throw error;
+ })
+ .then(() => {
+ dispatch('receiveDeleteBadge', badgeId);
+ });
+ },
+
+ editBadge({ commit }, badge) {
+ commit(types.START_EDITING, badge);
+ },
+
+ requestLoadBadges({ commit }, data) {
+ commit(types.REQUEST_LOAD_BADGES, data);
+ },
+ receiveLoadBadges({ commit }, badges) {
+ commit(types.RECEIVE_LOAD_BADGES, badges);
+ },
+ receiveLoadBadgesError({ commit }) {
+ commit(types.RECEIVE_LOAD_BADGES_ERROR);
+ },
+
+ loadBadges({ dispatch, state }, data) {
+ dispatch('requestLoadBadges', data);
+ const endpoint = state.apiEndpointUrl;
+ return axios
+ .get(endpoint)
+ .catch(error => {
+ dispatch('receiveLoadBadgesError');
+ throw error;
+ })
+ .then(res => {
+ dispatch('receiveLoadBadges', res.data.map(transformBackendBadge));
+ });
+ },
+
+ requestRenderedBadge({ commit }) {
+ commit(types.REQUEST_RENDERED_BADGE);
+ },
+ receiveRenderedBadge({ commit }, renderedBadge) {
+ commit(types.RECEIVE_RENDERED_BADGE, renderedBadge);
+ },
+ receiveRenderedBadgeError({ commit }) {
+ commit(types.RECEIVE_RENDERED_BADGE_ERROR);
+ },
+
+ renderBadge({ dispatch, state }) {
+ const badge = state.isEditing ? state.badgeInEditForm : state.badgeInAddForm;
+ const { linkUrl, imageUrl } = badge;
+ if (!linkUrl || linkUrl.trim() === '' || !imageUrl || imageUrl.trim() === '') {
+ return Promise.resolve(badge);
+ }
+
+ dispatch('requestRenderedBadge');
+
+ const parameters = [
+ `link_url=${encodeURIComponent(linkUrl)}`,
+ `image_url=${encodeURIComponent(imageUrl)}`,
+ ].join('&');
+ const renderEndpoint = `${state.apiEndpointUrl}/render?${parameters}`;
+ return axios
+ .get(renderEndpoint)
+ .catch(error => {
+ dispatch('receiveRenderedBadgeError');
+ throw error;
+ })
+ .then(res => {
+ dispatch('receiveRenderedBadge', transformBackendBadge(res.data));
+ });
+ },
+
+ requestUpdatedBadge({ commit }) {
+ commit(types.REQUEST_UPDATED_BADGE);
+ },
+ receiveUpdatedBadge({ commit }, updatedBadge) {
+ commit(types.RECEIVE_UPDATED_BADGE, updatedBadge);
+ },
+ receiveUpdatedBadgeError({ commit }) {
+ commit(types.RECEIVE_UPDATED_BADGE_ERROR);
+ },
+
+ saveBadge({ dispatch, state }) {
+ const badge = state.badgeInEditForm;
+ const endpoint = `${state.apiEndpointUrl}/${badge.id}`;
+ dispatch('requestUpdatedBadge');
+ return axios
+ .put(endpoint, {
+ image_url: badge.imageUrl,
+ link_url: badge.linkUrl,
+ })
+ .catch(error => {
+ dispatch('receiveUpdatedBadgeError');
+ throw error;
+ })
+ .then(res => {
+ dispatch('receiveUpdatedBadge', transformBackendBadge(res.data));
+ });
+ },
+
+ stopEditing({ commit }) {
+ commit(types.STOP_EDITING);
+ },
+
+ updateBadgeInForm({ commit }, badge) {
+ commit(types.UPDATE_BADGE_IN_FORM, badge);
+ },
+
+ updateBadgeInModal({ commit }, badge) {
+ commit(types.UPDATE_BADGE_IN_MODAL, badge);
+ },
+};
diff --git a/app/assets/javascripts/badges/store/index.js b/app/assets/javascripts/badges/store/index.js
new file mode 100644
index 00000000000..7a5df403a0e
--- /dev/null
+++ b/app/assets/javascripts/badges/store/index.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import createState from './state';
+import actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: createState(),
+ actions,
+ mutations,
+});
diff --git a/app/assets/javascripts/badges/store/mutation_types.js b/app/assets/javascripts/badges/store/mutation_types.js
new file mode 100644
index 00000000000..d73f91b6005
--- /dev/null
+++ b/app/assets/javascripts/badges/store/mutation_types.js
@@ -0,0 +1,21 @@
+export default {
+ RECEIVE_DELETE_BADGE: 'RECEIVE_DELETE_BADGE',
+ RECEIVE_DELETE_BADGE_ERROR: 'RECEIVE_DELETE_BADGE_ERROR',
+ RECEIVE_LOAD_BADGES: 'RECEIVE_LOAD_BADGES',
+ RECEIVE_LOAD_BADGES_ERROR: 'RECEIVE_LOAD_BADGES_ERROR',
+ RECEIVE_NEW_BADGE: 'RECEIVE_NEW_BADGE',
+ RECEIVE_NEW_BADGE_ERROR: 'RECEIVE_NEW_BADGE_ERROR',
+ RECEIVE_RENDERED_BADGE: 'RECEIVE_RENDERED_BADGE',
+ RECEIVE_RENDERED_BADGE_ERROR: 'RECEIVE_RENDERED_BADGE_ERROR',
+ RECEIVE_UPDATED_BADGE: 'RECEIVE_UPDATED_BADGE',
+ RECEIVE_UPDATED_BADGE_ERROR: 'RECEIVE_UPDATED_BADGE_ERROR',
+ REQUEST_DELETE_BADGE: 'REQUEST_DELETE_BADGE',
+ REQUEST_LOAD_BADGES: 'REQUEST_LOAD_BADGES',
+ REQUEST_NEW_BADGE: 'REQUEST_NEW_BADGE',
+ REQUEST_RENDERED_BADGE: 'REQUEST_RENDERED_BADGE',
+ REQUEST_UPDATED_BADGE: 'REQUEST_UPDATED_BADGE',
+ START_EDITING: 'START_EDITING',
+ STOP_EDITING: 'STOP_EDITING',
+ UPDATE_BADGE_IN_FORM: 'UPDATE_BADGE_IN_FORM',
+ UPDATE_BADGE_IN_MODAL: 'UPDATE_BADGE_IN_MODAL',
+};
diff --git a/app/assets/javascripts/badges/store/mutations.js b/app/assets/javascripts/badges/store/mutations.js
new file mode 100644
index 00000000000..bd84e68c00f
--- /dev/null
+++ b/app/assets/javascripts/badges/store/mutations.js
@@ -0,0 +1,158 @@
+import types from './mutation_types';
+import { PROJECT_BADGE } from '../constants';
+
+const reorderBadges = badges =>
+ badges.sort((a, b) => {
+ if (a.kind !== b.kind) {
+ return a.kind === PROJECT_BADGE ? 1 : -1;
+ }
+
+ return a.id - b.id;
+ });
+
+export default {
+ [types.RECEIVE_NEW_BADGE](state, newBadge) {
+ Object.assign(state, {
+ badgeInAddForm: null,
+ badges: reorderBadges(state.badges.concat(newBadge)),
+ isSaving: false,
+ renderedBadge: null,
+ });
+ },
+ [types.RECEIVE_NEW_BADGE_ERROR](state) {
+ Object.assign(state, {
+ isSaving: false,
+ });
+ },
+ [types.REQUEST_NEW_BADGE](state) {
+ Object.assign(state, {
+ isSaving: true,
+ });
+ },
+
+ [types.RECEIVE_UPDATED_BADGE](state, updatedBadge) {
+ const badges = state.badges.map(badge => {
+ if (badge.id === updatedBadge.id) {
+ return updatedBadge;
+ }
+ return badge;
+ });
+ Object.assign(state, {
+ badgeInEditForm: null,
+ badges,
+ isEditing: false,
+ isSaving: false,
+ renderedBadge: null,
+ });
+ },
+ [types.RECEIVE_UPDATED_BADGE_ERROR](state) {
+ Object.assign(state, {
+ isSaving: false,
+ });
+ },
+ [types.REQUEST_UPDATED_BADGE](state) {
+ Object.assign(state, {
+ isSaving: true,
+ });
+ },
+
+ [types.RECEIVE_LOAD_BADGES](state, badges) {
+ Object.assign(state, {
+ badges: reorderBadges(badges),
+ isLoading: false,
+ });
+ },
+ [types.RECEIVE_LOAD_BADGES_ERROR](state) {
+ Object.assign(state, {
+ isLoading: false,
+ });
+ },
+ [types.REQUEST_LOAD_BADGES](state, data) {
+ Object.assign(state, {
+ kind: data.kind, // project or group
+ apiEndpointUrl: data.apiEndpointUrl,
+ docsUrl: data.docsUrl,
+ isLoading: true,
+ });
+ },
+
+ [types.RECEIVE_DELETE_BADGE](state, badgeId) {
+ const badges = state.badges.filter(badge => badge.id !== badgeId);
+ Object.assign(state, {
+ badges,
+ });
+ },
+ [types.RECEIVE_DELETE_BADGE_ERROR](state, badgeId) {
+ const badges = state.badges.map(badge => {
+ if (badge.id === badgeId) {
+ return {
+ ...badge,
+ isDeleting: false,
+ };
+ }
+
+ return badge;
+ });
+ Object.assign(state, {
+ badges,
+ });
+ },
+ [types.REQUEST_DELETE_BADGE](state, badgeId) {
+ const badges = state.badges.map(badge => {
+ if (badge.id === badgeId) {
+ return {
+ ...badge,
+ isDeleting: true,
+ };
+ }
+
+ return badge;
+ });
+ Object.assign(state, {
+ badges,
+ });
+ },
+
+ [types.RECEIVE_RENDERED_BADGE](state, renderedBadge) {
+ Object.assign(state, { isRendering: false, renderedBadge });
+ },
+ [types.RECEIVE_RENDERED_BADGE_ERROR](state) {
+ Object.assign(state, { isRendering: false });
+ },
+ [types.REQUEST_RENDERED_BADGE](state) {
+ Object.assign(state, { isRendering: true });
+ },
+
+ [types.START_EDITING](state, badge) {
+ Object.assign(state, {
+ badgeInEditForm: { ...badge },
+ isEditing: true,
+ renderedBadge: { ...badge },
+ });
+ },
+ [types.STOP_EDITING](state) {
+ Object.assign(state, {
+ badgeInEditForm: null,
+ isEditing: false,
+ renderedBadge: null,
+ });
+ },
+
+ [types.UPDATE_BADGE_IN_FORM](state, badge) {
+ if (state.isEditing) {
+ Object.assign(state, {
+ badgeInEditForm: badge,
+ });
+ } else {
+ Object.assign(state, {
+ badgeInAddForm: badge,
+ });
+ }
+ },
+
+ [types.UPDATE_BADGE_IN_MODAL](state, badge) {
+ Object.assign(state, {
+ badgeInModal: badge,
+ });
+ },
+};
diff --git a/app/assets/javascripts/badges/store/state.js b/app/assets/javascripts/badges/store/state.js
new file mode 100644
index 00000000000..43413aeb5bb
--- /dev/null
+++ b/app/assets/javascripts/badges/store/state.js
@@ -0,0 +1,13 @@
+export default () => ({
+ apiEndpointUrl: null,
+ badgeInAddForm: null,
+ badgeInEditForm: null,
+ badgeInModal: null,
+ badges: [],
+ docsUrl: null,
+ renderedBadge: null,
+ isEditing: false,
+ isLoading: false,
+ isRendering: false,
+ isSaving: false,
+});
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index 030ca1907e5..ff1cbcad145 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -94,7 +94,7 @@ export default class FileTemplateMediator {
const hash = urlPieces[1];
if (hash === 'preview') {
this.hideTemplateSelectorMenu();
- } else if (hash === 'editor') {
+ } else if (hash === 'editor' && !this.typeSelector.isHidden()) {
this.showTemplateSelectorMenu();
}
});
diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js
index e52cf249f3a..02228434a29 100644
--- a/app/assets/javascripts/blob/file_template_selector.js
+++ b/app/assets/javascripts/blob/file_template_selector.js
@@ -32,6 +32,10 @@ export default class FileTemplateSelector {
}
}
+ isHidden() {
+ return this.$wrapper.hasClass('hidden');
+ }
+
getToggleText() {
return this.$dropdownToggleText.text();
}
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 3cffd91716a..bea818010a4 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -5,7 +5,7 @@ import Sortable from 'vendor/Sortable';
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue';
-import boardBlankState from './board_blank_state';
+import BoardBlankState from './board_blank_state.vue';
import './board_delete';
const Store = gl.issueBoards.BoardsStore;
@@ -18,7 +18,7 @@ gl.issueBoards.Board = Vue.extend({
components: {
boardList,
'board-delete': gl.issueBoards.BoardDelete,
- boardBlankState,
+ BoardBlankState,
},
props: {
list: Object,
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.vue
index 72db626d3c7..2049eeb9c30 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -1,42 +1,11 @@
+<script>
/* global ListLabel */
-
import _ from 'underscore';
import Cookies from 'js-cookie';
const Store = gl.issueBoards.BoardsStore;
export default {
- template: `
- <div class="board-blank-state">
- <p>
- Add the following default lists to your Issue Board with one click:
- </p>
- <ul class="board-blank-state-list">
- <li v-for="label in predefinedLabels">
- <span
- class="label-color"
- :style="{ backgroundColor: label.color }">
- </span>
- {{ label.title }}
- </li>
- </ul>
- <p>
- Starting out with the default set of lists will get you right on the way to making the most of your board.
- </p>
- <button
- class="btn btn-create btn-inverted btn-block"
- type="button"
- @click.stop="addDefaultLists">
- Add default lists
- </button>
- <button
- class="btn btn-default btn-block"
- type="button"
- @click.stop="clearBlankState">
- Nevermind, I'll use my own
- </button>
- </div>
- `,
data() {
return {
predefinedLabels: [
@@ -89,3 +58,41 @@ export default {
clearBlankState: Store.removeBlankState.bind(Store),
},
};
+
+</script>
+
+<template>
+ <div class="board-blank-state">
+ <p>
+ Add the following default lists to your Issue Board with one click:
+ </p>
+ <ul class="board-blank-state-list">
+ <li
+ v-for="(label, index) in predefinedLabels"
+ :key="index"
+ >
+ <span
+ class="label-color"
+ :style="{ backgroundColor: label.color }">
+ </span>
+ {{ label.title }}
+ </li>
+ </ul>
+ <p>
+ Starting out with the default set of lists will get you
+ right on the way to making the most of your board.
+ </p>
+ <button
+ class="btn btn-create btn-inverted btn-block"
+ type="button"
+ @click.stop="addDefaultLists">
+ Add default lists
+ </button>
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click.stop="clearBlankState">
+ Nevermind, I'll use my own
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index a44969272a1..c4ee4f6c855 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -60,10 +60,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
this.issue = this.detail.issue;
this.list = this.detail.list;
-
- this.$nextTick(() => {
- this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate;
- });
},
deep: true
},
@@ -91,7 +87,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
saveAssignees () {
this.loadingAssignees = true;
- gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint)
+ gl.issueBoards.BoardsStore.detail.issue.update()
.then(() => {
this.loadingAssignees = false;
})
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index 8aee5b23c76..84fe9b1288a 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -68,15 +68,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return this.issue.assignees.length > this.numberOverLimit;
},
- cardUrl() {
- let baseUrl = this.issueLinkBase;
-
- if (this.groupId && this.issue.project) {
- baseUrl = this.issueLinkBase.replace(':project_path', this.issue.project.path);
- }
-
- return `${baseUrl}/${this.issue.iid}`;
- },
issueId() {
if (this.issue.iid) {
return `#${this.issue.iid}`;
@@ -153,13 +144,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
/>
<a
class="js-no-trigger"
- :href="cardUrl"
+ :href="issue.path"
:title="issue.title">{{ issue.title }}</a>
<span
class="card-number"
v-if="issueId"
>
- <template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }}
+ {{ issue.referencePath }}
</span>
</h4>
<div class="card-assignee">
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index e571b11a83d..9e37f95cdd6 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
-
-const ModalStore = gl.issueBoards.ModalStore;
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalEmptyState = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
+ mixins: [modalMixin],
data() {
return ModalStore.store;
},
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index 03cd7ef65cb..9735e0ddacc 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -3,11 +3,11 @@ import Flash from '../../../flash';
import { __ } from '../../../locale';
import './lists_dropdown';
import { pluralize } from '../../../lib/utils/text_utility';
-
-const ModalStore = gl.issueBoards.ModalStore;
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalFooter = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
+ mixins: [modalMixin],
data() {
return {
modal: ModalStore.store,
diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js
index 31f59d295bf..67c29ebca72 100644
--- a/app/assets/javascripts/boards/components/modal/header.js
+++ b/app/assets/javascripts/boards/components/modal/header.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import modalFilters from './filters';
import './tabs';
-
-const ModalStore = gl.issueBoards.ModalStore;
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalHeader = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
+ mixins: [modalMixin],
props: {
projectId: {
type: Number,
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index d825ff38587..3083b3e4405 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -7,8 +7,7 @@ import './header';
import './list';
import './footer';
import './empty_state';
-
-const ModalStore = gl.issueBoards.ModalStore;
+import ModalStore from '../../stores/modal_store';
gl.issueBoards.IssuesModal = Vue.extend({
props: {
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
index 7c62134b3a3..6b04a6c7a6c 100644
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -2,8 +2,7 @@
import Vue from 'vue';
import bp from '../../../breakpoints';
-
-const ModalStore = gl.issueBoards.ModalStore;
+import ModalStore from '../../stores/modal_store';
gl.issueBoards.ModalList = Vue.extend({
props: {
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
index 4684ea76647..e644de2d4fc 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
-
-const ModalStore = gl.issueBoards.ModalStore;
+import ModalStore from '../../stores/modal_store';
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
data() {
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
index 3e5d08e3d75..b6465a88e5e 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
-
-const ModalStore = gl.issueBoards.ModalStore;
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalTabs = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
+ mixins: [modalMixin],
data() {
return ModalStore.store;
},
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index 09c683ff621..0a0820ec5fd 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -17,14 +17,10 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
type: Object,
required: true,
},
- issueUpdate: {
- type: String,
- required: true,
- },
},
computed: {
updateUrl() {
- return this.issueUpdate.replace(':project_path', this.issue.project.path);
+ return this.issue.path;
},
},
methods: {
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index fb40b9f5565..70367c4f711 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
super({
page: 'boards',
+ isGroupDecendent: true,
stateFiltersSelector: '.issues-state-filters',
});
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 8b1c14c04ff..a6f8681cfac 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -17,9 +17,9 @@ import './models/milestone';
import './models/project';
import './models/assignee';
import './stores/boards_store';
-import './stores/modal_store';
+import ModalStore from './stores/modal_store';
import BoardService from './services/board_service';
-import './mixins/modal_mixins';
+import modalMixin from './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
import './components/board';
@@ -31,7 +31,6 @@ import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/fi
export default () => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
- const ModalStore = gl.issueBoards.ModalStore;
window.gl = window.gl || {};
@@ -176,7 +175,7 @@ export default () => {
gl.IssueBoardsModalAddBtn = new Vue({
el: document.getElementById('js-add-issues-btn'),
- mixins: [gl.issueBoards.ModalMixins],
+ mixins: [modalMixin],
data() {
return {
modal: ModalStore.store,
diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js b/app/assets/javascripts/boards/mixins/modal_mixins.js
index 2b0a1aaa89f..6c97e1629bf 100644
--- a/app/assets/javascripts/boards/mixins/modal_mixins.js
+++ b/app/assets/javascripts/boards/mixins/modal_mixins.js
@@ -1,6 +1,6 @@
-const ModalStore = gl.issueBoards.ModalStore;
+import ModalStore from '../stores/modal_store';
-gl.issueBoards.ModalMixins = {
+export default {
methods: {
toggleModal(toggle) {
ModalStore.store.showAddIssuesModal = toggle;
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 4c5079efc8b..b381d48d625 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -23,6 +23,8 @@ class ListIssue {
};
this.isLoading = {};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
+ this.referencePath = obj.reference_path;
+ this.path = obj.real_path;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id;
@@ -98,7 +100,7 @@ class ListIssue {
this.isLoading[key] = value;
}
- update (url) {
+ update () {
const data = {
issue: {
milestone_id: this.milestone ? this.milestone.id : null,
@@ -113,7 +115,7 @@ class ListIssue {
}
const projectPath = this.project ? this.project.path : '';
- return Vue.http.patch(url.replace(':project_path', projectPath), data);
+ return Vue.http.patch(`${this.path}.json`, data);
}
}
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index d78d4701974..7c90597f77c 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -19,7 +19,7 @@ export default class BoardService {
}
static generateIssuePath(boardId, id) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
+ return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
}
all() {
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index 4fdc925c825..a4220cd840d 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -1,6 +1,3 @@
-window.gl = window.gl || {};
-window.gl.issueBoards = window.gl.issueBoards || {};
-
class ModalStore {
constructor() {
this.store = {
@@ -95,4 +92,4 @@ class ModalStore {
}
}
-gl.issueBoards.ModalStore = new ModalStore();
+export default new ModalStore();
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
index 839e369eaf6..f34496f84c6 100644
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ b/app/assets/javascripts/branches/branches_delete_modal.js
@@ -16,6 +16,7 @@ class DeleteModal {
bindEvents() {
this.$toggleBtns.on('click', this.setModalData.bind(this));
this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
+ this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this));
}
setModalData(e) {
@@ -30,6 +31,16 @@ class DeleteModal {
this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName);
}
+ setDisableDeleteButton(e) {
+ if (this.$deleteBtn.is('[disabled]')) {
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+
+ return true;
+ }
+
updateModal() {
this.$branchName.text(this.branchName);
this.$confirmInput.val('');
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index f8dcdf3f60a..9c12b89240c 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,96 +1,102 @@
<script>
- import _ from 'underscore';
- import { s__, sprintf } from '../../locale';
- import applicationRow from './application_row.vue';
- import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
- import {
- APPLICATION_INSTALLED,
- INGRESS,
- } from '../constants';
+import _ from 'underscore';
+import { s__, sprintf } from '../../locale';
+import applicationRow from './application_row.vue';
+import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import { APPLICATION_INSTALLED, INGRESS } from '../constants';
- export default {
- components: {
- applicationRow,
- clipboardButton,
+export default {
+ components: {
+ applicationRow,
+ clipboardButton,
+ },
+ props: {
+ applications: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
- props: {
- applications: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- helpPath: {
- type: String,
- required: false,
- default: '',
- },
- ingressHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- ingressDnsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- managePrometheusPath: {
- type: String,
- required: false,
- default: '',
- },
+ helpPath: {
+ type: String,
+ required: false,
+ default: '',
},
- computed: {
- generalApplicationDescription() {
- return sprintf(
- _.escape(s__(
+ ingressHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ ingressDnsHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ managePrometheusPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ generalApplicationDescription() {
+ return sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
- )), {
- helpLink: `<a href="${this.helpPath}">
+ ),
+ ),
+ {
+ helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
- },
- false,
- );
- },
- ingressId() {
- return INGRESS;
- },
- ingressInstalled() {
- return this.applications.ingress.status === APPLICATION_INSTALLED;
- },
- ingressExternalIp() {
- return this.applications.ingress.externalIp;
- },
- ingressDescription() {
- const extraCostParagraph = sprintf(
- _.escape(s__(
+ },
+ false,
+ );
+ },
+ ingressId() {
+ return INGRESS;
+ },
+ ingressInstalled() {
+ return this.applications.ingress.status === APPLICATION_INSTALLED;
+ },
+ ingressExternalIp() {
+ return this.applications.ingress.externalIp;
+ },
+ ingressDescription() {
+ const extraCostParagraph = sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
- the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
- you can %{pricingLink}.`,
- )), {
- boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
- pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
+ the hosting provider your Kubernetes cluster is installed on. If you are using
+ Google Kubernetes Engine, you can %{pricingLink}.`,
+ ),
+ ),
+ {
+ boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
+ pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`,
- },
- false,
- );
+ },
+ false,
+ );
- const externalIpParagraph = sprintf(
- _.escape(s__(
+ const externalIpParagraph = sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
- )), {
- ingressHelpLink: `<a href="${this.ingressHelpPath}">
+ ),
+ ),
+ {
+ ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
- },
- false,
- );
+ },
+ false,
+ );
- return `
+ return `
<p>
${extraCostParagraph}
</p>
@@ -98,22 +104,25 @@
${externalIpParagraph}
</p>
`;
- },
- prometheusDescription() {
- return sprintf(
- _.escape(s__(
+ },
+ prometheusDescription() {
+ return sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`,
- )), {
- gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
+ ),
+ ),
+ {
+ gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
- },
- false,
- );
- },
+ },
+ false,
+ );
},
- };
+ },
+};
</script>
<template>
@@ -205,7 +214,7 @@
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
- cluster or Quotas on GKE if it takes a long time.`) }}
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
<a
:href="ingressHelpPath"
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 466a5b5d635..24d63b99a29 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -55,22 +55,20 @@
},
methods: {
successCallback(resp) {
- return resp.json().then((response) => {
- // depending of the endpoint the response can either bring a `pipelines` key or not.
- const pipelines = response.pipelines || response;
- this.setCommonData(pipelines);
+ // depending of the endpoint the response can either bring a `pipelines` key or not.
+ const pipelines = resp.data.pipelines || resp.data;
+ this.setCommonData(pipelines);
- const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
- detail: {
- pipelines: response,
- },
- });
-
- // notifiy to update the count in tabs
- if (this.$el.parentElement) {
- this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
- }
+ const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
+ detail: {
+ pipelines: resp.data,
+ },
});
+
+ // notifiy to update the count in tabs
+ if (this.$el.parentElement) {
+ this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
+ }
},
},
};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
index c50ac667c20..2d5bae9a9c4 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -1,19 +1,19 @@
import $ from 'jquery';
-import _ from 'underscore';
import {
getSelector,
- togglePopover,
inserted,
- mouseenter,
- mouseleave,
} from './feature_highlight_helper';
+import {
+ togglePopover,
+ mouseenter,
+ debouncedMouseleave,
+} from '../shared/popover';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id));
const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = togglePopover.bind($selector, false);
- const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
$selector
// Setup popover
@@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
`,
})
.on('mouseenter', mouseenter)
- .on('mouseleave', debouncedMouseleave)
+ .on('mouseleave', debouncedMouseleave(debounceTimeout))
.on('inserted.bs.popover', inserted)
.on('show.bs.popover', () => {
- window.addEventListener('scroll', hideOnScroll);
- })
- .on('hide.bs.popover', () => {
- window.removeEventListener('scroll', hideOnScroll);
+ window.addEventListener('scroll', hideOnScroll, { once: true });
})
// Display feature highlight
.removeAttr('disabled');
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
index f480e72961c..d5b97ebb264 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
import Flash from '../flash';
import LazyLoader from '../lazy_loader';
+import { togglePopover } from '../shared/popover';
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
-export function togglePopover(show) {
- const isAlreadyShown = this.hasClass('js-popover-show');
- if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
- return false;
- }
- this.popover(show ? 'show' : 'hide');
- this.toggleClass('disable-animation js-popover-show', show);
-
- return true;
-}
-
export function dismiss(highlightId) {
axios.post(this.attr('data-dismiss-endpoint'), {
feature_name: highlightId,
@@ -27,23 +17,6 @@ export function dismiss(highlightId) {
this.hide();
}
-export function mouseleave() {
- if (!$('.popover:hover').length > 0) {
- const $featureHighlight = $(this);
- togglePopover.call($featureHighlight, false);
- }
-}
-
-export function mouseenter() {
- const $featureHighlight = $(this);
-
- const showedPopover = togglePopover.call($featureHighlight, true);
- if (showedPopover) {
- $('.popover')
- .on('mouseleave', mouseleave.bind($featureHighlight));
- }
-}
-
export function inserted() {
const popoverId = this.getAttribute('aria-describedby');
const highlightId = this.dataset.highlight;
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 e6390f0855b..d7e1de18d09 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager {
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page;
this.groupsOnly = isGroup;
- this.groupAncestor = isGroupAncestor;
- this.isGroupDecendent = isGroupDecendent;
+ this.includeAncestorGroups = isGroupAncestor;
+ this.includeDescendantGroups = isGroupDecendent;
this.setupMapping();
@@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager {
}
getLabelsEndpoint() {
- const endpoint = `${this.baseEndpoint}/labels.json`;
+ let endpoint = `${this.baseEndpoint}/labels.json?`;
+
+ if (this.groupsOnly) {
+ endpoint = `${endpoint}only_group_labels=true&`;
+ }
+
+ if (this.includeAncestorGroups) {
+ endpoint = `${endpoint}include_ancestor_groups=true&`;
+ }
+
+ if (this.includeDescendantGroups) {
+ endpoint = `${endpoint}include_descendant_groups=true`;
+ }
return endpoint;
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 71b7e80335b..cf5ba1e1771 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -21,7 +21,7 @@ export default class FilteredSearchManager {
constructor({
page,
isGroup = false,
- isGroupAncestor = false,
+ isGroupAncestor = true,
isGroupDecendent = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
@@ -86,6 +86,7 @@ export default class FilteredSearchManager {
page: this.page,
isGroup: this.isGroup,
isGroupAncestor: this.isGroupAncestor,
+ isGroupDecendent: this.isGroupDecendent,
filteredSearchTokenKeys: this.filteredSearchTokenKeys,
});
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index d22869466c9..1c237c0ec97 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -3,7 +3,6 @@ import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
-import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
@@ -12,7 +11,6 @@ export default {
ideSidebar,
ideContextbar,
repoTabs,
- repoFileButtons,
ideStatusBar,
repoEditor,
},
@@ -70,9 +68,6 @@ export default {
class="multi-file-edit-pane-content"
:file="activeFile"
/>
- <repo-file-buttons
- :file="activeFile"
- />
<ide-status-bar
:file="activeFile"
/>
diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue
new file mode 100644
index 00000000000..a6c6f46a144
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue
@@ -0,0 +1,84 @@
+<script>
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ showButtons() {
+ return (
+ this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink
+ );
+ },
+ rawDownloadButtonLabel() {
+ return this.file.binary ? __('Download') : __('Raw');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="showButtons"
+ class="pull-right ide-btn-group"
+ >
+ <a
+ v-tooltip
+ v-if="!file.binary"
+ :href="file.blamePath"
+ :title="__('Blame')"
+ class="btn btn-xs btn-transparent blame"
+ >
+ <icon
+ name="blame"
+ :size="16"
+ />
+ </a>
+ <a
+ v-tooltip
+ :href="file.commitsPath"
+ :title="__('History')"
+ class="btn btn-xs btn-transparent history"
+ >
+ <icon
+ name="history"
+ :size="16"
+ />
+ </a>
+ <a
+ v-tooltip
+ :href="file.permalink"
+ :title="__('Permalink')"
+ class="btn btn-xs btn-transparent permalink"
+ >
+ <icon
+ name="link"
+ :size="16"
+ />
+ </a>
+ <a
+ v-tooltip
+ :href="file.rawPath"
+ target="_blank"
+ class="btn btn-xs btn-transparent prepend-left-10 raw"
+ rel="noopener noreferrer"
+ :title="rawDownloadButtonLabel">
+ <icon
+ name="download"
+ :size="16"
+ />
+ </a>
+ </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 9c386896448..152a5f632ad 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,25 +1,23 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
- import timeAgoMixin from '~/vue_shared/mixins/timeago';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import timeAgoMixin from '~/vue_shared/mixins/timeago';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [timeAgoMixin],
+ props: {
+ file: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
- },
- mixins: [
- timeAgoMixin,
- ],
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- };
+ },
+};
</script>
<template>
@@ -50,7 +48,9 @@
<div class="text-right">
{{ file.eol }}
</div>
- <div class="text-right">
+ <div
+ class="text-right"
+ v-if="!file.binary">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div class="text-right">
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index b1a16350c19..711bafa17a9 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -2,10 +2,16 @@
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
+import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
+import IdeFileButtons from './ide_file_buttons.vue';
export default {
+ components: {
+ ContentViewer,
+ IdeFileButtons,
+ },
props: {
file: {
type: Object,
@@ -13,10 +19,20 @@ export default {
},
},
computed: {
- ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
+ ...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
...mapGetters(['currentMergeRequest']),
shouldHideEditor() {
- return this.file && this.file.binary && !this.file.raw;
+ return this.file && this.file.binary && !this.file.content;
+ },
+ editTabCSS() {
+ return {
+ active: this.file.viewMode === 'edit',
+ };
+ },
+ previewTabCSS() {
+ return {
+ active: this.file.viewMode === 'preview',
+ };
},
},
watch: {
@@ -26,15 +42,17 @@ export default {
this.initMonaco();
}
},
- leftPanelCollapsed() {
- this.editor.updateDimensions();
- },
rightPanelCollapsed() {
this.editor.updateDimensions();
},
viewer() {
this.createEditorInstance();
},
+ panelResizing() {
+ if (!this.panelResizing) {
+ this.editor.updateDimensions();
+ }
+ },
},
beforeDestroy() {
this.editor.dispose();
@@ -56,6 +74,7 @@ export default {
'changeFileContent',
'setFileLanguage',
'setEditorPosition',
+ 'setFileViewMode',
'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
@@ -152,16 +171,49 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
- <div
- v-if="shouldHideEditor"
- v-html="file.html"
- >
+ <div class="ide-mode-tabs clearfix">
+ <ul
+ class="nav-links pull-left"
+ v-if="!shouldHideEditor">
+ <li :class="editTabCSS">
+ <a
+ href="javascript:void(0);"
+ role="button"
+ @click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
+ <template v-if="viewer === 'editor'">
+ {{ __('Edit') }}
+ </template>
+ <template v-else>
+ {{ __('Review') }}
+ </template>
+ </a>
+ </li>
+ <li
+ v-if="file.previewMode"
+ :class="previewTabCSS">
+ <a
+ href="javascript:void(0);"
+ role="button"
+ @click.prevent="setFileViewMode({ file, viewMode:'preview' })">
+ {{ file.previewMode.previewTitle }}
+ </a>
+ </li>
+ </ul>
+ <ide-file-buttons
+ :file="file"
+ />
</div>
<div
- v-show="!shouldHideEditor"
+ v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
>
</div>
+ <content-viewer
+ v-if="shouldHideEditor || file.viewMode === 'preview'"
+ :content="file.content || file.raw"
+ :path="file.rawPath || file.path"
+ :file-size="file.size"
+ :project-path="file.projectId"/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue
deleted file mode 100644
index 4ea8cf7504b..00000000000
--- a/app/assets/javascripts/ide/components/repo_file_buttons.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-export default {
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- showButtons() {
- return this.file.rawPath ||
- this.file.blamePath ||
- this.file.commitsPath ||
- this.file.permalink;
- },
- rawDownloadButtonLabel() {
- return this.file.binary ? 'Download' : 'Raw';
- },
- },
-};
-</script>
-
-<template>
- <div
- v-if="showButtons"
- class="multi-file-editor-btn-group"
- >
- <a
- :href="file.rawPath"
- target="_blank"
- class="btn btn-default btn-sm raw"
- rel="noopener noreferrer">
- {{ rawDownloadButtonLabel }}
- </a>
-
- <div
- class="btn-group"
- role="group"
- aria-label="File actions"
- >
- <a
- :href="file.blamePath"
- class="btn btn-default btn-sm blame"
- >
- Blame
- </a>
- <a
- :href="file.commitsPath"
- class="btn btn-default btn-sm history"
- >
- History
- </a>
- <a
- :href="file.permalink"
- class="btn btn-default btn-sm permalink"
- >
- Permalink
- </a>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue
index faa690ecba0..5ea2a2f6825 100644
--- a/app/assets/javascripts/ide/components/resizable_panel.vue
+++ b/app/assets/javascripts/ide/components/resizable_panel.vue
@@ -1,67 +1,64 @@
<script>
- import { mapActions, mapState } from 'vuex';
- import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import { mapActions, mapState } from 'vuex';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
- export default {
- components: {
- PanelResizer,
+export default {
+ components: {
+ PanelResizer,
+ },
+ props: {
+ collapsible: {
+ type: Boolean,
+ required: true,
},
- props: {
- collapsible: {
- type: Boolean,
- required: true,
- },
- initialWidth: {
- type: Number,
- required: true,
- },
- minSize: {
- type: Number,
- required: false,
- default: 200,
- },
- side: {
- type: String,
- required: true,
- },
+ initialWidth: {
+ type: Number,
+ required: true,
},
- data() {
- return {
- width: this.initialWidth,
- };
+ minSize: {
+ type: Number,
+ required: false,
+ default: 340,
},
- computed: {
- ...mapState({
- collapsed(state) {
- return state[`${this.side}PanelCollapsed`];
- },
- }),
- panelStyle() {
- if (!this.collapsed) {
- return {
- width: `${this.width}px`,
- };
- }
-
- return {};
- },
+ side: {
+ type: String,
+ required: true,
},
- methods: {
- ...mapActions([
- 'setPanelCollapsedStatus',
- 'setResizingStatus',
- ]),
- toggleFullbarCollapsed() {
- if (this.collapsed && this.collapsible) {
- this.setPanelCollapsedStatus({
- side: this.side,
- collapsed: !this.collapsed,
- });
- }
+ },
+ data() {
+ return {
+ width: this.initialWidth,
+ };
+ },
+ computed: {
+ ...mapState({
+ collapsed(state) {
+ return state[`${this.side}PanelCollapsed`];
},
+ }),
+ panelStyle() {
+ if (!this.collapsed) {
+ return {
+ width: `${this.width}px`,
+ };
+ }
+
+ return {};
+ },
+ },
+ methods: {
+ ...mapActions(['setPanelCollapsedStatus', 'setResizingStatus']),
+ toggleFullbarCollapsed() {
+ if (this.collapsed && this.collapsible) {
+ this.setPanelCollapsedStatus({
+ side: this.side,
+ collapsed: !this.collapsed,
+ });
+ }
},
- maxSize: (window.innerWidth / 2),
- };
+ },
+ maxSize: window.innerWidth / 2,
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 20983666b4a..4a0a303d5a6 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -36,11 +36,11 @@ const router = new VueRouter({
base: `${gon.relative_url_root}/-/ide/`,
routes: [
{
- path: '/project/:namespace/:project',
+ path: '/project/:namespace/:project+',
component: EmptyRouterComponent,
children: [
{
- path: ':targetmode/:branch/*',
+ path: ':targetmode(edit|tree|blob)/:branch/*',
component: EmptyRouterComponent,
},
{
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 6b4ba30e086..001737d6ee8 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -69,6 +69,7 @@ export default class Editor {
occurrencesHighlight: false,
renderLineHighlight: 'none',
hideCursorInOverviewRuler: true,
+ renderSideBySide: Editor.renderSideBySide(domElement),
})),
);
@@ -81,7 +82,7 @@ export default class Editor {
}
attachModel(model) {
- if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
+ if (this.isDiffEditorType) {
this.instance.setModel({
original: model.getOriginalModel(),
modified: model.getModel(),
@@ -153,6 +154,7 @@ export default class Editor {
updateDimensions() {
this.instance.layout();
+ this.updateDiffView();
}
setPosition({ lineNumber, column }) {
@@ -171,4 +173,20 @@ export default class Editor {
this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
}
+
+ updateDiffView() {
+ if (!this.isDiffEditorType) return;
+
+ this.instance.updateOptions({
+ renderSideBySide: Editor.renderSideBySide(this.instance.getDomNode()),
+ });
+ }
+
+ get isDiffEditorType() {
+ return this.instance.getEditorType() === 'vs.editor.IDiffEditor';
+ }
+
+ static renderSideBySide(domElement) {
+ return domElement.offsetWidth >= 700;
+ }
}
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index a213862f9b3..9f895d49f2e 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -6,7 +6,7 @@ export const defaultEditorOptions = {
minimap: {
enabled: false,
},
- wordWrap: 'bounded',
+ wordWrap: 'on',
};
export default [
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 6b034ea1e82..1a17320a1ea 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -149,6 +149,10 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn
}
};
+export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
+ commit(types.SET_FILE_VIEWMODE, { file, viewMode });
+};
+
export const discardFileChanges = ({ state, commit }, path) => {
const file = state.entries[path];
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index f536ce6344b..367c45f7e2d 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -37,9 +37,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
const commitMsg = sprintf(
__('Your changes have been committed. Commit %{commitId} %{commitStats}'),
{
- commitId: `<a href="${currentProject.web_url}/commit/${
+ commitId: `<a href="${currentProject.web_url}/commit/${data.short_id}" class="commit-sha">${
data.short_id
- }" class="commit-sha">${data.short_id}</a>`,
+ }</a>`,
commitStats,
},
false,
@@ -54,9 +54,7 @@ export const checkCommitStatus = ({ rootState }) =>
.then(({ data }) => {
const { id } = data.commit;
const selectedBranch =
- rootState.projects[rootState.currentProjectId].branches[
- rootState.currentBranchId
- ];
+ rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId];
if (selectedBranch.workingReference !== id) {
return true;
@@ -135,32 +133,15 @@ export const updateFilesAfterCommit = (
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
router.push(
- `/project/${rootState.currentProjectId}/blob/${branch}/${
- rootGetters.activeFile.path
- }`,
+ `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
);
}
-
- dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH);
};
-export const commitChanges = ({
- commit,
- state,
- getters,
- dispatch,
- rootState,
-}) => {
+export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
- const payload = createCommitPayload(
- getters.branchName,
- newBranch,
- state,
- rootState,
- );
- const getCommitStatus = newBranch
- ? Promise.resolve(false)
- : dispatch('checkCommitStatus');
+ const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
+ const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
commit(types.UPDATE_LOADING, true);
@@ -182,28 +163,29 @@ export const commitChanges = ({
if (!data.short_id) {
flash(data.message, 'alert', document, null, false, true);
- return;
+ return null;
}
dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
-
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
- dispatch(
- 'redirectToUrl',
- createNewMergeRequestUrl(
- rootState.projects[rootState.currentProjectId].web_url,
- getters.branchName,
- rootState.currentBranchId,
- ),
- { root: true },
- );
- } else {
- dispatch('updateFilesAfterCommit', {
- data,
- branch: getters.branchName,
- });
- }
+ return dispatch('updateFilesAfterCommit', {
+ data,
+ branch: getters.branchName,
+ })
+ .then(() => {
+ if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
+ dispatch(
+ 'redirectToUrl',
+ createNewMergeRequestUrl(
+ rootState.projects[rootState.currentProjectId].web_url,
+ getters.branchName,
+ rootState.currentBranchId,
+ ),
+ { root: true },
+ );
+ }
+ })
+ .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
})
.catch(err => {
let errMsg = __('Error committing changes. Please try again.');
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index ee759bff516..e3f504e5ab0 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -38,6 +38,7 @@ export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
+export const SET_FILE_VIEWMODE = 'SET_FILE_VIEWMODE';
export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 926b6f66d78..eeb14b5490c 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -42,6 +42,8 @@ export default {
renderError: data.render_error,
raw: null,
baseRaw: null,
+ html: data.html,
+ size: data.size,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -83,6 +85,11 @@ export default {
mrChange,
});
},
+ [types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
+ Object.assign(state.entries[file.path], {
+ viewMode,
+ });
+ },
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 7f7e470c9bb..1176c040fb9 100644
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -17,12 +17,8 @@ export default {
});
},
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
- Object.assign(state, {
- trees: Object.assign(state.trees, {
- [treePath]: {
- tree: data,
- },
- }),
+ Object.assign(state.trees[treePath], {
+ tree: data,
});
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 63e4de3b17d..05a019de54f 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -38,6 +38,9 @@ export const dataStructure = () => ({
editorColumn: 1,
fileLanguage: '',
eol: '',
+ viewMode: 'edit',
+ previewMode: null,
+ size: 0,
});
export const decorateData = entity => {
@@ -57,8 +60,9 @@ export const decorateData = entity => {
changed = false,
parentTreeUrl = '',
base64 = false,
-
+ previewMode,
file_lock,
+ html,
} = entity;
return {
@@ -79,8 +83,9 @@ export const decorateData = entity => {
renderError,
content,
base64,
-
+ previewMode,
file_lock,
+ html,
};
};
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index a4cd1ab099f..a1673276900 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -1,14 +1,8 @@
+import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../utils';
self.addEventListener('message', e => {
- const {
- data,
- projectId,
- branchId,
- tempFile = false,
- content = '',
- base64 = false,
- } = e.data;
+ const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
const treeList = [];
let file;
@@ -19,9 +13,7 @@ self.addEventListener('message', e => {
if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName) => {
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
- const folderPath = `${
- parentFolder ? `${parentFolder.path}/` : ''
- }${folderName}`;
+ const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`;
const foundEntry = acc[folderPath];
if (!foundEntry) {
@@ -33,9 +25,7 @@ self.addEventListener('message', e => {
path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}/`,
type: 'tree',
- parentTreeUrl: parentFolder
- ? parentFolder.url
- : `/${projectId}/tree/${branchId}/`,
+ parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
changed: tempFile,
opened: tempFile,
@@ -70,13 +60,12 @@ self.addEventListener('message', e => {
path,
url: `/${projectId}/blob/${branchId}/${path}`,
type: 'blob',
- parentTreeUrl: fileFolder
- ? fileFolder.url
- : `/${projectId}/blob/${branchId}`,
+ parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
changed: tempFile,
content,
base64,
+ previewMode: viewerInformationForPath(blobName),
});
Object.assign(acc, {
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 172de6b3679..af47056d98f 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -45,7 +45,7 @@
return `#${this.job.runner.id}`;
},
hasTimeout() {
- return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
+ return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 824d3f7ca09..d0050abb8e9 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -10,6 +10,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
import flash from './flash';
+import ModalStore from './boards/stores/modal_store';
export default class LabelsSelect {
constructor(els, options = {}) {
@@ -350,7 +351,7 @@ export default class LabelsSelect {
}
if ($dropdown.closest('.add-issues-modal').length) {
- boardsModel = gl.issueBoards.ModalStore.store.filter;
+ boardsModel = ModalStore.store.filter;
}
if (boardsModel) {
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index de65ea15a60..914de9de940 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -1,7 +1,12 @@
-/* eslint-disable import/prefer-default-export */
+import $ from 'jquery';
+import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie } from './common_utils';
+
+const isVueMRDiscussions = () => isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
export const addClassIfElementExists = (element, className) => {
if (element) {
element.classList.add(className);
}
};
+
+export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 94d03621bff..b54ecd2d543 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -7,7 +7,8 @@
* @param {String} text
* @returns {String}
*/
-export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
+export const addDelimiter = text =>
+ (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
/**
* Returns '99+' for numbers bigger than 99.
@@ -22,7 +23,8 @@ export const highCountTrim = count => (count > 99 ? '99+' : count);
* @param {String} string
* @requires {String}
*/
-export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
+export const humanize = string =>
+ string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
/**
* Adds an 's' to the end of the string when count is bigger than 0
@@ -53,7 +55,7 @@ export const slugify = str => str.trim().toLowerCase();
* @param {Number} maxLength
* @returns {String}
*/
-export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
+export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`;
/**
* Capitalizes first character
@@ -80,3 +82,15 @@ export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, re
* @param {*} string
*/
export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase());
+
+/**
+ * Converts a sentence to lower case from the second word onwards
+ * e.g. Hello World => Hello world
+ *
+ * @param {*} string
+ */
+export const convertToSentenceCase = string => {
+ const splitWord = string.split(' ').map((word, index) => (index > 0 ? word.toLowerCase() : word));
+
+ return splitWord.join(' ');
+};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index e77318fef46..3f84f4b9499 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -7,11 +7,7 @@ import flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
-import {
- parseUrlPathname,
- handleLocationHash,
- isMetaClick,
-} from './lib/utils/common_utils';
+import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
@@ -69,11 +65,10 @@ import Notes from './notes';
let location = window.location;
export default class MergeRequestTabs {
-
constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab');
- const peek = document.getElementById('peek');
+ const peek = document.getElementById('js-peek');
const paddingTop = 16;
this.diffsLoaded = false;
@@ -109,8 +104,7 @@ export default class MergeRequestTabs {
.on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.on('click', '.js-show-tab', this.showTab);
- $('.merge-request-tabs a[data-toggle="tab"]')
- .on('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tab"]').on('click', this.clickTab);
}
// Used in tests
@@ -119,8 +113,7 @@ export default class MergeRequestTabs {
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.off('click', '.js-show-tab', this.showTab);
- $('.merge-request-tabs a[data-toggle="tab"]')
- .off('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tab"]').off('click', this.clickTab);
}
destroyPipelinesView() {
@@ -183,10 +176,7 @@ export default class MergeRequestTabs {
scrollToElement(container) {
if (location.hash) {
- const offset = 0 - (
- $('.navbar-gitlab').outerHeight() +
- $('.js-tabs-affix').outerHeight()
- );
+ const offset = 0 - ($('.navbar-gitlab').outerHeight() + $('.js-tabs-affix').outerHeight());
const $el = $(`${container} ${location.hash}:not(.match)`);
if ($el.length) {
$.scrollTo($el[0], { offset });
@@ -240,9 +230,13 @@ export default class MergeRequestTabs {
// Turbolinks' history.
//
// See https://github.com/rails/turbolinks/issues/363
- window.history.replaceState({
- url: newState,
- }, document.title, newState);
+ window.history.replaceState(
+ {
+ url: newState,
+ },
+ document.title,
+ newState,
+ );
return newState;
}
@@ -258,7 +252,8 @@ export default class MergeRequestTabs {
this.toggleLoading(true);
- axios.get(`${source}.json`)
+ axios
+ .get(`${source}.json`)
.then(({ data }) => {
document.querySelector('div#commits').innerHTML = data.html;
localTimeAgo($('.js-timeago', 'div#commits'));
@@ -303,7 +298,8 @@ export default class MergeRequestTabs {
this.toggleLoading(true);
- axios.get(`${urlPathname}.json${location.search}`)
+ axios
+ .get(`${urlPathname}.json${location.search}`)
.then(({ data }) => {
const $container = $('#diffs');
$container.html(data.html);
@@ -332,8 +328,7 @@ export default class MergeRequestTabs {
cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
- })
- .init();
+ }).init();
});
// Scroll any linked note into view
@@ -388,8 +383,7 @@ export default class MergeRequestTabs {
resetViewContainer() {
if (this.fixedLayoutPref !== null) {
- $('.content-wrapper .container-fluid')
- .toggleClass('container-limited', this.fixedLayoutPref);
+ $('.content-wrapper .container-fluid').toggleClass('container-limited', this.fixedLayoutPref);
}
}
@@ -438,12 +432,11 @@ export default class MergeRequestTabs {
const $diffTabs = $('#diff-notes-app');
- $tabs.off('affix.bs.affix affix-top.bs.affix')
+ $tabs
+ .off('affix.bs.affix affix-top.bs.affix')
.affix({
offset: {
- top: () => (
- $diffTabs.offset().top - $tabs.height() - $fixedNav.height()
- ),
+ top: () => $diffTabs.offset().top - $tabs.height() - $fixedNav.height(),
},
})
.on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index e6e3a66aa20..325fa570f37 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
export default class Milestone {
constructor() {
@@ -43,4 +44,25 @@ export default class Milestone {
.catch(() => flash('Error loading milestone tab'));
}
}
+
+ static initDeprecationMessage() {
+ const deprecationMesssageContainer = document.querySelector('.js-milestone-deprecation-message');
+
+ if (!deprecationMesssageContainer) return;
+
+ const deprecationMessage = deprecationMesssageContainer.querySelector('.js-milestone-deprecation-message-template').innerHTML;
+ const $popover = $('.js-popover-link', deprecationMesssageContainer);
+ const hideOnScroll = togglePopover.bind($popover, false);
+
+ $popover.popover({
+ content: deprecationMessage,
+ html: true,
+ placement: 'bottom',
+ })
+ .on('mouseenter', mouseenter)
+ .on('mouseleave', debouncedMouseleave())
+ .on('show.bs.popover', () => {
+ window.addEventListener('scroll', hideOnScroll, { once: true });
+ });
+ }
}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index add07c156a4..d0a2b27b0e6 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -6,6 +6,7 @@ import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
+import ModalStore from './boards/stores/modal_store';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
@@ -94,10 +95,10 @@ export default class MilestoneSelect {
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
- $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
+ $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`).addClass('is-active');
}),
renderRow: milestone => `
- <li data-milestone-id="${milestone.name}">
+ <li data-milestone-id="${_.escape(milestone.name)}">
<a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)}
</a>
@@ -125,7 +126,6 @@ export default class MilestoneSelect {
return milestone.id;
}
},
- isSelected: milestone => milestone.name === selectedMilestone,
hidden: () => {
$selectBox.hide();
// display:block overrides the hide-collapse rule
@@ -137,7 +137,7 @@ export default class MilestoneSelect {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
- $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
+ $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: (clickEvent) => {
@@ -158,13 +158,14 @@ export default class MilestoneSelect {
const isMRIndex = (page === page && page === 'projects:merge_requests:index');
const isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
+
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
}
if ($dropdown.closest('.add-issues-modal').length) {
- boardsStore = gl.issueBoards.ModalStore.store.filter;
+ boardsStore = ModalStore.store.filter;
}
if (boardsStore) {
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 04d546fafa0..f93b1da4f58 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -1,8 +1,10 @@
<script>
import { scaleLinear, scaleTime } from 'd3-scale';
import { axisLeft, axisBottom } from 'd3-axis';
+import _ from 'underscore';
import { max, extent } from 'd3-array';
import { select } from 'd3-selection';
+import GraphAxis from './graph/axis.vue';
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
@@ -18,10 +20,11 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }
export default {
components: {
- GraphLegend,
+ GraphAxis,
GraphFlag,
GraphDeployment,
GraphPath,
+ GraphLegend,
},
mixins: [MonitoringMixin],
props: {
@@ -138,7 +141,7 @@ export default {
this.legendTitle = query.label || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
- this.baseGraphHeight = this.graphHeight;
+ this.baseGraphHeight = this.graphHeight - 50;
this.baseGraphWidth = this.graphWidth;
// pixel offsets inside the svg and outside are not 1:1
@@ -177,10 +180,8 @@ export default {
this.graphHeightOffset,
);
- if (!this.showLegend) {
- this.baseGraphHeight -= 50;
- } else if (this.timeSeries.length > 3) {
- this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
+ if (_.findWhere(this.timeSeries, { renderCanary: true })) {
+ this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
}
const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
@@ -251,17 +252,13 @@ export default {
class="y-axis"
transform="translate(70, 20)"
/>
- <graph-legend
+ <graph-axis
:graph-width="graphWidth"
:graph-height="graphHeight"
:margin="margin"
:measurements="measurements"
- :legend-title="legendTitle"
:y-axis-label="yAxisLabel"
- :time-series="timeSeries"
:unit-of-display="unitOfDisplay"
- :current-data-index="currentDataIndex"
- :show-legend-group="showLegend"
/>
<svg
class="graph-data"
@@ -306,5 +303,10 @@ export default {
:deployment-flag-data="deploymentFlagData"
/>
</div>
+ <graph-legend
+ v-if="showLegend"
+ :legend-title="legendTitle"
+ :time-series="timeSeries"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue
new file mode 100644
index 00000000000..fc4b3689dfd
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph/axis.vue
@@ -0,0 +1,142 @@
+<script>
+import { convertToSentenceCase } from '~/lib/utils/text_utility';
+import { s__ } from '~/locale';
+
+export default {
+ props: {
+ graphWidth: {
+ type: Number,
+ required: true,
+ },
+ graphHeight: {
+ type: Number,
+ required: true,
+ },
+ margin: {
+ type: Object,
+ required: true,
+ },
+ measurements: {
+ type: Object,
+ required: true,
+ },
+ yAxisLabel: {
+ type: String,
+ required: true,
+ },
+ unitOfDisplay: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ yLabelWidth: 0,
+ yLabelHeight: 0,
+ };
+ },
+ computed: {
+ textTransform() {
+ const yCoordinate =
+ (this.graphHeight -
+ this.margin.top +
+ this.measurements.axisLabelLineOffset) /
+ 2 || 0;
+
+ return `translate(15, ${yCoordinate}) rotate(-90)`;
+ },
+
+ rectTransform() {
+ const yCoordinate =
+ (this.graphHeight -
+ this.margin.top +
+ this.measurements.axisLabelLineOffset) /
+ 2 +
+ this.yLabelWidth / 2 || 0;
+
+ return `translate(0, ${yCoordinate}) rotate(-90)`;
+ },
+
+ xPosition() {
+ return (
+ (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 -
+ this.margin.right || 0
+ );
+ },
+
+ yPosition() {
+ return (
+ this.graphHeight -
+ this.margin.top +
+ this.measurements.axisLabelLineOffset || 0
+ );
+ },
+
+ yAxisLabelSentenceCase() {
+ return `${convertToSentenceCase(this.yAxisLabel)} (${this.unitOfDisplay})`;
+ },
+
+ timeString() {
+ return s__('PrometheusDashboard|Time');
+ },
+ },
+ mounted() {
+ this.$nextTick(() => {
+ const bbox = this.$refs.ylabel.getBBox();
+ this.yLabelWidth = bbox.width + 10; // Added some padding
+ this.yLabelHeight = bbox.height + 5;
+ });
+ },
+};
+</script>
+<template>
+ <g class="axis-label-container">
+ <line
+ class="label-x-axis-line"
+ stroke="#000000"
+ stroke-width="1"
+ x1="10"
+ :y1="yPosition"
+ :x2="graphWidth + 20"
+ :y2="yPosition"
+ />
+ <line
+ class="label-y-axis-line"
+ stroke="#000000"
+ stroke-width="1"
+ x1="10"
+ y1="0"
+ :x2="10"
+ :y2="yPosition"
+ />
+ <rect
+ class="rect-axis-text"
+ :transform="rectTransform"
+ :width="yLabelWidth"
+ :height="yLabelHeight"
+ />
+ <text
+ class="label-axis-text y-label-text"
+ text-anchor="middle"
+ :transform="textTransform"
+ ref="ylabel"
+ >
+ {{ yAxisLabelSentenceCase }}
+ </text>
+ <rect
+ class="rect-axis-text"
+ :x="xPosition + 60"
+ :y="graphHeight - 80"
+ width="35"
+ height="50"
+ />
+ <text
+ class="label-axis-text x-label-text"
+ :x="xPosition + 60"
+ :y="yPosition"
+ dy=".35em"
+ >
+ {{ timeString }}
+ </text>
+ </g>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index 906c7c51f52..b8202e25685 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -1,11 +1,13 @@
<script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
-import icon from '../../../vue_shared/components/icon.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
+import TrackLine from './track_line.vue';
export default {
components: {
- icon,
+ Icon,
+ TrackLine,
},
props: {
currentXCoordinate: {
@@ -107,11 +109,6 @@ export default {
}
return `series ${index + 1}`;
},
- strokeDashArray(type) {
- if (type === 'dashed') return '6, 3';
- if (type === 'dotted') return '3, 3';
- return null;
- },
},
};
</script>
@@ -160,28 +157,13 @@ export default {
</div>
</div>
<div class="popover-content">
- <table>
+ <table class="prometheus-table">
<tr
v-for="(series, index) in timeSeries"
:key="index"
>
- <td>
- <svg
- width="15"
- height="6"
- >
- <line
- :stroke="series.lineColor"
- :stroke-dasharray="strokeDashArray(series.lineStyle)"
- stroke-width="4"
- x1="0"
- x2="15"
- y1="2"
- y2="2"
- />
- </svg>
- </td>
- <td>{{ seriesMetricLabel(index, series) }}</td>
+ <track-line :track="series"/>
+ <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
<td>
<strong>{{ seriesMetricValue(series) }}</strong>
</td>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index a7a058a9203..da9280cf1f1 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -1,204 +1,72 @@
<script>
-import { formatRelevantDigits } from '../../../lib/utils/number_utils';
+import TrackLine from './track_line.vue';
+import TrackInfo from './track_info.vue';
export default {
+ components: {
+ TrackLine,
+ TrackInfo,
+ },
props: {
- graphWidth: {
- type: Number,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- margin: {
- type: Object,
- required: true,
- },
- measurements: {
- type: Object,
- required: true,
- },
legendTitle: {
type: String,
required: true,
},
- yAxisLabel: {
- type: String,
- required: true,
- },
timeSeries: {
type: Array,
required: true,
},
- unitOfDisplay: {
- type: String,
- required: true,
- },
- currentDataIndex: {
- type: Number,
- required: true,
- },
- showLegendGroup: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- data() {
- return {
- yLabelWidth: 0,
- yLabelHeight: 0,
- seriesXPosition: 0,
- metricUsageXPosition: 0,
- };
- },
- computed: {
- textTransform() {
- const yCoordinate =
- (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
-
- return `translate(15, ${yCoordinate}) rotate(-90)`;
- },
- rectTransform() {
- const yCoordinate =
- (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
- this.yLabelWidth / 2 || 0;
-
- return `translate(0, ${yCoordinate}) rotate(-90)`;
- },
- xPosition() {
- return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
- },
- yPosition() {
- return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
- },
- },
- mounted() {
- this.$nextTick(() => {
- const bbox = this.$refs.ylabel.getBBox();
- this.metricUsageXPosition = 0;
- this.seriesXPosition = 0;
- if (this.$refs.legendTitleSvg != null) {
- this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
- }
- if (this.$refs.seriesTitleSvg != null) {
- this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
- }
- this.yLabelWidth = bbox.width + 10; // Added some padding
- this.yLabelHeight = bbox.height + 5;
- });
},
methods: {
- translateLegendGroup(index) {
- return `translate(0, ${12 * index})`;
- },
- formatMetricUsage(series) {
- const value =
- series.values[this.currentDataIndex] && series.values[this.currentDataIndex].value;
- if (isNaN(value)) {
- return '-';
- }
- return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
- },
- createSeriesString(index, series) {
- if (series.metricTag) {
- return `${series.metricTag} ${this.formatMetricUsage(series)}`;
- }
- return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
- },
- strokeDashArray(type) {
- if (type === 'dashed') return '6, 3';
- if (type === 'dotted') return '3, 3';
- return null;
+ isStable(track) {
+ return {
+ 'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary,
+ };
},
},
};
</script>
<template>
- <g class="axis-label-container">
- <line
- class="label-x-axis-line"
- stroke="#000000"
- stroke-width="1"
- x1="10"
- :y1="yPosition"
- :x2="graphWidth + 20"
- :y2="yPosition"
- />
- <line
- class="label-y-axis-line"
- stroke="#000000"
- stroke-width="1"
- x1="10"
- y1="0"
- :x2="10"
- :y2="yPosition"
- />
- <rect
- class="rect-axis-text"
- :transform="rectTransform"
- :width="yLabelWidth"
- :height="yLabelHeight"
- />
- <text
- class="label-axis-text y-label-text"
- text-anchor="middle"
- :transform="textTransform"
- ref="ylabel"
- >
- {{ yAxisLabel }}
- </text>
- <rect
- class="rect-axis-text"
- :x="xPosition + 60"
- :y="graphHeight - 80"
- width="35"
- height="50"
- />
- <text
- class="label-axis-text x-label-text"
- :x="xPosition + 60"
- :y="yPosition"
- dy=".35em"
- >
- Time
- </text>
- <template v-if="showLegendGroup">
- <g
- class="legend-group"
+ <div class="prometheus-graph-legends prepend-left-10">
+ <table class="prometheus-table">
+ <tr
v-for="(series, index) in timeSeries"
:key="index"
- :transform="translateLegendGroup(index)"
+ v-if="series.shouldRenderLegend"
+ :class="isStable(series)"
>
- <line
- :stroke="series.lineColor"
- :stroke-width="measurements.legends.height"
- :stroke-dasharray="strokeDashArray(series.lineStyle)"
- :x1="measurements.legends.offsetX"
- :x2="measurements.legends.offsetX + measurements.legends.width"
- :y1="graphHeight - measurements.legends.offsetY"
- :y2="graphHeight - measurements.legends.offsetY"
- />
- <text
- v-if="timeSeries.length > 1"
- class="legend-metric-title"
- ref="legendTitleSvg"
- x="38"
- :y="graphHeight - 30"
- >
- {{ createSeriesString(index, series) }}
- </text>
- <text
- v-else
+ <td>
+ <strong v-if="series.renderCanary">{{ series.trackName }}</strong>
+ </td>
+ <track-line :track="series" />
+ <td
class="legend-metric-title"
- ref="legendTitleSvg"
- x="38"
- :y="graphHeight - 30"
- >
- {{ legendTitle }} {{ formatMetricUsage(series) }}
- </text>
- </g>
- </template>
- </g>
+ v-if="timeSeries.length > 1">
+ <track-info
+ :track="series"
+ v-if="series.metricTag" />
+ <track-info
+ v-else
+ :track="series">
+ <strong>{{ legendTitle }}</strong> series {{ index + 1 }}
+ </track-info>
+ </td>
+ <td v-else>
+ <track-info :track="series">
+ <strong>{{ legendTitle }}</strong>
+ </track-info>
+ </td>
+ <template v-for="(track, trackIndex) in series.tracksLegend">
+ <track-line
+ :track="track"
+ :key="`track-line-${trackIndex}`"/>
+ <td :key="`track-info-${trackIndex}`">
+ <track-info
+ class="legend-metric-title"
+ :track="track" />
+ </td>
+ </template>
+ </tr>
+ </table>
+ </div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_info.vue b/app/assets/javascripts/monitoring/components/graph/track_info.vue
new file mode 100644
index 00000000000..ec1c2222af9
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph/track_info.vue
@@ -0,0 +1,29 @@
+<script>
+import { formatRelevantDigits } from '~/lib/utils/number_utils';
+
+export default {
+ name: 'TrackInfo',
+ props: {
+ track: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ summaryMetrics() {
+ return `Avg: ${formatRelevantDigits(this.track.average)} · Max: ${formatRelevantDigits(
+ this.track.max,
+ )}`;
+ },
+ },
+};
+</script>
+<template>
+ <span>
+ <slot>
+ <strong> {{ track.metricTag }} </strong>
+ </slot>
+ {{ summaryMetrics }}
+ </span>
+</template>
+
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
new file mode 100644
index 00000000000..79b322e2e42
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue
@@ -0,0 +1,36 @@
+<script>
+export default {
+ name: 'TrackLine',
+ props: {
+ track: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ stylizedLine() {
+ if (this.track.lineStyle === 'dashed') return '6, 3';
+ if (this.track.lineStyle === 'dotted') return '3, 3';
+ return null;
+ },
+ },
+};
+</script>
+<template>
+ <td>
+ <svg
+ width="15"
+ height="6">
+ <line
+ :stroke-dasharray="stylizedLine"
+ :stroke="track.lineColor"
+ stroke-width="4"
+ :x1="0"
+ :x2="15"
+ :y1="2"
+ :y2="2"
+ />
+ </svg>
+ </td>
+</template>
+
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 854636e9a89..535c415cd6d 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -1,7 +1,7 @@
import _ from 'underscore';
function sortMetrics(metrics) {
- return _.chain(metrics).sortBy('weight').sortBy('title').value();
+ return _.chain(metrics).sortBy('title').sortBy('weight').value();
}
function normalizeMetrics(metrics) {
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index b5b8e3c255d..8a93c7e6bae 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -1,10 +1,21 @@
import _ from 'underscore';
import { scaleLinear, scaleTime } from 'd3-scale';
import { line, area, curveLinear } from 'd3-shape';
-import { extent, max } from 'd3-array';
+import { extent, max, sum } from 'd3-array';
import { timeMinute } from 'd3-time';
-
-const d3 = { scaleLinear, scaleTime, line, area, curveLinear, extent, max, timeMinute };
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+
+const d3 = {
+ scaleLinear,
+ scaleTime,
+ line,
+ area,
+ curveLinear,
+ extent,
+ max,
+ timeMinute,
+ sum,
+};
const defaultColorPalette = {
blue: ['#1f78d1', '#8fbce8'],
@@ -20,6 +31,8 @@ const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
let usedColors = [];
+ let renderCanary = false;
+ const timeSeriesParsed = [];
function pickColor(name) {
let pick;
@@ -38,16 +51,23 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
return defaultColorPalette[pick];
}
- return query.result.map((timeSeries, timeSeriesNumber) => {
+ query.result.forEach((timeSeries, timeSeriesNumber) => {
let metricTag = '';
let lineColor = '';
let areaColor = '';
+ let shouldRenderLegend = true;
+ const timeSeriesValues = timeSeries.values.map(d => d.value);
+ const maximumValue = d3.max(timeSeriesValues);
+ const accum = d3.sum(timeSeriesValues);
+ const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable');
+
+ if (trackName === 'Canary') {
+ renderCanary = true;
+ }
- const timeSeriesScaleX = d3.scaleTime()
- .range([0, graphWidth - 70]);
+ const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
- const timeSeriesScaleY = d3.scaleLinear()
- .range([graphHeight - graphHeightOffset, 0]);
+ const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(xDom);
timeSeriesScaleX.ticks(d3.timeMinute, 60);
@@ -55,13 +75,15 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
const defined = d => !isNaN(d.value) && d.value != null;
- const lineFunction = d3.line()
+ 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()
+ const areaFunction = d3
+ .area()
.defined(defined)
.curve(d3.curveLinear)
.x(d => timeSeriesScaleX(d.time))
@@ -69,38 +91,62 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
.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 });
+ const seriesCustomizationData =
+ query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
if (seriesCustomizationData) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
+ shouldRenderLegend = false;
} else {
metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
[lineColor, areaColor] = pickColor();
+ if (timeSeriesParsed.length > 1) {
+ shouldRenderLegend = false;
+ }
}
- if (query.track) {
- metricTag += ` - ${query.track}`;
+ if (!shouldRenderLegend) {
+ if (!timeSeriesParsed[0].tracksLegend) {
+ timeSeriesParsed[0].tracksLegend = [];
+ }
+ timeSeriesParsed[0].tracksLegend.push({
+ max: maximumValue,
+ average: accum / timeSeries.values.length,
+ lineStyle,
+ lineColor,
+ metricTag,
+ });
}
- return {
+ timeSeriesParsed.push({
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
values: timeSeries.values,
+ max: maximumValue,
+ average: accum / timeSeries.values.length,
lineStyle,
lineColor,
areaColor,
metricTag,
- };
+ trackName,
+ shouldRenderLegend,
+ renderCanary,
+ });
});
+
+ return timeSeriesParsed;
}
export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
- const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat(
- query.result.reduce((allResults, result) => allResults.concat(result.values), []),
- ), []);
+ const allValues = queries.reduce(
+ (allQueryResults, query) =>
+ allQueryResults.concat(
+ query.result.reduce((allResults, result) => allResults.concat(result.values), []),
+ ),
+ [],
+ );
const xDom = d3.extent(allValues, d => d.time);
const yDom = [0, d3.max(allValues.map(d => d.value))];
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index 096c4ef5f31..e3c5bf06b3d 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -13,8 +13,11 @@ export default function initMrNotes() {
data() {
const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset;
+ const noteableData = JSON.parse(notesDataset.noteableData);
+ noteableData.noteableType = notesDataset.noteableType;
+
return {
- noteableData: JSON.parse(notesDataset.noteableData),
+ noteableData,
currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: JSON.parse(notesDataset.notesData),
};
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index b0573510ff9..2121907dff0 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -19,7 +19,6 @@ 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 { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
@@ -198,6 +197,8 @@ export default class Notes {
);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
+ 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);
// when issue status changes, we need to refresh data
@@ -244,6 +245,7 @@ export default class Notes {
this.$wrapperEl.off('click', '.js-comment-resolve-button');
this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
this.$wrapperEl.off('click', '.js-toggle-lazy-diff');
+ this.$wrapperEl.off('click', '.js-toggle-lazy-diff-retry-button');
this.$wrapperEl.off('ajax:success', '.js-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
@@ -1431,16 +1433,15 @@ export default class Notes {
syntaxHighlight(fileHolder);
}
- static renderDiffError($container) {
- $container.find('.line_content').html(
- $(`
- <div class="nothing-here-block">
- ${__(
- 'Unable to load the diff.',
- )} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
- </div>
- `),
- );
+ onClickRetryLazyLoad(e) {
+ const $retryButton = $(e.currentTarget);
+
+ $retryButton.prop('disabled', true);
+
+ return this.loadLazyDiff(e)
+ .then(() => {
+ $retryButton.prop('disabled', false);
+ });
}
loadLazyDiff(e) {
@@ -1449,20 +1450,35 @@ export default class Notes {
$container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff');
- const tableEl = $container.find('tbody');
- if (tableEl.length === 0) return;
+ const $tableEl = $container.find('tbody');
+ if ($tableEl.length === 0) return;
const fileHolder = $container.find('.file-holder');
const url = fileHolder.data('linesPath');
- axios
+ const $errorContainer = $container.find('.js-error-lazy-load-diff');
+ const $successContainer = $container.find('.js-success-lazy-load');
+
+ /**
+ * We only fetch resolved discussions.
+ * Unresolved discussions don't have an endpoint being provided.
+ */
+ 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(() => {
- Notes.renderDiffError($container);
+ $successContainer.addClass('hidden');
+ $errorContainer.removeClass('hidden');
});
+ }
+ return Promise.resolve();
}
toggleCommitList(e) {
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 648fa6ff804..396a675b4ac 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -317,10 +317,10 @@ Please check your network connection and try again.`;
<note-signed-out-widget v-if="!isLoggedIn" />
<discussion-locked-widget
issuable-type="issue"
- v-else-if="!canCreateNote"
+ v-else-if="isLocked(getNoteableData) && !canCreateNote"
/>
<ul
- v-else
+ v-else-if="canCreateNote"
class="notes notes-form timeline">
<li class="timeline-entry">
<div class="timeline-entry-inner">
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index a7e2d857013..626b0799581 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -40,6 +40,10 @@ export default {
type: Boolean,
required: true,
},
+ canAwardEmoji: {
+ type: Boolean,
+ required: true,
+ },
canDelete: {
type: Boolean,
required: true,
@@ -74,9 +78,6 @@ export default {
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
- canAddAwardEmoji() {
- return this.currentUserId;
- },
isAuthoredByCurrentUser() {
return this.authorId === this.currentUserId;
},
@@ -149,7 +150,7 @@ export default {
</button>
</div>
<div
- v-if="canAddAwardEmoji"
+ v-if="canAwardEmoji"
class="note-actions-item">
<a
v-tooltip
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 6cb8229e268..e8fd155a1ee 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -28,6 +28,10 @@ export default {
type: Number,
required: true,
},
+ canAwardEmoji: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
...mapGetters(['getUserData']),
@@ -67,9 +71,6 @@ export default {
isAuthoredByMe() {
return this.noteAuthorId === this.getUserData.id;
},
- isLoggedIn() {
- return this.getUserData.id;
- },
},
created() {
this.emojiSmiling = emojiSmiling;
@@ -156,7 +157,7 @@ export default {
return title;
},
handleAward(awardName) {
- if (!this.isLoggedIn) {
+ if (!this.canAwardEmoji) {
return;
}
@@ -208,7 +209,7 @@ export default {
</span>
</button>
<div
- v-if="isLoggedIn"
+ v-if="canAwardEmoji"
class="award-menu-holder">
<button
v-tooltip
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 069f94c5845..0cb626c14f4 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -112,6 +112,7 @@ export default {
:note-author-id="note.author.id"
:awards="note.award_emoji"
:toggle-award-path="note.toggle_award_path"
+ :can-award-emoji="note.current_user.can_award_emoji"
/>
<note-attachment
v-if="note.attachment"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 3554027d2b4..566f5c68e66 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -177,6 +177,7 @@ export default {
:note-id="note.id"
:access-level="note.human_access"
:can-edit="note.current_user.can_edit"
+ :can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
:report-abuse-path="note.report_abuse_path"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 5bd81c7cad6..ebfc827ac57 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -49,16 +49,7 @@ export default {
computed: {
...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
- // FIXME -- @fatihacet Get this from JSON data.
- const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
-
- if (this.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
- return EPIC_NOTEABLE_TYPE;
- }
-
- return this.noteableData.merge_params
- ? MERGE_REQUEST_NOTEABLE_TYPE
- : ISSUE_NOTEABLE_TYPE;
+ return this.noteableData.noteableType;
},
allNotes() {
if (this.isLoading) {
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index 68f8cb1cf1e..c4de4826eda 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -14,3 +14,9 @@ export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
+
+export const NOTEABLE_TYPE_MAPPING = {
+ Issue: ISSUE_NOTEABLE_TYPE,
+ MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE,
+ Epic: EPIC_NOTEABLE_TYPE,
+};
diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js
index 5bf8216a1f3..b68543d71c8 100644
--- a/app/assets/javascripts/notes/mixins/noteable.js
+++ b/app/assets/javascripts/notes/mixins/noteable.js
@@ -9,16 +9,7 @@ export default {
},
computed: {
noteableType() {
- switch (this.note.noteable_type) {
- case 'MergeRequest':
- return constants.MERGE_REQUEST_NOTEABLE_TYPE;
- case 'Issue':
- return constants.ISSUE_NOTEABLE_TYPE;
- case 'Epic':
- return constants.EPIC_NOTEABLE_TYPE;
- default:
- return '';
- }
+ return constants.NOTEABLE_TYPE_MAPPING[this.note.noteable_type];
},
},
};
diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js
index 397149aaa9e..8b529585898 100644
--- a/app/assets/javascripts/pages/dashboard/milestones/show/index.js
+++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js
@@ -6,4 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
new Milestone(); // eslint-disable-line no-new
new Sidebar(); // eslint-disable-line no-new
new MountMilestoneSidebar(); // eslint-disable-line no-new
+
+ Milestone.initDeprecationMessage();
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index d149b307e7f..914f804fdd3 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
+ isGroupDecendent: true,
});
projectSelect();
});
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index a5cc1f34b63..1600faa3611 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
+ isGroupDecendent: true,
});
projectSelect();
});
diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js
index 88f40b5278e..74cc4ba42c1 100644
--- a/app/assets/javascripts/pages/groups/milestones/show/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/show/index.js
@@ -1,3 +1,8 @@
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
+import Milestone from '~/milestone';
-document.addEventListener('DOMContentLoaded', initMilestonesShow);
+document.addEventListener('DOMContentLoaded', () => {
+ initMilestonesShow();
+
+ Milestone.initDeprecationMessage();
+});
diff --git a/app/assets/javascripts/pages/groups/settings/badges/index.js b/app/assets/javascripts/pages/groups/settings/badges/index.js
new file mode 100644
index 00000000000..74e96ee4a8f
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/settings/badges/index.js
@@ -0,0 +1,10 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import { GROUP_BADGE } from '~/badges/constants';
+import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
+
+Vue.use(Translate);
+
+document.addEventListener('DOMContentLoaded', () => {
+ mountBadgeSettings(GROUP_BADGE);
+});
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index be37df36be8..628913483c6 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,12 +1,12 @@
import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
-import ProjectNew from '../shared/project_new';
+import initProjectLoadingSpinner from '../shared/save_project_loader';
import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
document.addEventListener('DOMContentLoaded', () => {
- new ProjectNew(); // eslint-disable-line no-new
+ initProjectLoadingSpinner();
setupProjectEdit();
// Initialize expandable settings panels
initSettingsPanels();
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index ea6fd961393..7db644e2477 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -1,9 +1,9 @@
-import ProjectNew from '../shared/project_new';
+import initProjectLoadingSpinner from '../shared/save_project_loader';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
document.addEventListener('DOMContentLoaded', () => {
- new ProjectNew(); // eslint-disable-line no-new
+ initProjectLoadingSpinner();
initProjectVisibilitySelector();
initProjectNew.bindEvents();
});
diff --git a/app/assets/javascripts/pages/projects/settings/badges/index/index.js b/app/assets/javascripts/pages/projects/settings/badges/index/index.js
new file mode 100644
index 00000000000..30469550866
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/badges/index/index.js
@@ -0,0 +1,10 @@
+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/settings/repository/create_deploy_token/index.js b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
new file mode 100644
index 00000000000..ffc84dc106b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
@@ -0,0 +1,3 @@
+import initForm from '../form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
new file mode 100644
index 00000000000..a5c17ab322c
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -0,0 +1,19 @@
+/* eslint-disable no-new */
+
+import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
+import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
+import initSettingsPanels from '~/settings_panels';
+import initDeployKeys from '~/deploy_keys';
+import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
+import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
+import DueDateSelectors from '~/due_date_select';
+
+export default () => {
+ new ProtectedTagCreate();
+ new ProtectedTagEditList();
+ initDeployKeys();
+ initSettingsPanels();
+ new ProtectedBranchCreate(); // eslint-disable-line no-new
+ new ProtectedBranchEditList(); // eslint-disable-line no-new
+ new DueDateSelectors();
+};
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index 788d86d1192..ffc84dc106b 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,17 +1,3 @@
-/* eslint-disable no-new */
+import initForm from '../form';
-import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
-import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
-import initSettingsPanels from '~/settings_panels';
-import initDeployKeys from '~/deploy_keys';
-import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
-import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
-
-document.addEventListener('DOMContentLoaded', () => {
- new ProtectedTagCreate();
- new ProtectedTagEditList();
- initDeployKeys();
- initSettingsPanels();
- new ProtectedBranchCreate(); // eslint-disable-line no-new
- new ProtectedBranchEditList(); // eslint-disable-line no-new
-});
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/shared/project_new.js b/app/assets/javascripts/pages/projects/shared/project_new.js
deleted file mode 100644
index 56d5574aa2f..00000000000
--- a/app/assets/javascripts/pages/projects/shared/project_new.js
+++ /dev/null
@@ -1,152 +0,0 @@
-/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/
-
-import $ from 'jquery';
-import VisibilitySelect from '../../../visibility_select';
-
-function highlightChanges($elm) {
- $elm.addClass('highlight-changes');
- setTimeout(() => $elm.removeClass('highlight-changes'), 10);
-}
-
-export default class ProjectNew {
- constructor() {
- this.toggleSettings = this.toggleSettings.bind(this);
- this.$selects = $('.features select');
- this.$repoSelects = this.$selects.filter('.js-repo-select');
- this.$projectSelects = this.$selects.not('.js-repo-select');
-
- $('.project-edit-container').on('ajax:before', () => {
- $('.project-edit-container').hide();
- return $('.save-project-loader').show();
- });
-
- this.initVisibilitySelect();
-
- this.toggleSettings();
- this.toggleSettingsOnclick();
- this.toggleRepoVisibility();
- }
-
- initVisibilitySelect() {
- const visibilityContainer = document.querySelector('.js-visibility-select');
- if (!visibilityContainer) return;
- const visibilitySelect = new VisibilitySelect(visibilityContainer);
- visibilitySelect.init();
-
- const $visibilitySelect = $(visibilityContainer).find('select');
- let projectVisibility = $visibilitySelect.val();
- const PROJECT_VISIBILITY_PRIVATE = '0';
-
- $visibilitySelect.on('change', () => {
- const newProjectVisibility = $visibilitySelect.val();
-
- if (projectVisibility !== newProjectVisibility) {
- this.$projectSelects.each((idx, select) => {
- const $select = $(select);
- const $options = $select.find('option');
- const values = $.map($options, e => e.value);
-
- // if switched to "private", limit visibility options
- if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
- if ($select.val() !== values[0] && $select.val() !== values[1]) {
- $select.val(values[1]).trigger('change');
- highlightChanges($select);
- }
- $options.slice(2).disable();
- }
-
- // if switched from "private", increase visibility for non-disabled options
- if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
- $options.enable();
- if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
- $select.val(values[values.length - 1]).trigger('change');
- highlightChanges($select);
- }
- }
- });
-
- projectVisibility = newProjectVisibility;
- }
- });
- }
-
- toggleSettings() {
- this.$selects.each(function () {
- var $select = $(this);
- var className = $select.data('field')
- .replace(/_/g, '-')
- .replace('access-level', 'feature');
- ProjectNew._showOrHide($select, '.' + className);
- });
- }
-
- toggleSettingsOnclick() {
- this.$selects.on('change', this.toggleSettings);
- }
-
- static _showOrHide(checkElement, container) {
- const $container = $(container);
-
- if ($(checkElement).val() !== '0') {
- return $container.show();
- }
- return $container.hide();
- }
-
- toggleRepoVisibility() {
- var $repoAccessLevel = $('.js-repo-access-level select');
- var $lfsEnabledOption = $('.js-lfs-enabled select');
- var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
- var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
- var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
-
- this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
- .nextAll()
- .hide();
-
- $repoAccessLevel
- .off('change')
- .on('change', function () {
- var selectedVal = parseInt($repoAccessLevel.val(), 10);
-
- this.$repoSelects.each(function () {
- var $this = $(this);
- var repoSelectVal = parseInt($this.val(), 10);
-
- $this.find('option').enable();
-
- if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
- $this.val(selectedVal).trigger('change');
- highlightChanges($this);
- }
-
- $this.find("option[value='" + selectedVal + "']").nextAll().disable();
- });
-
- if (selectedVal) {
- this.$repoSelects.removeClass('disabled');
-
- if ($lfsEnabledOption.length) {
- $lfsEnabledOption.removeClass('disabled');
- highlightChanges($lfsEnabledOption);
- }
- if (containerRegistry) {
- containerRegistry.style.display = '';
- }
- } else {
- this.$repoSelects.addClass('disabled');
-
- if ($lfsEnabledOption.length) {
- $lfsEnabledOption.val('false').addClass('disabled');
- highlightChanges($lfsEnabledOption);
- }
- if (containerRegistry) {
- containerRegistry.style.display = 'none';
- containerRegistryCheckbox.checked = false;
- }
- }
-
- prevSelectedVal = selectedVal;
- }.bind(this));
- }
-}
diff --git a/app/assets/javascripts/pages/projects/shared/save_project_loader.js b/app/assets/javascripts/pages/projects/shared/save_project_loader.js
new file mode 100644
index 00000000000..aa3589ac88d
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/shared/save_project_loader.js
@@ -0,0 +1,12 @@
+import $ from 'jquery';
+
+export default function initProjectLoadingSpinner() {
+ const $formContainer = $('.project-edit-container');
+ const $loadingSpinner = $('.save-project-loader');
+
+ // show loading spinner when saving
+ $formContainer.on('ajax:before', () => {
+ $formContainer.hide();
+ $loadingSpinner.show();
+ });
+}
diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js
index a134599cb04..c35b9c30058 100644
--- a/app/assets/javascripts/pages/projects/snippets/show/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/show/index.js
@@ -1,11 +1,13 @@
import initNotes from '~/init_notes';
import ZenMode from '~/zen_mode';
-import LineHighlighter from '../../../../line_highlighter';
-import BlobViewer from '../../../../blob/viewer';
+import LineHighlighter from '~/line_highlighter';
+import BlobViewer from '~/blob/viewer';
+import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new
initNotes();
new ZenMode(); // eslint-disable-line no-new
+ snippetEmbed();
});
diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
index 08f0afdcce3..d321892d2d2 100644
--- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
+++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
@@ -5,7 +5,7 @@ import AccessorUtilities from '~/lib/utils/accessor';
* Does that setting the current selected tab in the localStorage
*/
export default class SigninTabsMemoizer {
- constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
+ constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.new-session-tabs' } = {}) {
this.currentTabKey = currentTabKey;
this.tabSelector = tabSelector;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
diff --git a/app/assets/javascripts/pages/shared/mount_badge_settings.js b/app/assets/javascripts/pages/shared/mount_badge_settings.js
new file mode 100644
index 00000000000..1397c0834ff
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/mount_badge_settings.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import BadgeSettings from '~/badges/components/badge_settings.vue';
+import store from '~/badges/store';
+
+export default kind => {
+ const badgeSettingsElement = document.getElementById('badge-settings');
+
+ store.dispatch('loadBadges', {
+ kind,
+ apiEndpointUrl: badgeSettingsElement.dataset.apiEndpointUrl,
+ docsUrl: badgeSettingsElement.dataset.docsUrl,
+ });
+
+ return new Vue({
+ el: badgeSettingsElement,
+ store,
+ components: {
+ BadgeSettings,
+ },
+ render(createElement) {
+ return createElement(BadgeSettings);
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js
index f548b9fad65..26936110402 100644
--- a/app/assets/javascripts/pages/snippets/show/index.js
+++ b/app/assets/javascripts/pages/snippets/show/index.js
@@ -1,11 +1,13 @@
-import LineHighlighter from '../../../line_highlighter';
-import BlobViewer from '../../../blob/viewer';
-import ZenMode from '../../../zen_mode';
-import initNotes from '../../../init_notes';
+import LineHighlighter from '~/line_highlighter';
+import BlobViewer from '~/blob/viewer';
+import ZenMode from '~/zen_mode';
+import initNotes from '~/init_notes';
+import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new
initNotes();
new ZenMode(); // eslint-disable-line no-new
+ snippetEmbed();
});
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 3ebfaa87a4e..bc71911ae35 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -10,29 +10,25 @@ export default class PerformanceBarService {
}
static registerInterceptor(peekUrl, callback) {
- vueResourceInterceptor = (request, next) => {
- next(response => {
- const requestId = response.headers['x-request-id'];
- const requestUrl = response.url;
-
- if (requestUrl !== peekUrl && requestId) {
- callback(requestId, requestUrl);
- }
- });
- };
-
- Vue.http.interceptors.push(vueResourceInterceptor);
-
- return axios.interceptors.response.use(response => {
+ const interceptor = response => {
const requestId = response.headers['x-request-id'];
- const requestUrl = response.config.url;
+ // Get the request URL from response.config for Axios, and response for
+ // Vue Resource.
+ const requestUrl = (response.config || response).url;
+ const cachedResponse = response.headers['x-gitlab-from-cache'] === 'true';
- if (requestUrl !== peekUrl && requestId) {
+ if (requestUrl !== peekUrl && requestId && !cachedResponse) {
callback(requestId, requestUrl);
}
return response;
- });
+ };
+
+ vueResourceInterceptor = (request, next) => next(interceptor);
+
+ Vue.http.interceptors.push(vueResourceInterceptor);
+
+ return axios.interceptors.response.use(interceptor);
}
static removeInterceptor(interceptor) {
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index d7effb27bff..e99d949801f 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,60 +1,72 @@
<script>
- import tooltip from '../../../vue_shared/directives/tooltip';
- import icon from '../../../vue_shared/components/icon.vue';
- import { dasherize } from '../../../lib/utils/text_utility';
- /**
- * Renders either a cancel, retry or play icon pointing to the given path.
- * TODO: Remove UJS from here and use an async request instead.
- */
- export default {
- components: {
- icon,
- },
+import $ from 'jquery';
+import tooltip from '../../../vue_shared/directives/tooltip';
+import Icon from '../../../vue_shared/components/icon.vue';
+import { dasherize } from '../../../lib/utils/text_utility';
+import eventHub from '../../event_hub';
+/**
+ * Renders either a cancel, retry or play icon pointing to the given path.
+ */
+export default {
+ components: {
+ Icon,
+ },
- directives: {
- tooltip,
- },
+ directives: {
+ tooltip,
+ },
- props: {
- tooltipText: {
- type: String,
- required: true,
- },
+ props: {
+ tooltipText: {
+ type: String,
+ required: true,
+ },
- link: {
- type: String,
- required: true,
- },
+ link: {
+ type: String,
+ required: true,
+ },
- actionMethod: {
- type: String,
- required: true,
- },
+ actionIcon: {
+ type: String,
+ required: true,
+ },
- actionIcon: {
- type: String,
- required: true,
- },
+ buttonDisabled: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ cssClass() {
+ const actionIconDash = dasherize(this.actionIcon);
+ return `${actionIconDash} js-icon-${actionIconDash}`;
+ },
+ isDisabled() {
+ return this.buttonDisabled === this.link;
},
+ },
- computed: {
- cssClass() {
- const actionIconDash = dasherize(this.actionIcon);
- return `${actionIconDash} js-icon-${actionIconDash}`;
- },
+ methods: {
+ onClickAction() {
+ $(this.$el).tooltip('hide');
+ eventHub.$emit('graphAction', this.link);
},
- };
+ },
+};
</script>
<template>
- <a
+ <button
+ type="button"
+ @click="onClickAction"
v-tooltip
- :data-method="actionMethod"
:title="tooltipText"
- :href="link"
- class="ci-action-icon-container ci-action-icon-wrapper"
+ class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass"
data-container="body"
+ :disabled="isDisabled"
>
<icon :name="actionIcon" />
- </a>
+ </button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ab84711d4a2..ac9ce7e47d6 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,54 +1,59 @@
<script>
- import loadingIcon from '~/vue_shared/components/loading_icon.vue';
- import stageColumnComponent from './stage_column_component.vue';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import StageColumnComponent from './stage_column_component.vue';
- export default {
- components: {
- stageColumnComponent,
- loadingIcon,
- },
+export default {
+ components: {
+ StageColumnComponent,
+ LoadingIcon,
+ },
- props: {
- isLoading: {
- type: Boolean,
- required: true,
- },
- pipeline: {
- type: Object,
- required: true,
- },
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ actionDisabled: {
+ type: String,
+ required: false,
+ default: null,
},
+ },
- computed: {
- graph() {
- return this.pipeline.details && this.pipeline.details.stages;
- },
+ computed: {
+ graph() {
+ return this.pipeline.details && this.pipeline.details.stages;
},
+ },
- methods: {
- capitalizeStageName(name) {
- return name.charAt(0).toUpperCase() + name.slice(1);
- },
+ methods: {
+ capitalizeStageName(name) {
+ return name.charAt(0).toUpperCase() + name.slice(1);
+ },
- isFirstColumn(index) {
- return index === 0;
- },
+ isFirstColumn(index) {
+ return index === 0;
+ },
- stageConnectorClass(index, stage) {
- let className;
+ stageConnectorClass(index, stage) {
+ let className;
- // If it's the first stage column and only has one job
- if (index === 0 && stage.groups.length === 1) {
- className = 'no-margin';
- } else if (index > 0) {
- // If it is not the first column
- className = 'left-margin';
- }
+ // If it's the first stage column and only has one job
+ if (index === 0 && stage.groups.length === 1) {
+ className = 'no-margin';
+ } else if (index > 0) {
+ // If it is not the first column
+ className = 'left-margin';
+ }
- return className;
- },
+ return className;
},
- };
+ },
+};
</script>
<template>
<div class="build-content middle-block js-pipeline-graph">
@@ -70,6 +75,7 @@
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
+ :action-disabled="actionDisabled"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 9b136573135..c6e5ae6df41 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -1,95 +1,102 @@
<script>
- import actionComponent from './action_component.vue';
- import dropdownActionComponent from './dropdown_action_component.vue';
- import jobNameComponent from './job_name_component.vue';
- import tooltip from '../../../vue_shared/directives/tooltip';
-
- /**
- * Renders the badge for the pipeline graph and the job's dropdown.
- *
- * The following object should be provided as `job`:
- *
- * {
- * "id": 4256,
- * "name": "test",
- * "status": {
- * "icon": "icon_status_success",
- * "text": "passed",
- * "label": "passed",
- * "group": "success",
- * "details_path": "/root/ci-mock/builds/4256",
- * "action": {
- * "icon": "retry",
- * "title": "Retry",
- * "path": "/root/ci-mock/builds/4256/retry",
- * "method": "post"
- * }
- * }
- * }
- */
-
- export default {
- components: {
- actionComponent,
- dropdownActionComponent,
- jobNameComponent,
+import ActionComponent from './action_component.vue';
+import DropdownActionComponent from './dropdown_action_component.vue';
+import JobNameComponent from './job_name_component.vue';
+import tooltip from '../../../vue_shared/directives/tooltip';
+
+/**
+ * Renders the badge for the pipeline graph and the job's dropdown.
+ *
+ * The following object should be provided as `job`:
+ *
+ * {
+ * "id": 4256,
+ * "name": "test",
+ * "status": {
+ * "icon": "icon_status_success",
+ * "text": "passed",
+ * "label": "passed",
+ * "group": "success",
+ * "tooltip": "passed",
+ * "details_path": "/root/ci-mock/builds/4256",
+ * "action": {
+ * "icon": "retry",
+ * "title": "Retry",
+ * "path": "/root/ci-mock/builds/4256/retry",
+ * "method": "post"
+ * }
+ * }
+ * }
+ */
+
+export default {
+ components: {
+ ActionComponent,
+ DropdownActionComponent,
+ JobNameComponent,
+ },
+
+ directives: {
+ tooltip,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ cssClassJobName: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- job: {
- type: Object,
- required: true,
- },
-
- cssClassJobName: {
- type: String,
- required: false,
- default: '',
- },
-
- isDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
+
+ isDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ actionDisabled: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+
+ computed: {
+ status() {
+ return this.job && this.job.status ? this.job.status : {};
+ },
+
+ tooltipText() {
+ const textBuilder = [];
+
+ if (this.job.name) {
+ textBuilder.push(this.job.name);
+ }
+
+ if (this.job.name && this.status.tooltip) {
+ textBuilder.push('-');
+ }
+
+ if (this.status.tooltip) {
+ textBuilder.push(`${this.job.status.tooltip}`);
+ }
+
+ return textBuilder.join(' ');
},
- computed: {
- status() {
- return this.job && this.job.status ? this.job.status : {};
- },
-
- tooltipText() {
- const textBuilder = [];
-
- if (this.job.name) {
- textBuilder.push(this.job.name);
- }
-
- if (this.job.name && this.status.label) {
- textBuilder.push('-');
- }
-
- if (this.status.label) {
- textBuilder.push(`${this.job.status.label}`);
- }
-
- return textBuilder.join(' ');
- },
-
- /**
- * Verifies if the provided job has an action path
- *
- * @return {Boolean}
- */
- hasAction() {
- return this.job.status && this.job.status.action && this.job.status.action.path;
- },
+ /**
+ * Verifies if the provided job has an action path
+ *
+ * @return {Boolean}
+ */
+ hasAction() {
+ return this.job.status && this.job.status.action && this.job.status.action.path;
},
- };
+ },
+};
</script>
<template>
<div class="ci-job-component">
@@ -100,6 +107,7 @@
:title="tooltipText"
:class="cssClassJobName"
data-container="body"
+ data-html="true"
class="js-pipeline-graph-job-link"
>
@@ -115,6 +123,7 @@
class="js-job-component-tooltip"
:title="tooltipText"
:class="cssClassJobName"
+ data-html="true"
data-container="body"
>
@@ -129,7 +138,7 @@
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
- :action-method="status.action.method"
+ :button-disabled="actionDisabled"
/>
<dropdown-action-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 7adcf4017b8..f6e6569e15b 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -1,50 +1,55 @@
<script>
- import jobComponent from './job_component.vue';
- import dropdownJobComponent from './dropdown_job_component.vue';
+import JobComponent from './job_component.vue';
+import DropdownJobComponent from './dropdown_job_component.vue';
- export default {
- components: {
- jobComponent,
- dropdownJobComponent,
+export default {
+ components: {
+ JobComponent,
+ DropdownJobComponent,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- jobs: {
- type: Array,
- required: true,
- },
+ jobs: {
+ type: Array,
+ required: true,
+ },
- isFirstColumn: {
- type: Boolean,
- required: false,
- default: false,
- },
+ isFirstColumn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- stageConnectorClass: {
- type: String,
- required: false,
- default: '',
- },
+ stageConnectorClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ actionDisabled: {
+ type: String,
+ required: false,
+ default: null,
},
+ },
- methods: {
- firstJob(list) {
- return list[0];
- },
+ methods: {
+ firstJob(list) {
+ return list[0];
+ },
- jobId(job) {
- return `ci-badge-${job.name}`;
- },
+ jobId(job) {
+ return `ci-badge-${job.name}`;
+ },
- buildConnnectorClass(index) {
- return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
- },
+ buildConnnectorClass(index) {
+ return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
- };
+ },
+};
</script>
<template>
<li
@@ -69,6 +74,7 @@
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
+ :action-disabled="actionDisabled"
/>
<dropdown-job-component
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index e0a7284124d..497a09cec65 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -7,10 +7,7 @@
import TablePagination from '../../vue_shared/components/table_pagination.vue';
import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import NavigationControls from './nav_controls.vue';
- import {
- getParameterByName,
- parseQueryStringIntoObject,
- } from '../../lib/utils/common_utils';
+ import { getParameterByName } from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
@@ -19,10 +16,7 @@
NavigationTabs,
NavigationControls,
},
- mixins: [
- pipelinesMixin,
- CIPaginationMixin,
- ],
+ mixins: [pipelinesMixin, CIPaginationMixin],
props: {
store: {
type: Object,
@@ -147,25 +141,26 @@
*/
shouldRenderTabs() {
const { stateMap } = this.$options;
- return this.hasMadeRequest &&
- [
- stateMap.loading,
- stateMap.tableList,
- stateMap.error,
- stateMap.emptyTab,
- ].includes(this.stateToRender);
+ return (
+ this.hasMadeRequest &&
+ [stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes(
+ this.stateToRender,
+ )
+ );
},
shouldRenderButtons() {
- return (this.newPipelinePath ||
- this.resetCachePath ||
- this.ciLintPath) && this.shouldRenderTabs;
+ return (
+ (this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs
+ );
},
shouldRenderPagination() {
- return !this.isLoading &&
+ return (
+ !this.isLoading &&
this.state.pipelines.length &&
- this.state.pageInfo.total > this.state.pageInfo.perPage;
+ this.state.pageInfo.total > this.state.pageInfo.perPage
+ );
},
emptyTabMessage() {
@@ -229,15 +224,13 @@
},
methods: {
successCallback(resp) {
- return resp.json().then((response) => {
- // Because we are polling & the user is interacting verify if the response received
- // matches the last request made
- if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
- this.store.storeCount(response.count);
- this.store.storePagination(resp.headers);
- this.setCommonData(response.pipelines);
- }
- });
+ // Because we are polling & the user is interacting verify if the response received
+ // matches the last request made
+ if (_.isEqual(resp.config.params, this.requestData)) {
+ this.store.storeCount(resp.data.count);
+ this.store.storePagination(resp.headers);
+ this.setCommonData(resp.data.pipelines);
+ }
},
/**
* Handles URL and query parameter changes.
@@ -251,8 +244,9 @@
this.updateInternalState(parameters);
// fetch new data
- return this.service.getPipelines(this.requestData)
- .then((response) => {
+ return this.service
+ .getPipelines(this.requestData)
+ .then(response => {
this.isLoading = false;
this.successCallback(response);
@@ -271,13 +265,11 @@
handleResetRunnersCache(endpoint) {
this.isResetCacheButtonLoading = true;
- this.service.postAction(endpoint)
+ this.service
+ .postAction(endpoint)
.then(() => {
this.isResetCacheButtonLoading = false;
- createFlash(
- s__('Pipelines|Project cache successfully reset.'),
- 'notice',
- );
+ createFlash(s__('Pipelines|Project cache successfully reset.'), 'notice');
})
.catch(() => {
this.isResetCacheButtonLoading = false;
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 8bc7a1f20b2..32cf3dba3c3 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -1,5 +1,4 @@
<script>
- import $ from 'jquery';
/**
* Renders each stage of the pipeline mini graph.
@@ -14,15 +13,18 @@
* 4. Commit widget
*/
+ import $ from 'jquery';
import Flash from '../../flash';
- import icon from '../../vue_shared/components/icon.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ 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 tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
- loadingIcon,
- icon,
+ LoadingIcon,
+ Icon,
},
directives: {
@@ -82,15 +84,15 @@
methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
+ eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
fetchJobs() {
- this.$http.get(this.stage.dropdown_path)
- .then(response => response.json())
- .then((data) => {
+ axios.get(this.stage.dropdown_path)
+ .then(({ data }) => {
this.dropdownContent = data.html;
this.isLoading = false;
})
@@ -98,8 +100,7 @@
this.closeDropdown();
this.isLoading = false;
- const flash = new Flash('Something went wrong on our end.');
- return flash;
+ Flash('Something went wrong on our end.');
});
},
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
new file mode 100644
index 00000000000..b384c7500e7
--- /dev/null
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export const CANCEL_REQUEST = 'CANCEL_REQUEST';
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 522a4277bd7..6d87f75ae8e 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -7,6 +7,7 @@ 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';
export default {
components: {
@@ -52,34 +53,58 @@ export default {
});
eventHub.$on('postAction', this.postAction);
+ eventHub.$on('clickedDropdown', this.updateTable);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
+ eventHub.$off('clickedDropdown', this.updateTable);
},
destroyed() {
this.poll.stop();
},
methods: {
+ updateTable() {
+ // Cancel ongoing request
+ if (this.isMakingRequest) {
+ this.service.cancelationSource.cancel(CANCEL_REQUEST);
+ }
+ // Stop polling
+ this.poll.stop();
+ // Update the table
+ return this.getPipelines()
+ .then(() => this.poll.restart());
+ },
fetchPipelines() {
if (!this.isMakingRequest) {
this.isLoading = true;
- this.service.getPipelines(this.requestData)
- .then(response => this.successCallback(response))
- .catch(() => this.errorCallback());
+ this.getPipelines();
}
},
+ getPipelines() {
+ return this.service.getPipelines(this.requestData)
+ .then(response => this.successCallback(response))
+ .catch((error) => this.errorCallback(error));
+ },
setCommonData(pipelines) {
this.store.storePipelines(pipelines);
this.isLoading = false;
this.updateGraphDropdown = true;
this.hasMadeRequest = true;
+
+ // In case the previous polling request returned an error, we need to reset it
+ if (this.hasError) {
+ this.hasError = false;
+ }
},
- errorCallback() {
- this.hasError = true;
- this.isLoading = false;
- this.updateGraphDropdown = false;
+ errorCallback(error) {
this.hasMadeRequest = true;
+ this.isLoading = false;
+
+ if (error && error.message && error.message !== CANCEL_REQUEST) {
+ this.hasError = true;
+ this.updateGraphDropdown = false;
+ }
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 6b26708148c..900eb7855f4 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -25,13 +25,36 @@ export default () => {
data() {
return {
mediator,
+ actionDisabled: null,
};
},
+ created() {
+ eventHub.$on('graphAction', this.postAction);
+ },
+ beforeDestroy() {
+ eventHub.$off('graphAction', this.postAction);
+ },
+ methods: {
+ postAction(action) {
+ this.actionDisabled = action;
+
+ this.mediator.service.postAction(action)
+ .then(() => {
+ this.mediator.refreshPipeline();
+ this.actionDisabled = null;
+ })
+ .catch(() => {
+ this.actionDisabled = null;
+ Flash(__('An error occurred while making the request.'));
+ });
+ },
+ },
render(createElement) {
return createElement('pipeline-graph', {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
+ actionDisabled: this.actionDisabled,
},
});
},
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index 10f238fe73b..5633e54b28a 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -40,10 +40,8 @@ export default class pipelinesMediator {
}
successCallback(response) {
- return response.json().then((data) => {
- this.state.isLoading = false;
- this.store.storePipeline(data);
- });
+ this.state.isLoading = false;
+ this.store.storePipeline(response.data);
}
errorCallback() {
@@ -52,8 +50,11 @@ export default class pipelinesMediator {
}
refreshPipeline() {
- this.service.getPipeline()
+ this.poll.stop();
+
+ return this.service.getPipeline()
.then(response => this.successCallback(response))
- .catch(() => this.errorCallback());
+ .catch(() => this.errorCallback())
+ .finally(() => this.poll.restart());
}
}
diff --git a/app/assets/javascripts/pipelines/services/pipeline_service.js b/app/assets/javascripts/pipelines/services/pipeline_service.js
index 3e0c52c7726..a53a9cc8365 100644
--- a/app/assets/javascripts/pipelines/services/pipeline_service.js
+++ b/app/assets/javascripts/pipelines/services/pipeline_service.js
@@ -1,19 +1,16 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '../../lib/utils/axios_utils';
export default class PipelineService {
constructor(endpoint) {
- this.pipeline = Vue.resource(endpoint);
+ this.pipeline = endpoint;
}
getPipeline() {
- return this.pipeline.get();
+ return axios.get(this.pipeline);
}
- // eslint-disable-next-line
+ // eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
- return Vue.http.post(`${endpoint}.json`);
+ return axios.post(`${endpoint}.json`);
}
}
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 47736fc5f42..59c8b9c58e5 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -1,35 +1,32 @@
-/* eslint-disable class-methods-use-this */
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-import '../../vue_shared/vue_resource_interceptor';
-
-Vue.use(VueResource);
+import axios from '../../lib/utils/axios_utils';
export default class PipelinesService {
-
/**
- * Commits and merge request endpoints need to be requested with `.json`.
- *
- * The url provided to request the pipelines in the new merge request
- * page already has `.json`.
- *
- * @param {String} root
- */
+ * Commits and merge request endpoints need to be requested with `.json`.
+ *
+ * The url provided to request the pipelines in the new merge request
+ * page already has `.json`.
+ *
+ * @param {String} root
+ */
constructor(root) {
- let endpoint;
-
if (root.indexOf('.json') === -1) {
- endpoint = `${root}.json`;
+ this.endpoint = `${root}.json`;
} else {
- endpoint = root;
+ this.endpoint = root;
}
-
- this.pipelines = Vue.resource(endpoint);
}
getPipelines(data = {}) {
const { scope, page } = data;
- return this.pipelines.get({ scope, page });
+ const CancelToken = axios.CancelToken;
+
+ this.cancelationSource = CancelToken.source();
+
+ return axios.get(this.endpoint, {
+ params: { scope, page },
+ cancelToken: this.cancelationSource.token,
+ });
}
/**
@@ -38,7 +35,8 @@ export default class PipelinesService {
* @param {String} endpoint
* @return {Promise}
*/
+ // eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
- return Vue.http.post(`${endpoint}.json`);
+ return axios.post(`${endpoint}.json`);
}
}
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
new file mode 100644
index 00000000000..e5de3f69b01
--- /dev/null
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -0,0 +1,121 @@
+<script>
+import _ from 'underscore';
+import axios from '~/lib/utils/axios_utils';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+import Flash from '~/flash';
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ actionUrl: {
+ type: String,
+ required: true,
+ },
+ rootUrl: {
+ type: String,
+ required: true,
+ },
+ initialUsername: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isRequestPending: false,
+ username: this.initialUsername,
+ newUsername: this.initialUsername,
+ };
+ },
+ computed: {
+ path() {
+ return sprintf(s__('Profiles|Current path: %{path}'), {
+ path: `${this.rootUrl}${this.username}`,
+ });
+ },
+ modalText() {
+ return sprintf(
+ s__(`Profiles|
+You are going to change the username %{currentUsernameBold} to %{newUsernameBold}.
+Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group.
+Please update your Git repository remotes as soon as possible.`),
+ {
+ currentUsernameBold: `<strong>${_.escape(this.username)}</strong>`,
+ newUsernameBold: `<strong>${_.escape(this.newUsername)}</strong>`,
+ currentUsername: _.escape(this.username),
+ newUsername: _.escape(this.newUsername),
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ onConfirm() {
+ this.isRequestPending = true;
+ const username = this.newUsername;
+ const putData = {
+ user: {
+ username,
+ },
+ };
+
+ return axios
+ .put(this.actionUrl, putData)
+ .then(result => {
+ Flash(result.data.message, 'notice');
+ this.username = username;
+ this.isRequestPending = false;
+ })
+ .catch(error => {
+ Flash(error.response.data.message);
+ this.isRequestPending = false;
+ throw error;
+ });
+ },
+ },
+ modalId: 'username-change-confirmation-modal',
+ inputId: 'username-change-input',
+ buttonText: s__('Profiles|Update username'),
+};
+</script>
+<template>
+ <div>
+ <div class="form-group">
+ <label :for="$options.inputId">{{ s__('Profiles|Path') }}</label>
+ <div class="input-group">
+ <div class="input-group-addon">{{ rootUrl }}</div>
+ <input
+ :id="$options.inputId"
+ class="form-control"
+ required="required"
+ v-model="newUsername"
+ :disabled="isRequestPending"
+ />
+ </div>
+ <p class="help-block">
+ {{ path }}
+ </p>
+ </div>
+ <button
+ :data-target="`#${$options.modalId}`"
+ class="btn btn-warning"
+ type="button"
+ data-toggle="modal"
+ :disabled="isRequestPending || newUsername === username"
+ >
+ {{ $options.buttonText }}
+ </button>
+ <gl-modal
+ :id="$options.modalId"
+ :header-title-text="s__('Profiles|Change username') + '?'"
+ footer-primary-button-variant="warning"
+ :footer-primary-button-text="$options.buttonText"
+ @submit="onConfirm"
+ >
+ <span v-html="modalText"></span>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
index 84049a1f0b7..59c13e1a042 100644
--- a/app/assets/javascripts/profile/account/index.js
+++ b/app/assets/javascripts/profile/account/index.js
@@ -1,10 +1,25 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
+import UpdateUsername from './components/update_username.vue';
import deleteAccountModal from './components/delete_account_modal.vue';
export default () => {
Vue.use(Translate);
+ const updateUsernameElement = document.getElementById('update-username');
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: updateUsernameElement,
+ components: {
+ UpdateUsername,
+ },
+ render(createElement) {
+ return createElement('update-username', {
+ props: { ...updateUsernameElement.dataset },
+ });
+ },
+ });
+
const deleteAccountButton = document.getElementById('delete-account-button');
const deleteAccountModalEl = document.getElementById('delete-account-modal');
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 7dd3e9858c6..2da022fde63 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -233,21 +233,21 @@ export default class SearchAutocomplete {
const issueItems = [
{
text: 'Issues assigned to me',
- url: `${issuesPath}/?assignee_username=${userName}`,
+ url: `${issuesPath}/?assignee_id=${userId}`,
},
{
text: "Issues I've created",
- url: `${issuesPath}/?author_username=${userName}`,
+ url: `${issuesPath}/?author_id=${userId}`,
},
];
const mergeRequestItems = [
{
text: 'Merge requests assigned to me',
- url: `${mrPath}/?assignee_username=${userName}`,
+ url: `${mrPath}/?assignee_id=${userId}`,
},
{
text: "Merge requests I've created",
- url: `${mrPath}/?author_username=${userName}`,
+ url: `${mrPath}/?author_id=${userId}`,
},
];
diff --git a/app/assets/javascripts/shared/popover.js b/app/assets/javascripts/shared/popover.js
new file mode 100644
index 00000000000..3fc03553bdd
--- /dev/null
+++ b/app/assets/javascripts/shared/popover.js
@@ -0,0 +1,33 @@
+import $ from 'jquery';
+import _ from 'underscore';
+
+export function togglePopover(show) {
+ const isAlreadyShown = this.hasClass('js-popover-show');
+ if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
+ return false;
+ }
+ this.popover(show ? 'show' : 'hide');
+ this.toggleClass('disable-animation js-popover-show', show);
+
+ return true;
+}
+
+export function mouseleave() {
+ if (!$('.popover:hover').length > 0) {
+ const $popover = $(this);
+ togglePopover.call($popover, false);
+ }
+}
+
+export function mouseenter() {
+ const $popover = $(this);
+
+ const showedPopover = togglePopover.call($popover, true);
+ if (showedPopover) {
+ $('.popover').on('mouseleave', mouseleave.bind($popover));
+ }
+}
+
+export function debouncedMouseleave(debounceTimeout = 300) {
+ return _.debounce(mouseleave, debounceTimeout);
+}
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index 25f39e4fdb6..9f69f110d06 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,12 +1,15 @@
+import { visitUrl } from './lib/utils/url_utility';
+
/**
* Helper function that finds the href of the fiven selector and updates the location.
*
* @param {String} selector
*/
-export default (selector) => {
- const link = document.querySelector(selector).getAttribute('href');
+export default function findAndFollowLink(selector) {
+ const element = document.querySelector(selector);
+ const link = element && element.getAttribute('href');
if (link) {
- window.location = link;
+ visitUrl(link);
}
-};
+}
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js
deleted file mode 100644
index 2d324c71379..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export default {
- name: 'time-tracking-estimate-only-pane',
- props: {
- timeEstimateHumanReadable: {
- type: String,
- required: true,
- },
- },
- template: `
- <div class="time-tracking-estimate-only-pane">
- <span class="bold">
- {{ s__('TimeTracking|Estimated:') }}
- </span>
- {{ timeEstimateHumanReadable }}
- </div>
- `,
-};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue
new file mode 100644
index 00000000000..08fce597e50
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue
@@ -0,0 +1,20 @@
+<script>
+export default {
+ name: 'TimeTrackingEstimateOnlyPane',
+ props: {
+ timeEstimateHumanReadable: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="time-tracking-estimate-only-pane">
+ <span class="bold">
+ {{ s__('TimeTracking|Estimated:') }}
+ </span>
+ {{ timeEstimateHumanReadable }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 19f74ad3c6d..825063d9ba6 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -1,7 +1,8 @@
+<script>
import { sprintf, s__ } from '../../../locale';
export default {
- name: 'time-tracking-help-state',
+ name: 'TimeTrackingHelpState',
props: {
rootPath: {
type: String,
@@ -27,26 +28,28 @@ export default {
);
},
},
- template: `
- <div class="time-tracking-help-state">
- <div class="time-tracking-info">
- <h4>
- {{ __('Track time with quick actions') }}
- </h4>
- <p>
- {{ __('Quick actions can be used in the issues description and comment boxes.') }}
- </p>
- <p v-html="estimateText">
- </p>
- <p v-html="spendText">
- </p>
- <a
- class="btn btn-default learn-more-button"
- :href="href"
- >
- {{ __('Learn more') }}
- </a>
- </div>
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="time-tracking-help-state">
+ <div class="time-tracking-info">
+ <h4>
+ {{ __('Track time with quick actions') }}
+ </h4>
+ <p>
+ {{ __('Quick actions can be used in the issues description and comment boxes.') }}
+ </p>
+ <p v-html="estimateText">
+ </p>
+ <p v-html="spendText">
+ </p>
+ <a
+ class="btn btn-default learn-more-button"
+ :href="href"
+ >
+ {{ __('Learn more') }}
+ </a>
+ </div>
+ </div>
+</template>
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 1c641c73ea3..71dca498b3d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,9 +1,9 @@
<script>
-import timeTrackingHelpState from './help_state';
+import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane';
import timeTrackingNoTrackingPane from './no_tracking_pane';
-import timeTrackingEstimateOnlyPane from './estimate_only_pane';
+import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
import eventHub from '../../event_hub';
@@ -12,11 +12,11 @@ export default {
name: 'IssuableTimeTracker',
components: {
TimeTrackingCollapsedState,
- 'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
+ TimeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
TimeTrackingComparisonPane,
- 'time-tracking-help-state': timeTrackingHelpState,
+ TimeTrackingHelpState,
},
props: {
time_estimate: {
diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js
new file mode 100644
index 00000000000..81ec483f2d9
--- /dev/null
+++ b/app/assets/javascripts/snippet/snippet_embed.js
@@ -0,0 +1,23 @@
+export default () => {
+ const { protocol, host, pathname } = location;
+ const shareBtn = document.querySelector('.js-share-btn');
+ const embedBtn = document.querySelector('.js-embed-btn');
+ const snippetUrlArea = document.querySelector('.js-snippet-url-area');
+ const embedAction = document.querySelector('.js-embed-action');
+ const url = `${protocol}//${host + pathname}`;
+
+ shareBtn.addEventListener('click', () => {
+ shareBtn.classList.add('is-active');
+ embedBtn.classList.remove('is-active');
+ snippetUrlArea.value = url;
+ embedAction.innerText = 'Share';
+ });
+
+ embedBtn.addEventListener('click', () => {
+ embedBtn.classList.add('is-active');
+ shareBtn.classList.remove('is-active');
+ const scriptTag = `<script src="${url}.js"></script>`;
+ snippetUrlArea.value = scriptTag;
+ embedAction.innerText = 'Embed';
+ });
+};
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index f3b961eb109..520a0b3f424 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -5,6 +5,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
+import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
@@ -441,7 +442,7 @@ function UsersSelect(currentUser, els, options = {}) {
return;
}
if ($el.closest('.add-issues-modal').length) {
- gl.issueBoards.ModalStore.store.filter[$dropdown.data('fieldName')] = user.id;
+ ModalStore.store.filter[$dropdown.data('fieldName')] = user.id;
} else if (handleClick) {
e.preventDefault();
handleClick(user, isMarking);
diff --git a/app/assets/javascripts/visibility_select.js b/app/assets/javascripts/visibility_select.js
deleted file mode 100644
index 0c928d0d5f6..00000000000
--- a/app/assets/javascripts/visibility_select.js
+++ /dev/null
@@ -1,21 +0,0 @@
-export default class VisibilitySelect {
- constructor(container) {
- if (!container) throw new Error('VisibilitySelect requires a container element as argument 1');
- this.container = container;
- this.helpBlock = this.container.querySelector('.help-block');
- this.select = this.container.querySelector('select');
- }
-
- init() {
- if (this.select) {
- this.updateHelpText();
- this.select.addEventListener('change', this.updateHelpText.bind(this));
- } else {
- this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock;
- }
- }
-
- updateHelpText() {
- this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description;
- }
-}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
index 95c8b0a4c55..f012f9c6772 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
@@ -146,8 +146,8 @@ export default {
</p>
<p
v-if="shouldShowMemoryGraph"
- class="usage-info js-usage-info">
- {{ memoryChangeMessage }}
+ class="usage-info js-usage-info"
+ v-html="memoryChangeMessage">
</p>
<p
v-if="shouldShowLoadFailure"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 54a98abf860..48dff8c4916 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -1,56 +1,61 @@
<script>
- /* eslint-disable vue/require-default-prop */
- import pipelineStage from '~/pipelines/components/stage.vue';
- import ciIcon from '~/vue_shared/components/ci_icon.vue';
- import icon from '~/vue_shared/components/icon.vue';
+/* eslint-disable vue/require-default-prop */
+import PipelineStage from '~/pipelines/components/stage.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
- export default {
- name: 'MRWidgetPipeline',
- components: {
- pipelineStage,
- ciIcon,
- icon,
+export default {
+ name: 'MRWidgetPipeline',
+ components: {
+ PipelineStage,
+ CiIcon,
+ Icon,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
},
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- // This prop needs to be camelCase, html attributes are case insensive
- // https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
- hasCi: {
- type: Boolean,
- required: false,
- },
- ciStatus: {
- type: String,
- required: false,
- },
+ // This prop needs to be camelCase, html attributes are case insensive
+ // https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
+ hasCi: {
+ type: Boolean,
+ required: false,
},
- computed: {
- hasPipeline() {
- return this.pipeline && Object.keys(this.pipeline).length > 0;
- },
- hasCIError() {
- return this.hasCi && !this.ciStatus;
- },
- status() {
- return this.pipeline.details &&
- this.pipeline.details.status ? this.pipeline.details.status : {};
- },
- hasStages() {
- return this.pipeline.details &&
- this.pipeline.details.stages &&
- this.pipeline.details.stages.length;
- },
+ ciStatus: {
+ type: String,
+ required: false,
},
- };
+ },
+ computed: {
+ hasPipeline() {
+ return this.pipeline && Object.keys(this.pipeline).length > 0;
+ },
+ hasCIError() {
+ return this.hasCi && !this.ciStatus;
+ },
+ status() {
+ return this.pipeline.details && this.pipeline.details.status
+ ? this.pipeline.details.status
+ : {};
+ },
+ hasStages() {
+ return (
+ this.pipeline.details && this.pipeline.details.stages && this.pipeline.details.stages.length
+ );
+ },
+ hasCommitInfo() {
+ return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
+ },
+ },
+};
</script>
<template>
<div
v-if="hasPipeline || hasCIError"
- class="mr-widget-heading">
+ class="mr-widget-heading"
+ >
<div class="ci-widget media">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
@@ -77,13 +82,17 @@
#{{ pipeline.id }}
</a>
- {{ pipeline.details.status.label }} for
+ {{ pipeline.details.status.label }}
- <a
- :href="pipeline.commit.commit_path"
- class="commit-sha js-commit-link"
- >
- {{ pipeline.commit.short_id }}</a>.
+ <template v-if="hasCommitInfo">
+ for
+
+ <a
+ :href="pipeline.commit.commit_path"
+ class="commit-sha js-commit-link"
+ >
+ {{ pipeline.commit.short_id }}</a>.
+ </template>
<span class="mr-widget-pipeline-graph">
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
deleted file mode 100644
index 4d9a2ca530f..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import statusIcon from '../mr_widget_status_icon.vue';
-
-export default {
- name: 'MRWidgetPipelineBlocked',
- components: {
- statusIcon,
- },
- template: `
- <div class="mr-widget-body media">
- <status-icon status="warning" :show-disabled-button="true" />
- <div class="media-body space-children">
- <span class="bold">
- The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
- </span>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
new file mode 100644
index 00000000000..8d55477929f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -0,0 +1,25 @@
+<script>
+import statusIcon from '../mr_widget_status_icon.vue';
+
+export default {
+ name: 'PipelineFailed',
+ components: {
+ statusIcon,
+ },
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon
+ status="warning"
+ :show-disabled-button="true"
+ />
+ <div class="media-body space-children">
+ <span class="bold">
+ {{ s__(`mrWidget|The pipeline for this merge request failed.
+Please retry the job or push a new commit to fix the failure`) }}
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 3c781ccddc8..0264625a526 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -1,3 +1,4 @@
+<script>
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
@@ -7,7 +8,10 @@ import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
- name: 'MRWidgetReadyToMerge',
+ name: 'ReadyToMerge',
+ components: {
+ statusIcon,
+ },
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
@@ -26,9 +30,6 @@ export default {
warningSvg,
};
},
- components: {
- statusIcon,
- },
computed: {
shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive;
@@ -217,136 +218,146 @@ export default {
});
},
},
- template: `
- <div class="mr-widget-body media">
- <status-icon :status="iconClass" />
- <div class="media-body">
- <div class="mr-widget-body-controls media space-children">
- <span class="btn-group append-bottom-5">
- <button
- @click="handleMergeButtonClick()"
- :disabled="isMergeButtonDisabled"
- :class="mergeButtonClass"
- type="button"
- class="qa-merge-button">
- <i
- v-if="isMakingRequest"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- {{mergeButtonText}}
- </button>
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon :status="iconClass" />
+ <div class="media-body">
+ <div class="mr-widget-body-controls media space-children">
+ <span class="btn-group append-bottom-5">
+ <button
+ @click="handleMergeButtonClick()"
+ :disabled="isMergeButtonDisabled"
+ :class="mergeButtonClass"
+ type="button"
+ class="qa-merge-button">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ ></i>
+ {{ mergeButtonText }}
+ </button>
+ <button
+ v-if="shouldShowMergeOptionsDropdown"
+ :disabled="isMergeButtonDisabled"
+ type="button"
+ class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
+ data-toggle="dropdown"
+ aria-label="Select merge moment">
+ <i
+ class="fa fa-chevron-down"
+ aria-hidden="true"
+ ></i>
+ </button>
+ <ul
+ v-if="shouldShowMergeOptionsDropdown"
+ class="dropdown-menu dropdown-menu-right"
+ role="menu">
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(true)"
+ class="merge_when_pipeline_succeeds"
+ href="#">
+ <span class="media">
+ <span
+ v-html="successSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(false, true)"
+ class="accept-merge-request"
+ href="#">
+ <span class="media">
+ <span
+ v-html="warningSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="media-body merge-opt-title">Merge immediately</span>
+ </span>
+ </a>
+ </li>
+ </ul>
+ </span>
+ <div class="media-body-wrap space-children">
+ <template v-if="shouldShowMergeControls()">
+ <label v-if="mr.canRemoveSourceBranch">
+ <input
+ id="remove-source-branch-input"
+ v-model="removeSourceBranch"
+ class="js-remove-source-branch-checkbox"
+ :disabled="isRemoveSourceBranchButtonDisabled"
+ type="checkbox"/> Remove source branch
+ </label>
+
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ :mr="mr"
+ :is-merge-button-disabled="isMergeButtonDisabled" />
+
+ <span
+ v-if="mr.ffOnlyEnabled"
+ class="js-fast-forward-message">
+ Fast-forward merge without a merge commit
+ </span>
<button
- v-if="shouldShowMergeOptionsDropdown"
+ v-else
+ @click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
- type="button"
- class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
- data-toggle="dropdown"
- aria-label="Select merge moment">
- <i
- class="fa fa-chevron-down"
- aria-hidden="true" />
+ class="js-modify-commit-message-button btn btn-default btn-xs"
+ type="button">
+ Modify commit message
</button>
- <ul
- v-if="shouldShowMergeOptionsDropdown"
- class="dropdown-menu dropdown-menu-right"
- role="menu">
- <li>
- <a
- @click.prevent="handleMergeButtonClick(true)"
- class="merge_when_pipeline_succeeds"
- href="#">
- <span class="media">
- <span
- v-html="successSvg"
- class="merge-opt-icon"
- aria-hidden="true"></span>
- <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
- </span>
- </a>
- </li>
- <li>
- <a
- @click.prevent="handleMergeButtonClick(false, true)"
- class="accept-merge-request"
- href="#">
- <span class="media">
- <span
- v-html="warningSvg"
- class="merge-opt-icon"
- aria-hidden="true"></span>
- <span class="media-body merge-opt-title">Merge immediately</span>
- </span>
- </a>
- </li>
- </ul>
- </span>
- <div class="media-body-wrap space-children">
- <template v-if="shouldShowMergeControls()">
- <label v-if="mr.canRemoveSourceBranch">
- <input
- id="remove-source-branch-input"
- v-model="removeSourceBranch"
- class="js-remove-source-branch-checkbox"
- :disabled="isRemoveSourceBranchButtonDisabled"
- type="checkbox"/> Remove source branch
- </label>
-
- <!-- Placeholder for EE extension of this component -->
- <squash-before-merge
- v-if="shouldShowSquashBeforeMerge"
- :mr="mr"
- :is-merge-button-disabled="isMergeButtonDisabled" />
-
- <span
- v-if="mr.ffOnlyEnabled"
- class="js-fast-forward-message">
- Fast-forward merge without a merge commit
- </span>
- <button
- v-else
- @click="toggleCommitMessageEditor"
- :disabled="isMergeButtonDisabled"
- class="js-modify-commit-message-button btn btn-default btn-xs"
- type="button">
- Modify commit message
- </button>
- </template>
- <template v-else>
- <span class="bold js-resolve-mr-widget-items-message">
- You can only merge once the items above are resolved
- </span>
- </template>
- </div>
+ </template>
+ <template v-else>
+ <span class="bold js-resolve-mr-widget-items-message">
+ You can only merge once the items above are resolved
+ </span>
+ </template>
</div>
- <div
- v-if="showCommitMessageEditor"
- class="prepend-top-default commit-message-editor">
- <div class="form-group clearfix">
- <label
- class="control-label"
- for="commit-message">
- Commit message
- </label>
- <div class="col-sm-10">
- <div class="commit-message-container">
- <div class="max-width-marker"></div>
- <textarea
- v-model="commitMessage"
- class="form-control js-commit-message"
- required="required"
- rows="14"
- name="Commit message"></textarea>
- </div>
- <p class="hint">Try to keep the first line under 52 characters and the others under 72</p>
- <div class="hint">
- <a
- @click.prevent="updateCommitMessage"
- href="#">{{commitMessageLinkTitle}}</a>
- </div>
+ </div>
+ <div
+ v-if="showCommitMessageEditor"
+ class="prepend-top-default commit-message-editor">
+ <div class="form-group clearfix">
+ <label
+ class="control-label"
+ for="commit-message">
+ Commit message
+ </label>
+ <div class="col-sm-10">
+ <div class="commit-message-container">
+ <div class="max-width-marker"></div>
+ <textarea
+ id="commit-message"
+ v-model="commitMessage"
+ class="form-control js-commit-message"
+ required="required"
+ rows="14"
+ name="Commit message"></textarea>
+ </div>
+ <p class="hint">
+ Try to keep the first line under 52 characters and the others under 72
+ </p>
+ <div class="hint">
+ <a
+ @click.prevent="updateCommitMessage"
+ href="#"
+ >
+ {{ commitMessageLinkTitle }}
+ </a>
</div>
</div>
</div>
</div>
</div>
- `,
-};
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index 9ade6a91747..a1f7e696795 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -7,7 +7,10 @@ export default {
statusIcon,
},
props: {
- mr: { type: Object, required: true },
+ mr: {
+ type: Object,
+ required: true,
+ },
},
};
</script>
@@ -20,13 +23,14 @@ export default {
/>
<div class="media-body space-children">
<span class="bold">
- There are unresolved discussions. Please resolve these discussions
+ {{ s__("mrWidget|There are unresolved discussions. Please resolve these discussions") }}
</span>
<a
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
- class="btn btn-default btn-xs js-create-issue">
- Create an issue to resolve them later
+ class="btn btn-default btn-xs js-create-issue"
+ >
+ {{ s__("mrWidget|Create an issue to resolve them later") }}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index ed15fc6ab0f..3b5c973e4a0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -27,11 +27,11 @@ export { default as ConflictsState } from './components/states/mr_widget_conflic
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
-export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge';
+export { default as ReadyToMergeState } from './components/states/ready_to_merge.vue';
export { default as ShaMismatchState } from './components/states/sha_mismatch.vue';
export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
-export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
+export { default as PipelineFailedState } from './components/states/pipeline_failed.vue';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 97789636787..b8875d04488 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -175,7 +175,7 @@
</a>
</span>
<span v-else>
- Cant find HEAD commit for this branch
+ Can't find HEAD commit for this branch
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
new file mode 100644
index 00000000000..4155e1bab9c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
@@ -0,0 +1,58 @@
+<script>
+import { viewerInformationForPath } from './lib/viewer_utils';
+import MarkdownViewer from './viewers/markdown_viewer.vue';
+import ImageViewer from './viewers/image_viewer.vue';
+import DownloadViewer from './viewers/download_viewer.vue';
+
+export default {
+ props: {
+ content: {
+ type: String,
+ default: '',
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ fileSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ viewer() {
+ if (!this.path) return null;
+
+ const previewInfo = viewerInformationForPath(this.path);
+ if (!previewInfo) return DownloadViewer;
+
+ switch (previewInfo.id) {
+ case 'markdown':
+ return MarkdownViewer;
+ case 'image':
+ return ImageViewer;
+ default:
+ return DownloadViewer;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="preview-container">
+ <component
+ :is="viewer"
+ :path="path"
+ :file-size="fileSize"
+ :project-path="projectPath"
+ :content="content"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
new file mode 100644
index 00000000000..f01a51da0b3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
@@ -0,0 +1,32 @@
+const viewers = {
+ image: {
+ id: 'image',
+ },
+ markdown: {
+ id: 'markdown',
+ previewTitle: 'Preview Markdown',
+ },
+};
+
+const fileNameViewers = {};
+const fileExtensionViewers = {
+ jpg: 'image',
+ jpeg: 'image',
+ gif: 'image',
+ png: 'image',
+ bmp: 'image',
+ ico: 'image',
+ md: 'markdown',
+ markdown: 'markdown',
+};
+
+export function viewerInformationForPath(path) {
+ if (!path) return null;
+ const name = path.split('/').pop();
+ const viewerName =
+ fileNameViewers[name] || fileExtensionViewers[name ? name.split('.').pop() : ''] || '';
+
+ return viewers[viewerName];
+}
+
+export default viewers;
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
new file mode 100644
index 00000000000..395a71acccf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -0,0 +1,52 @@
+<script>
+import Icon from '../../icon.vue';
+import { numberToHumanSize } from '../../../../lib/utils/number_utils';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ fileSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ computed: {
+ fileSizeReadable() {
+ return numberToHumanSize(this.fileSize);
+ },
+ fileName() {
+ return this.path.split('/').pop();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="file-container">
+ <div class="file-content">
+ <p class="prepend-top-10 file-info">
+ {{ fileName }} ({{ fileSizeReadable }})
+ </p>
+ <a
+ :href="path"
+ class="btn btn-default"
+ rel="nofollow"
+ download
+ target="_blank">
+ <icon
+ name="download"
+ css-classes="pull-left append-right-8"
+ :size="16"
+ />
+ {{ __('Download') }}
+ </a>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
new file mode 100644
index 00000000000..a5999f909ca
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -0,0 +1,68 @@
+<script>
+import { numberToHumanSize } from '../../../../lib/utils/number_utils';
+
+export default {
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ fileSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ data() {
+ return {
+ width: 0,
+ height: 0,
+ isZoomable: false,
+ isZoomed: false,
+ };
+ },
+ computed: {
+ fileSizeReadable() {
+ return numberToHumanSize(this.fileSize);
+ },
+ },
+ methods: {
+ onImgLoad() {
+ const contentImg = this.$refs.contentImg;
+ this.isZoomable =
+ contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height;
+
+ this.width = contentImg.naturalWidth;
+ this.height = contentImg.naturalHeight;
+ },
+ onImgClick() {
+ if (this.isZoomable) this.isZoomed = !this.isZoomed;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="file-container">
+ <div class="file-content image_file">
+ <img
+ ref="contentImg"
+ :class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }"
+ :src="path"
+ :alt="path"
+ @load="onImgLoad"
+ @click="onImgClick"/>
+ <p class="file-info prepend-top-10">
+ <template v-if="fileSize>0">
+ {{ fileSizeReadable }}
+ </template>
+ <template v-if="fileSize>0 && width && height">
+ -
+ </template>
+ <template v-if="width && height">
+ {{ width }} x {{ height }}
+ </template>
+ </p>
+ </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
new file mode 100644
index 00000000000..09e0094054d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -0,0 +1,90 @@
+<script>
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import $ from 'jquery';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+
+const CancelToken = axios.CancelToken;
+let axiosSource;
+
+export default {
+ components: {
+ SkeletonLoadingContainer,
+ },
+ props: {
+ content: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ previewContent: null,
+ isLoading: false,
+ };
+ },
+ watch: {
+ content() {
+ this.previewContent = null;
+ },
+ },
+ created() {
+ axiosSource = CancelToken.source();
+ this.fetchMarkdownPreview();
+ },
+ updated() {
+ this.fetchMarkdownPreview();
+ },
+ destroyed() {
+ if (this.isLoading) axiosSource.cancel('Cancelling Preview');
+ },
+ methods: {
+ fetchMarkdownPreview() {
+ if (this.content && this.previewContent === null) {
+ this.isLoading = true;
+ const postBody = {
+ text: this.content,
+ };
+ const postOptions = {
+ cancelToken: axiosSource.token,
+ };
+
+ axios
+ .post(
+ `${gon.relative_url_root}/${this.projectPath}/preview_markdown`,
+ postBody,
+ postOptions,
+ )
+ .then(({ data }) => {
+ this.previewContent = data.body;
+ this.isLoading = false;
+
+ this.$nextTick(() => {
+ $(this.$refs['markdown-preview']).renderGFM();
+ });
+ })
+ .catch(() => {
+ this.previewContent = __('An error occurred while fetching markdown preview');
+ this.isLoading = false;
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ ref="markdown-preview"
+ class="md md-previewer">
+ <skeleton-loading-container v-if="isLoading" />
+ <div
+ v-else
+ v-html="previewContent">
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue
index 67c9181c7b1..f28e5e2715d 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue
@@ -1,47 +1,42 @@
<script>
- const buttonVariants = [
- 'danger',
- 'primary',
- 'success',
- 'warning',
- ];
+const buttonVariants = ['danger', 'primary', 'success', 'warning'];
- export default {
- name: 'GlModal',
+export default {
+ name: 'GlModal',
- props: {
- id: {
- type: String,
- required: false,
- default: null,
- },
- headerTitleText: {
- type: String,
- required: false,
- default: '',
- },
- footerPrimaryButtonVariant: {
- type: String,
- required: false,
- default: 'primary',
- validator: value => buttonVariants.indexOf(value) !== -1,
- },
- footerPrimaryButtonText: {
- type: String,
- required: false,
- default: '',
- },
+ props: {
+ id: {
+ type: String,
+ required: false,
+ default: null,
},
+ headerTitleText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ footerPrimaryButtonVariant: {
+ type: String,
+ required: false,
+ default: 'primary',
+ validator: value => buttonVariants.includes(value),
+ },
+ footerPrimaryButtonText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
- methods: {
- emitCancel(event) {
- this.$emit('cancel', event);
- },
- emitSubmit(event) {
- this.$emit('submit', event);
- },
+ methods: {
+ emitCancel(event) {
+ this.$emit('cancel', event);
+ },
+ emitSubmit(event) {
+ this.$emit('submit', event);
},
- };
+ },
+};
</script>
<template>
@@ -60,7 +55,7 @@
<slot name="header">
<button
type="button"
- class="close"
+ class="close js-modal-close-action"
data-dismiss="modal"
:aria-label="s__('Modal|Close')"
@click="emitCancel($event)"
@@ -83,7 +78,7 @@
<slot name="footer">
<button
type="button"
- class="btn"
+ class="btn js-modal-cancel-action"
data-dismiss="modal"
@click="emitCancel($event)"
>
@@ -91,7 +86,7 @@
</button>
<button
type="button"
- class="btn"
+ class="btn js-modal-primary-action"
:class="`btn-${footerPrimaryButtonVariant}`"
data-dismiss="modal"
@click="emitSubmit($event)"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index d91fe3cf0c5..db453c30576 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -27,20 +27,22 @@
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
methods: {
- isMarkdownForm(form) {
- return form && !form.find('.js-vue-markdown-field').length;
+ isValid(form) {
+ return !form ||
+ form.find('.js-vue-markdown-field').length ||
+ $(this.$el).closest('form') === form[0];
},
previewMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
- if (this.isMarkdownForm(form)) return;
+ if (!this.isValid(form)) return;
this.$emit('preview-markdown');
},
writeMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
- if (this.isMarkdownForm(form)) return;
+ if (!this.isValid(form)) return;
this.$emit('write-markdown');
},
diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
index b06493e6c66..16304e4815d 100644
--- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
+++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
@@ -9,7 +9,7 @@
lines: {
type: Number,
required: false,
- default: 6,
+ default: 3,
},
},
computed: {
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 0665622fe4a..f2950308019 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -37,7 +37,11 @@
/*
* Code highlight
*/
-@import "highlight/**/*";
+@import "highlight/dark";
+@import "highlight/monokai";
+@import "highlight/solarized_dark";
+@import "highlight/solarized_light";
+@import "highlight/white";
/*
* Styles for JS behaviors.
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 728f9a27aca..14cd32da9eb 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -187,12 +187,9 @@ a {
animation: fadeInFull $fade-in-duration 1;
}
-
.animation-container {
- background: $repo-editor-grey;
height: 40px;
overflow: hidden;
- position: relative;
&.animation-container-small {
height: 12px;
@@ -205,60 +202,43 @@ a {
}
}
- &::before {
- animation-duration: 1s;
- animation-fill-mode: forwards;
- animation-iteration-count: infinite;
- animation-name: blockTextShine;
- animation-timing-function: linear;
- background-image: $repo-editor-linear-gradient;
- background-repeat: no-repeat;
- background-size: 800px 45px;
- content: ' ';
- display: block;
- height: 100%;
+ [class^="skeleton-line-"] {
position: relative;
- }
-
- div {
- background: $white-light;
- height: 6px;
- left: 0;
- position: absolute;
- right: 0;
- }
-
- .skeleton-line-1 {
- left: 0;
- top: 8px;
- }
-
- .skeleton-line-2 {
- left: 150px;
- top: 0;
+ background-color: $theme-gray-100;
height: 10px;
- }
+ overflow: hidden;
- .skeleton-line-3 {
- left: 0;
- top: 23px;
- }
+ &:not(:last-of-type) {
+ margin-bottom: 4px;
+ }
- .skeleton-line-4 {
- left: 0;
- top: 38px;
+ &::after {
+ content: ' ';
+ display: block;
+ animation: blockTextShine 1s linear infinite forwards;
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-image: linear-gradient(
+ to right,
+ $theme-gray-100 0%,
+ $theme-gray-50 20%,
+ $theme-gray-100 40%,
+ $theme-gray-100 100%
+ );
+ height: 10px;
+ }
}
+}
- .skeleton-line-5 {
- left: 200px;
- top: 28px;
- height: 10px;
- }
+$skeleton-line-widths: (
+ 156px,
+ 235px,
+ 200px,
+);
- .skeleton-line-6 {
- top: 14px;
- left: 230px;
- height: 10px;
+@for $count from 1 through length($skeleton-line-widths) {
+ .skeleton-line-#{$count} {
+ width: nth($skeleton-line-widths, $count);
}
}
diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss
index 6433b0c7855..02f3896d591 100644
--- a/app/assets/stylesheets/framework/banner.scss
+++ b/app/assets/stylesheets/framework/banner.scss
@@ -1,7 +1,7 @@
.banner-callout {
display: flex;
position: relative;
- flex-wrap: wrap;
+ align-items: start;
.banner-close {
position: absolute;
@@ -16,10 +16,25 @@
}
.banner-graphic {
- margin: 20px auto;
+ margin: 0 $gl-padding $gl-padding 0;
}
&.banner-non-empty-state {
border-bottom: 1px solid $border-color;
}
+
+ @media (max-width: $screen-xs-max) {
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+
+ .banner-title,
+ .banner-buttons {
+ text-align: center;
+ }
+
+ .banner-graphic {
+ margin-left: $gl-padding;
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 6b89387ab5f..f4f5926e198 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -422,25 +422,43 @@
}
}
-.btn-link.btn-secondary-hover-link {
- color: $gl-text-color-secondary;
+.btn-link {
+ padding: 0;
+ background-color: transparent;
+ color: $blue-600;
+ font-weight: normal;
+ border-radius: 0;
+ border-color: transparent;
&:hover,
&:active,
&:focus {
- color: $gl-link-color;
- text-decoration: none;
+ color: $blue-800;
+ text-decoration: underline;
+ background-color: transparent;
+ border-color: transparent;
}
-}
-.btn-link.btn-primary-hover-link {
- color: inherit;
+ &.btn-secondary-hover-link {
+ color: $gl-text-color-secondary;
- &:hover,
- &:active,
- &:focus {
- color: $gl-link-color;
- text-decoration: none;
+ &:hover,
+ &:active,
+ &:focus {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+ }
+
+ &.btn-primary-hover-link {
+ color: inherit;
+
+ &:hover,
+ &:active,
+ &:focus {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
}
}
@@ -485,3 +503,7 @@ fieldset[disabled] .btn,
@extend %disabled;
}
}
+
+.btn-no-padding {
+ padding: 0;
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cc74cb72795..cc5fac6816d 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -481,7 +481,8 @@
.dropdown-menu-selectable {
li {
- a {
+ a,
+ button {
padding: 8px 40px;
position: relative;
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index df1cafc9f8e..62a0fba3da3 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -20,7 +20,7 @@
width: 100%;
}
- $image-widths: 80 250 306 394 430;
+ $image-widths: 80 130 250 306 394 430;
@each $width in $image-widths {
&.svg-#{$width} {
img,
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 7e829826eba..f1a8a46dda4 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -24,6 +24,10 @@
color: $list-text-disabled-color;
}
+ &:not(.ui-sort-disabled):hover {
+ background: $row-hover;
+ }
+
&.unstyled {
&:hover {
background: none;
@@ -34,14 +38,15 @@
background-color: $list-warning-row-bg;
border-color: $list-warning-row-border;
color: $list-warning-row-color;
- }
- &.smoke { background-color: $gray-light; }
+ &:hover {
+ background: $list-warning-row-bg;
+ }
- &:not(.ui-sort-disabled):hover {
- background: $row-hover;
}
+ &.smoke { background-color: $gray-light; }
+
&:last-child {
border-bottom: 0;
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index 7829d722560..34fccf6f0a4 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 100;
+ $section-widths: 10 15 20 25 30 40 50 100;
@each $width in $section-widths {
&.section-#{$width} {
flex: 0 0 #{$width + '%'};
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 30c15c231d5..606d4675f19 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -29,8 +29,10 @@
}
.snippet-title {
- font-size: 24px;
+ color: $gl-text-color;
+ font-size: 2em;
font-weight: $gl-font-weight-bold;
+ min-height: $header-height;
}
.snippet-edited-ago {
@@ -46,3 +48,26 @@
.snippet-scope-menu .btn-new {
margin-top: 15px;
}
+
+.snippet-embed-input {
+ height: 35px;
+}
+
+.embed-snippet {
+ padding-right: 0;
+ padding-top: $gl-padding;
+
+ .form-control {
+ cursor: auto;
+ width: 101%;
+ margin-left: -1px;
+ }
+
+ .embed-toggle-list li button {
+ padding: 8px 40px;
+ }
+
+ .embed-toggle {
+ height: 35px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 294c59f037f..9e1371648ed 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -289,6 +289,11 @@ body {
&:last-child {
margin-bottom: 0;
}
+
+ &.with-button {
+ line-height: 34px;
+ }
+
}
.page-title-empty {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index a81904d5338..37223175199 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -714,20 +714,6 @@ $color-average-score: $orange-400;
$color-low-score: $red-400;
/*
-Repo editor
-*/
-$repo-editor-grey: #f6f7f9;
-$repo-editor-grey-darker: #e9ebee;
-$repo-editor-linear-gradient: linear-gradient(
- to right,
- $repo-editor-grey 0%,
- $repo-editor-grey-darker,
- 20%,
- $repo-editor-grey 40%,
- $repo-editor-grey 100%
-);
-
-/*
Performance Bar
*/
$perf-bar-text: #999;
@@ -767,3 +753,8 @@ $border-color-settings: #e1e1e1;
Modals
*/
$modal-body-height: 134px;
+
+/*
+Prometheus
+*/
+$prometheus-table-row-highlight-color: $theme-gray-100;
diff --git a/app/assets/stylesheets/highlight/embedded.scss b/app/assets/stylesheets/highlight/embedded.scss
new file mode 100644
index 00000000000..44c8a1d39ec
--- /dev/null
+++ b/app/assets/stylesheets/highlight/embedded.scss
@@ -0,0 +1,3 @@
+.code {
+ @import "white_base";
+}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index c3d8f0c61a2..355c8d223f7 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,292 +1,3 @@
-/* https://github.com/aahan/pygments-github-style */
-
-/*
-* White Syntax Colors
-*/
-$white-code-color: $gl-text-color;
-$white-highlight: #fafe3d;
-$white-pre-hll-bg: #f8eec7;
-$white-hll-bg: #f8f8f8;
-$white-over-bg: #ded7fc;
-$white-expanded-border: #e0e0e0;
-$white-expanded-bg: #f7f7f7;
-$white-c: #998;
-$white-err: #a61717;
-$white-err-bg: #e3d2d2;
-$white-cm: #998;
-$white-cp: #999;
-$white-c1: #998;
-$white-cs: #999;
-$white-gd: $black;
-$white-gd-bg: #fdd;
-$white-gd-x: $black;
-$white-gd-x-bg: #faa;
-$white-gr: #a00;
-$white-gh: #999;
-$white-gi: $black;
-$white-gi-bg: #dfd;
-$white-gi-x: $black;
-$white-gi-x-bg: #afa;
-$white-go: #888;
-$white-gp: #555;
-$white-gu: #800080;
-$white-gt: #a00;
-$white-kt: #458;
-$white-m: #099;
-$white-s: #d14;
-$white-n: #333;
-$white-na: teal;
-$white-nb: #0086b3;
-$white-nc: #458;
-$white-no: teal;
-$white-ni: purple;
-$white-ne: #900;
-$white-nf: #900;
-$white-nn: #555;
-$white-nt: navy;
-$white-nv: teal;
-$white-w: #bbb;
-$white-mf: #099;
-$white-mh: #099;
-$white-mi: #099;
-$white-mo: #099;
-$white-sb: #d14;
-$white-sc: #d14;
-$white-sd: #d14;
-$white-s2: #d14;
-$white-se: #d14;
-$white-sh: #d14;
-$white-si: #d14;
-$white-sx: #d14;
-$white-sr: #009926;
-$white-s1: #d14;
-$white-ss: #990073;
-$white-bp: #999;
-$white-vc: teal;
-$white-vg: teal;
-$white-vi: teal;
-$white-il: #099;
-$white-gc-color: #999;
-$white-gc-bg: #eaf2f5;
-
-
-@mixin matchLine {
- color: $black-transparent;
- background-color: $gray-light;
-}
-
.code.white {
- // Line numbers
- .line-numbers,
- .diff-line-num {
- background-color: $gray-light;
- }
-
- .diff-line-num,
- .diff-line-num a {
- color: $black-transparent;
- }
-
- // Code itself
- pre.code,
- .diff-line-num {
- border-color: $white-normal;
- }
-
- &,
- pre.code,
- .line_holder .line_content {
- background-color: $white-light;
- color: $white-code-color;
- }
-
- // Diff line
- .line_holder {
-
- &.match .line_content {
- @include matchLine;
- }
-
- .diff-line-num {
- &.old {
- background-color: $line-number-old;
- border-color: $line-removed-dark;
-
- a {
- color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
- }
- }
-
- &.new {
- background-color: $line-number-new;
- border-color: $line-added-dark;
-
- a {
- color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
- }
- }
-
- &.is-over,
- &.hll:not(.empty-cell).is-over {
- background-color: $white-over-bg;
- border-color: darken($white-over-bg, 5%);
-
- a {
- color: darken($white-over-bg, 15%);
- }
- }
-
- &.hll:not(.empty-cell) {
- background-color: $line-number-select;
- border-color: $line-select-yellow-dark;
- }
- }
-
- &:not(.diff-expanded) + .diff-expanded,
- &.diff-expanded + .line_holder:not(.diff-expanded) {
- > .diff-line-num,
- > .line_content {
- border-top: 1px solid $white-expanded-border;
- }
- }
-
- &.diff-expanded {
- > .diff-line-num,
- > .line_content {
- background: $white-expanded-bg;
- border-color: $white-expanded-bg;
- }
- }
-
- .line_content {
- &.old {
- background-color: $line-removed;
-
- &::before {
- color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
- }
-
- span.idiff {
- background-color: $line-removed-dark;
- }
- }
-
- &.new {
- background-color: $line-added;
-
- &::before {
- color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
- }
-
- span.idiff {
- background-color: $line-added-dark;
- }
- }
-
- &.match {
- @include matchLine;
- }
-
- &.hll:not(.empty-cell) {
- background-color: $line-select-yellow;
- }
- }
- }
-
- // highlight line via anchor
- pre .hll {
- background-color: $white-pre-hll-bg !important;
- }
-
- // Search result highlight
- span.highlight_word {
- background-color: $white-highlight !important;
- }
-
- // Links to URLs, emails, or dependencies
- .line a {
- color: $white-nb;
- }
-
- .hll { background-color: $white-hll-bg; }
- .c { color: $white-c; font-style: italic; }
- .err { color: $white-err; background-color: $white-err-bg; }
- .k { font-weight: $gl-font-weight-bold; }
- .o { font-weight: $gl-font-weight-bold; }
- .cm { color: $white-cm; font-style: italic; }
- .cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
- .c1 { color: $white-c1; font-style: italic; }
- .cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
-
- .gd {
- color: $white-gd;
- background-color: $white-gd-bg;
-
- .x {
- color: $white-gd-x;
- background-color: $white-gd-x-bg;
- }
- }
-
- .ge { font-style: italic; }
- .gr { color: $white-gr; }
- .gh { color: $white-gh; }
-
- .gi {
- color: $white-gi;
- background-color: $white-gi-bg;
-
- .x {
- color: $white-gi-x;
- background-color: $white-gi-x-bg;
- }
- }
-
- .go { color: $white-go; }
- .gp { color: $white-gp; }
- .gs { font-weight: $gl-font-weight-bold; }
- .gu { color: $white-gu; font-weight: $gl-font-weight-bold; }
- .gt { color: $white-gt; }
- .kc { font-weight: $gl-font-weight-bold; }
- .kd { font-weight: $gl-font-weight-bold; }
- .kn { font-weight: $gl-font-weight-bold; }
- .kp { font-weight: $gl-font-weight-bold; }
- .kr { font-weight: $gl-font-weight-bold; }
- .kt { color: $white-kt; font-weight: $gl-font-weight-bold; }
- .m { color: $white-m; }
- .s { color: $white-s; }
- .n { color: $white-n; }
- .na { color: $white-na; }
- .nb { color: $white-nb; }
- .nc { color: $white-nc; font-weight: $gl-font-weight-bold; }
- .no { color: $white-no; }
- .ni { color: $white-ni; }
- .ne { color: $white-ne; font-weight: $gl-font-weight-bold; }
- .nf { color: $white-nf; font-weight: $gl-font-weight-bold; }
- .nn { color: $white-nn; }
- .nt { color: $white-nt; }
- .nv { color: $white-nv; }
- .ow { font-weight: $gl-font-weight-bold; }
- .w { color: $white-w; }
- .mf { color: $white-mf; }
- .mh { color: $white-mh; }
- .mi { color: $white-mi; }
- .mo { color: $white-mo; }
- .sb { color: $white-sb; }
- .sc { color: $white-sc; }
- .sd { color: $white-sd; }
- .s2 { color: $white-s2; }
- .se { color: $white-se; }
- .sh { color: $white-sh; }
- .si { color: $white-si; }
- .sx { color: $white-sx; }
- .sr { color: $white-sr; }
- .s1 { color: $white-s1; }
- .ss { color: $white-ss; }
- .bp { color: $white-bp; }
- .vc { color: $white-vc; }
- .vg { color: $white-vg; }
- .vi { color: $white-vi; }
- .il { color: $white-il; }
- .gc { color: $white-gc-color; background-color: $white-gc-bg; }
+ @import "white_base";
}
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
new file mode 100644
index 00000000000..8cc5252648d
--- /dev/null
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -0,0 +1,290 @@
+/* https://github.com/aahan/pygments-github-style */
+
+/*
+* White Syntax Colors
+*/
+$white-code-color: $gl-text-color;
+$white-highlight: #fafe3d;
+$white-pre-hll-bg: #f8eec7;
+$white-hll-bg: #f8f8f8;
+$white-over-bg: #ded7fc;
+$white-expanded-border: #e0e0e0;
+$white-expanded-bg: #f7f7f7;
+$white-c: #998;
+$white-err: #a61717;
+$white-err-bg: #e3d2d2;
+$white-cm: #998;
+$white-cp: #999;
+$white-c1: #998;
+$white-cs: #999;
+$white-gd: $black;
+$white-gd-bg: #fdd;
+$white-gd-x: $black;
+$white-gd-x-bg: #faa;
+$white-gr: #a00;
+$white-gh: #999;
+$white-gi: $black;
+$white-gi-bg: #dfd;
+$white-gi-x: $black;
+$white-gi-x-bg: #afa;
+$white-go: #888;
+$white-gp: #555;
+$white-gu: #800080;
+$white-gt: #a00;
+$white-kt: #458;
+$white-m: #099;
+$white-s: #d14;
+$white-n: #333;
+$white-na: teal;
+$white-nb: #0086b3;
+$white-nc: #458;
+$white-no: teal;
+$white-ni: purple;
+$white-ne: #900;
+$white-nf: #900;
+$white-nn: #555;
+$white-nt: navy;
+$white-nv: teal;
+$white-w: #bbb;
+$white-mf: #099;
+$white-mh: #099;
+$white-mi: #099;
+$white-mo: #099;
+$white-sb: #d14;
+$white-sc: #d14;
+$white-sd: #d14;
+$white-s2: #d14;
+$white-se: #d14;
+$white-sh: #d14;
+$white-si: #d14;
+$white-sx: #d14;
+$white-sr: #009926;
+$white-s1: #d14;
+$white-ss: #990073;
+$white-bp: #999;
+$white-vc: teal;
+$white-vg: teal;
+$white-vi: teal;
+$white-il: #099;
+$white-gc-color: #999;
+$white-gc-bg: #eaf2f5;
+
+
+@mixin matchLine {
+ color: $black-transparent;
+ background-color: $gray-light;
+}
+
+ // Line numbers
+.line-numbers,
+.diff-line-num {
+ background-color: $gray-light;
+}
+
+.diff-line-num,
+.diff-line-num a {
+ color: $black-transparent;
+}
+
+// Code itself
+pre.code,
+.diff-line-num {
+ border-color: $white-normal;
+}
+
+&,
+pre.code,
+.line_holder .line_content {
+ background-color: $white-light;
+ color: $white-code-color;
+}
+
+// Diff line
+.line_holder {
+
+ &.match .line_content {
+ @include matchLine;
+ }
+
+ .diff-line-num {
+ &.old {
+ background-color: $line-number-old;
+ border-color: $line-removed-dark;
+
+ a {
+ color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
+ }
+ }
+
+ &.new {
+ background-color: $line-number-new;
+ border-color: $line-added-dark;
+
+ a {
+ color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
+ }
+ }
+
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $white-over-bg;
+ border-color: darken($white-over-bg, 5%);
+
+ a {
+ color: darken($white-over-bg, 15%);
+ }
+ }
+
+ &.hll:not(.empty-cell) {
+ background-color: $line-number-select;
+ border-color: $line-select-yellow-dark;
+ }
+ }
+
+ &:not(.diff-expanded) + .diff-expanded,
+ &.diff-expanded + .line_holder:not(.diff-expanded) {
+ > .diff-line-num,
+ > .line_content {
+ border-top: 1px solid $white-expanded-border;
+ }
+ }
+
+ &.diff-expanded {
+ > .diff-line-num,
+ > .line_content {
+ background: $white-expanded-bg;
+ border-color: $white-expanded-bg;
+ }
+ }
+
+ .line_content {
+ &.old {
+ background-color: $line-removed;
+
+ &::before {
+ color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
+ }
+
+ span.idiff {
+ background-color: $line-removed-dark;
+ }
+ }
+
+ &.new {
+ background-color: $line-added;
+
+ &::before {
+ color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
+ }
+
+ span.idiff {
+ background-color: $line-added-dark;
+ }
+ }
+
+ &.match {
+ @include matchLine;
+ }
+
+ &.hll:not(.empty-cell) {
+ background-color: $line-select-yellow;
+ }
+ }
+}
+
+// highlight line via anchor
+pre .hll {
+ background-color: $white-pre-hll-bg !important;
+}
+
+ // Search result highlight
+span.highlight_word {
+ background-color: $white-highlight !important;
+}
+
+ // Links to URLs, emails, or dependencies
+.line a {
+ color: $white-nb;
+}
+
+.hll { background-color: $white-hll-bg; }
+.c { color: $white-c; font-style: italic; }
+.err { color: $white-err; background-color: $white-err-bg; }
+.k { font-weight: $gl-font-weight-bold; }
+.o { font-weight: $gl-font-weight-bold; }
+.cm { color: $white-cm; font-style: italic; }
+.cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
+.c1 { color: $white-c1; font-style: italic; }
+.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
+
+.gd {
+ color: $white-gd;
+ background-color: $white-gd-bg;
+
+ .x {
+ color: $white-gd-x;
+ background-color: $white-gd-x-bg;
+ }
+}
+
+.ge { font-style: italic; }
+.gr { color: $white-gr; }
+.gh { color: $white-gh; }
+
+.gi {
+ color: $white-gi;
+ background-color: $white-gi-bg;
+
+ .x {
+ color: $white-gi-x;
+ background-color: $white-gi-x-bg;
+ }
+}
+
+.go { color: $white-go; }
+.gp { color: $white-gp; }
+.gs { font-weight: $gl-font-weight-bold; }
+.gu { color: $white-gu; font-weight: $gl-font-weight-bold; }
+.gt { color: $white-gt; }
+.kc { font-weight: $gl-font-weight-bold; }
+.kd { font-weight: $gl-font-weight-bold; }
+.kn { font-weight: $gl-font-weight-bold; }
+.kp { font-weight: $gl-font-weight-bold; }
+.kr { font-weight: $gl-font-weight-bold; }
+.kt { color: $white-kt; font-weight: $gl-font-weight-bold; }
+.m { color: $white-m; }
+.s { color: $white-s; }
+.n { color: $white-n; }
+.na { color: $white-na; }
+.nb { color: $white-nb; }
+.nc { color: $white-nc; font-weight: $gl-font-weight-bold; }
+.no { color: $white-no; }
+.ni { color: $white-ni; }
+.ne { color: $white-ne; font-weight: $gl-font-weight-bold; }
+.nf { color: $white-nf; font-weight: $gl-font-weight-bold; }
+.nn { color: $white-nn; }
+.nt { color: $white-nt; }
+.nv { color: $white-nv; }
+.ow { font-weight: $gl-font-weight-bold; }
+.w { color: $white-w; }
+.mf { color: $white-mf; }
+.mh { color: $white-mh; }
+.mi { color: $white-mi; }
+.mo { color: $white-mo; }
+.sb { color: $white-sb; }
+.sc { color: $white-sc; }
+.sd { color: $white-sd; }
+.s2 { color: $white-s2; }
+.se { color: $white-se; }
+.sh { color: $white-sh; }
+.si { color: $white-si; }
+.sx { color: $white-sx; }
+.sr { color: $white-sr; }
+.s1 { color: $white-s1; }
+.ss { color: $white-ss; }
+.bp { color: $white-bp; }
+.vc { color: $white-vc; }
+.vg { color: $white-vg; }
+.vi { color: $white-vi; }
+.il { color: $white-il; }
+.gc { color: $white-gc-color; background-color: $white-gc-bg; }
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 98d460339cd..7a6352e45f1 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -391,7 +391,7 @@
}
&:hover {
- background-color: $row-hover;
+ background-color: $dropdown-item-hover-bg;
}
.icon-retry {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index b487f6278c2..86cdda0359e 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -107,7 +107,6 @@
}
}
-
.commits-compare-switch {
float: left;
margin-right: 9px;
@@ -179,7 +178,7 @@
.commit-detail {
display: flex;
justify-content: space-between;
- align-items: flex-start;
+ align-items: center;
flex-grow: 1;
.merge-request-branches & {
@@ -200,37 +199,63 @@
}
.ci-status-link {
- display: inline-block;
- position: relative;
- top: 2px;
+ display: inline-flex;
}
- .btn-clipboard,
- .btn-transparent {
- padding-left: 0;
- padding-right: 0;
+ > .ci-status-link,
+ > .btn,
+ > .commit-sha-group {
+ margin-left: $gl-padding-8;
}
+}
+.commit-sha-group {
+ display: inline-flex;
+
+ .label,
.btn {
- &:not(:first-child) {
- margin-left: $gl-padding;
- }
+ padding: $gl-vert-padding $gl-btn-padding;
+ border: 1px $border-color solid;
+ font-size: $gl-font-size;
+ line-height: $line-height-base;
+ border-radius: 0;
+ display: flex;
+ align-items: center;
+ }
+
+ .label-monospace {
+ @extend .monospace;
+ user-select: text;
+ color: $gl-text-color;
+ background-color: $gray-light;
}
- .commit-sha {
- font-size: 14px;
- font-weight: $gl-font-weight-bold;
+ .btn svg {
+ top: auto;
+ fill: $gl-text-color-secondary;
}
- .ci-status-icon {
- position: relative;
- top: 2px;
+ .fa-clipboard {
+ color: $gl-text-color-secondary;
+ }
+
+ :first-child {
+ border-bottom-left-radius: $border-radius-default;
+ border-top-left-radius: $border-radius-default;
+ }
+
+ :not(:first-child) {
+ border-left: 0;
+ }
+
+ :last-child {
+ border-bottom-right-radius: $border-radius-default;
+ border-top-right-radius: $border-radius-default;
}
}
.commit,
.generic_commit_status {
-
a,
button {
color: $gl-text-color;
@@ -303,10 +328,8 @@
}
}
-
.gpg-status-box {
padding: 2px 10px;
- margin-right: $gl-padding;
&:empty {
display: none;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 7f037582ca0..11052be40a8 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -160,6 +160,11 @@
}
}
}
+
+ .diff-loading-error-block {
+ padding: $gl-padding * 2 $gl-padding;
+ text-align: center;
+ }
}
.image {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 58700661142..3a300086fa3 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -273,21 +273,6 @@
line-height: 1.2;
}
- table {
- border-collapse: collapse;
- padding: 0;
- margin: 0;
- }
-
- td {
- vertical-align: middle;
-
- + td {
- padding-left: 5px;
- vertical-align: top;
- }
- }
-
.deploy-meta-content {
border-bottom: 1px solid $white-dark;
@@ -323,6 +308,26 @@
}
}
+.prometheus-table {
+ border-collapse: collapse;
+ padding: 0;
+ margin: 0;
+
+ td {
+ vertical-align: middle;
+
+ + td {
+ padding-left: 5px;
+ vertical-align: top;
+ }
+ }
+
+ .legend-metric-title {
+ font-size: 12px;
+ vertical-align: middle;
+ }
+}
+
.prometheus-svg-container {
position: relative;
height: 0;
@@ -330,8 +335,7 @@
padding: 0;
padding-bottom: 100%;
- .text-metric-usage,
- .legend-metric-title {
+ .text-metric-usage {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 12px;
@@ -374,10 +378,6 @@
}
}
- .text-metric-title {
- font-size: 12px;
- }
-
.y-label-text,
.x-label-text {
fill: $gray-darkest;
@@ -414,3 +414,7 @@
}
}
}
+
+.prometheus-table-row-highlight {
+ background-color: $prometheus-table-row-highlight-color;
+}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index b2250a1ce2f..97303d02666 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -154,26 +154,10 @@
a {
width: 100%;
font-size: 18px;
- margin-right: 0;
-
- &:hover {
- border: 1px solid transparent;
- }
}
- &.active {
- border-bottom: 1px solid $border-color;
-
- a {
- border: 0;
- border-bottom: 2px solid $link-underline-blue;
- margin-right: 0;
- color: $black;
-
- &:hover {
- border-bottom: 2px solid $link-underline-blue;
- }
- }
+ &.active > a {
+ cursor: default;
}
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 4692d0fb873..66db4917178 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -762,3 +762,20 @@
max-width: 100%;
}
}
+
+// Hack alert: we've rewritten `btn` class in a way that
+// we've broken it and it is not possible to use with `btn-link`
+// which causes a blank button when it's disabled and hovering
+// The css in here is the boostrap one
+.btn-link-retry {
+ &[disabled] {
+ cursor: not-allowed;
+ box-shadow: none;
+ opacity: .65;
+
+ &:hover {
+ color: $file-mode-changed;
+ text-decoration: none;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index e5afa8fffcb..3af8d80daab 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -194,3 +194,38 @@
.issuable-row {
background-color: $white-light;
}
+
+.milestone-deprecation-message {
+ .popover {
+ padding: 0;
+ }
+
+ .popover-content {
+ padding: 0;
+ }
+}
+
+.milestone-popover-body {
+ padding: $gl-padding-8;
+ background-color: $gray-light;
+}
+
+.milestone-popover-footer {
+ padding: $gl-padding-8 $gl-padding;
+ border-top: 1px solid $white-dark;
+}
+
+.milestone-popover-instructions-list {
+ padding-left: 2em;
+
+ > li {
+ padding-left: 1em;
+ }
+}
+
+@media (max-width: $screen-xs-max) {
+ .milestone-banner-text,
+ .milestone-banner-link {
+ display: inline;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pages.scss b/app/assets/stylesheets/pages/pages.scss
new file mode 100644
index 00000000000..fb42dee66d2
--- /dev/null
+++ b/app/assets/stylesheets/pages/pages.scss
@@ -0,0 +1,60 @@
+.pages-domain-list {
+ &-item {
+ position: relative;
+ display: flex;
+ align-items: center;
+
+ .domain-status {
+ display: inline-flex;
+ left: $gl-padding;
+ position: absolute;
+ }
+
+ .domain-name {
+ flex-grow: 1;
+ }
+
+ }
+
+ &.has-verification-status > li {
+ padding-left: 3 * $gl-padding;
+ }
+
+}
+
+.status-badge {
+
+ display: inline-flex;
+ margin-bottom: $gl-padding-8;
+
+ // Most of the following settings "stolen" from btn-sm
+ // Border radius is overwritten for both
+ .label,
+ .btn {
+ padding: $gl-padding-4 $gl-padding-8;
+ font-size: $gl-font-size;
+ line-height: $gl-btn-line-height;
+ border-radius: 0;
+ display: flex;
+ align-items: center;
+ }
+
+ .btn svg {
+ top: auto;
+ }
+
+ :first-child {
+ border-bottom-left-radius: $border-radius-default;
+ border-top-left-radius: $border-radius-default;
+ }
+
+ :not(:first-child) {
+ border-left: 0;
+ }
+
+ :last-child {
+ border-bottom-right-radius: $border-radius-default;
+ border-top-right-radius: $border-radius-default;
+ }
+
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index ce2f1482456..855ebf7d86d 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -14,6 +14,11 @@
.commit-title {
margin: 0;
+ white-space: normal;
+
+ @media (max-width: $screen-sm-max) {
+ justify-content: flex-end;
+ }
}
.ci-table {
@@ -344,7 +349,6 @@
svg {
vertical-align: middle;
- margin-right: 3px;
}
.stage-column {
@@ -495,17 +499,12 @@
svg {
fill: $gl-text-color-secondary;
position: relative;
- left: 5px;
- top: 2px;
- width: 18px;
- height: 18px;
+ top: -1px;
}
&.play {
svg {
- width: #{$ci-action-icon-size - 8};
- height: #{$ci-action-icon-size - 8};
- left: 8px;
+ left: 2px;
}
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index ac745019319..b199f9876d3 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -210,13 +210,8 @@
}
.created-personal-access-token-container {
- #created-personal-access-token {
- width: 90%;
- display: inline;
- }
-
.btn-clipboard {
- margin-left: 5px;
+ border: 1px solid $border-color;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 9a770d77685..d7d343b088a 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -935,11 +935,6 @@ pre.light-well {
}
}
- .dropdown-menu-toggle {
- width: 100%;
- max-width: 300px;
- }
-
.flash-container {
padding: 0;
}
@@ -1143,3 +1138,11 @@ pre.light-well {
white-space: pre-wrap;
}
}
+
+.project-badge {
+ opacity: 0.9;
+
+ &:hover {
+ opacity: 1;
+ }
+}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 1f6f7138e1f..5f46e69a56d 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -308,14 +308,73 @@
height: 100%;
}
-.multi-file-editor-btn-group {
- padding: $gl-bar-padding $gl-padding;
- border-top: 1px solid $white-dark;
+.preview-container {
+ height: 100%;
+ overflow: auto;
+
+ .file-container {
+ background-color: $gray-darker;
+ display: flex;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+
+ text-align: center;
+
+ .file-content {
+ padding: $gl-padding;
+ max-width: 100%;
+ max-height: 100%;
+
+ img {
+ max-width: 90%;
+ max-height: 90%;
+ }
+
+ .isZoomable {
+ cursor: pointer;
+ cursor: zoom-in;
+
+ &.isZoomed {
+ cursor: pointer;
+ cursor: zoom-out;
+ max-width: none;
+ max-height: none;
+ margin-right: $gl-padding;
+ }
+ }
+ }
+
+ .file-info {
+ font-size: $label-font-size;
+ color: $diff-image-info-color;
+ }
+ }
+
+ .md-previewer {
+ padding: $gl-padding;
+ }
+}
+
+.ide-mode-tabs {
border-bottom: 1px solid $white-dark;
- background: $white-light;
+
+ .nav-links {
+ border-bottom: 0;
+
+ li a {
+ padding: $gl-padding-8 $gl-padding;
+ line-height: $gl-btn-line-height;
+ }
+ }
+}
+
+.ide-btn-group {
+ padding: $gl-padding-4 $gl-vert-padding;
}
.ide-status-bar {
+ border-top: 1px solid $white-dark;
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
@@ -370,6 +429,7 @@
.projects-sidebar {
display: flex;
flex-direction: column;
+ height: 100%;
.context-header {
width: auto;
@@ -379,8 +439,8 @@
.multi-file-commit-panel-inner {
display: flex;
- flex: 1;
flex-direction: column;
+ height: 100%;
}
.multi-file-commit-panel-inner-scroll {
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index a6ca8ed5016..c410049bc0b 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -284,3 +284,23 @@
.deprecated-service {
cursor: default;
}
+
+.personal-access-tokens-never-expires-label {
+ color: $note-disabled-comment-color;
+}
+
+.created-deploy-token-container {
+ .deploy-token-field {
+ width: 90%;
+ display: inline;
+ }
+
+ .btn-clipboard {
+ margin-left: 5px;
+ }
+
+ .deploy-token-help-block {
+ display: block;
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
new file mode 100644
index 00000000000..0d6b0735f70
--- /dev/null
+++ b/app/assets/stylesheets/snippets.scss
@@ -0,0 +1,156 @@
+@import "framework/variables";
+
+.gitlab-embed-snippets {
+ @import "highlight/embedded";
+ @import "framework/images";
+
+ $border-style: 1px solid $border-color;
+
+ font-family: $regular_font;
+ font-size: $gl-font-size;
+ line-height: $code_line_height;
+ color: $gl-text-color;
+ margin: 20px;
+ font-weight: 200;
+
+ .gl-snippet-icon {
+ display: inline-block;
+ background: url(asset_path('ext_snippet_icons/ext_snippet_icons.png')) no-repeat;
+ overflow: hidden;
+ text-align: left;
+ width: 16px;
+ height: 16px;
+ background-size: cover;
+
+ &.gl-snippet-icon-doc_code { background-position: 0 0; }
+ &.gl-snippet-icon-doc_text { background-position: 0 -16px; }
+ &.gl-snippet-icon-download { background-position: 0 -32px; }
+ }
+
+ .blob-viewer {
+ background-color: $white-light;
+ text-align: left;
+ }
+
+ .file-content.code {
+ border: $border-style;
+ border-radius: 0 0 4px 4px;
+ display: flex;
+ box-shadow: none;
+ margin: 0;
+ padding: 0;
+ table-layout: fixed;
+
+ .blob-content {
+ overflow-x: auto;
+
+ pre {
+ padding: 10px;
+ border: 0;
+ border-radius: 0;
+ font-family: $monospace_font;
+ font-size: $code_font_size;
+ line-height: $code_line_height;
+ margin: 0;
+ overflow: auto;
+ overflow-y: hidden;
+ white-space: pre;
+ word-wrap: normal;
+ border-left: $border-style;
+ }
+ }
+
+ .line-numbers {
+ padding: 10px;
+ text-align: right;
+ float: left;
+
+ .diff-line-num {
+ font-family: $monospace_font;
+ display: block;
+ font-size: $code_font_size;
+ min-height: $code_line_height;
+ white-space: nowrap;
+ color: $black-transparent;
+ min-width: 30px;
+ }
+
+ .diff-line-num:hover {
+ color: $almost-black;
+ cursor: pointer;
+ }
+ }
+ }
+
+ .file-title-flex-parent {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: $gray-light;
+ border: $border-style;
+ border-bottom: 0;
+ padding: $gl-padding-top $gl-padding;
+ margin: 0;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+
+ .file-header-content {
+ .file-title-name {
+ font-weight: $gl-font-weight-bold;
+ }
+
+ .gitlab-embedded-snippets-title {
+ text-decoration: none;
+ color: $gl-text-color;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .gitlab-logo {
+ display: inline-block;
+ padding-left: 5px;
+ text-decoration: none;
+ color: $gl-text-color-secondary;
+
+ .logo-text {
+ background: image_url('ext_snippet_icons/logo.png') no-repeat left center;
+ background-size: 18px;
+ font-weight: $gl-font-weight-normal;
+ padding-left: 24px;
+ }
+ }
+ }
+
+ img,
+ .gl-snippet-icon {
+ display: inline-block;
+ vertical-align: middle;
+ }
+ }
+
+ .btn-group {
+ a.btn {
+ background-color: $white-light;
+ text-decoration: none;
+ padding: 7px 9px;
+ border: $border-style;
+ border-right: 0;
+
+ &:hover {
+ background-color: $white-normal;
+ border-color: $border-white-normal;
+ text-decoration: none;
+ }
+
+ &:first-child {
+ border-radius: 3px 0 0 3px;
+ }
+
+ &:last-child {
+ border-radius: 0 3px 3px 0;
+ border-right: $border-style;
+ }
+ }
+ }
+}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 4dfb397e82c..8958eab0423 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -56,21 +56,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def application_setting_params
- import_sources = params[:application_setting][:import_sources]
- if import_sources.nil?
- params[:application_setting][:import_sources] = []
- else
- import_sources.map! do |source|
- source.to_str
- end
- end
+ params[:application_setting] ||= {}
- enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources)
+ if params[:application_setting].key?(:enabled_oauth_sign_in_sources)
+ enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources)
+ enabled_oauth_sign_in_sources&.delete("")
- params[:application_setting][:disabled_oauth_sign_in_sources] =
- AuthHelper.button_based_providers.map(&:to_s) -
- Array(enabled_oauth_sign_in_sources)
+ params[:application_setting][:disabled_oauth_sign_in_sources] =
+ AuthHelper.button_based_providers.map(&:to_s) -
+ Array(enabled_oauth_sign_in_sources)
+ end
+ params[:application_setting][:import_sources]&.delete("")
params[:application_setting][:restricted_visibility_levels]&.delete("")
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 19dbee84c11..7d7ff217e5d 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -96,7 +96,8 @@ module Boards
resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true,
- sidebar_endpoints: true,
+ issue_endpoints: true,
+ include_full_project_path: board.group_board?,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 2753f83c3cf..2fdf346ef44 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -10,7 +10,7 @@ module AuthenticatesWithTwoFactor
# This action comes from DeviseController, but because we call `sign_in`
# manually, not skipping this action would cause a "You are already signed
# in." error message to be shown upon successful login.
- skip_before_action :require_no_authentication, only: [:create]
+ skip_before_action :require_no_authentication, only: [:create], raise: false
end
# Store the user's ID in the session for later retrieval and render the
diff --git a/app/controllers/concerns/checks_collaboration.rb b/app/controllers/concerns/checks_collaboration.rb
new file mode 100644
index 00000000000..81367663a06
--- /dev/null
+++ b/app/controllers/concerns/checks_collaboration.rb
@@ -0,0 +1,21 @@
+module ChecksCollaboration
+ def can_collaborate_with_project?(project, ref: nil)
+ return true if can?(current_user, :push_code, project)
+
+ can_create_merge_request =
+ can?(current_user, :create_merge_request_in, project) &&
+ current_user.already_forked?(project)
+
+ can_create_merge_request ||
+ user_access(project).can_push_to_branch?(ref)
+ end
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ # enabling this so we can easily cache the user access value as it might be
+ # used across multiple calls in the view
+ def user_access(project)
+ @user_access ||= {}
+ @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 839cac3687c..ad4e936a3d4 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -41,7 +41,7 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note)
- Notes::RenderService.new(current_user).execute([@note], @project)
+ Notes::RenderService.new(current_user).execute([@note])
end
respond_to do |format|
@@ -56,7 +56,7 @@ module NotesActions
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
- Notes::RenderService.new(current_user).execute([@note], @project)
+ Notes::RenderService.new(current_user).execute([@note])
end
respond_to do |format|
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index e7ef297879f..36e3d76ecfe 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -4,7 +4,7 @@ module RendersNotes
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes)
- Notes::RenderService.new(current_user).execute(notes, @project)
+ Notes::RenderService.new(current_user).execute(notes)
notes
end
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index 9095cc7f783..120614739aa 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -17,6 +17,10 @@ module SnippetsActions
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
+ def js_request?
+ request.format.js?
+ end
+
private
def convert_line_endings(content)
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 280ed93faf8..68d328fa797 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -2,9 +2,17 @@ class DashboardController < Dashboard::ApplicationController
include IssuesAction
include MergeRequestsAction
+ FILTER_PARAMS = [
+ :author_id,
+ :assignee_id,
+ :milestone_title,
+ :label_name
+ ].freeze
+
before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests]
before_action :set_show_full_reference, only: [:issues, :merge_requests]
+ before_action :check_filters_presence!, only: [:issues, :merge_requests]
respond_to :html
@@ -39,4 +47,15 @@ class DashboardController < Dashboard::ApplicationController
def set_show_full_reference
@show_full_reference = true
end
+
+ def check_filters_presence!
+ @no_filters_set = FILTER_PARAMS.none? { |k| params.key?(k) }
+
+ return unless @no_filters_set
+
+ respond_to do |format|
+ format.html
+ format.atom { head :bad_request }
+ end
+ end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index acf6aaf57f4..5903689dc62 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -12,7 +12,7 @@ class Groups::MilestonesController < Groups::ApplicationController
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
- render json: milestones.map { |m| m.for_display.slice(:title, :name) }
+ render json: milestones.map { |m| m.for_display.slice(:id, :title, :name) }
end
end
end
diff --git a/app/controllers/groups/settings/badges_controller.rb b/app/controllers/groups/settings/badges_controller.rb
new file mode 100644
index 00000000000..edb334a3d88
--- /dev/null
+++ b/app/controllers/groups/settings/badges_controller.rb
@@ -0,0 +1,13 @@
+module Groups
+ module Settings
+ class BadgesController < Groups::ApplicationController
+ include GrapeRouteHelpers::NamedRouteMatcher
+
+ before_action :authorize_admin_group!
+
+ def index
+ @badge_api_endpoint = api_v4_groups_badges_path(id: @group.id)
+ end
+ end
+ end
+end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 283c3e5f1e0..5ac4b8710e2 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -173,7 +173,9 @@ class GroupsController < Groups::ApplicationController
.new(@projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
- Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
+ Events::RenderService
+ .new(current_user)
+ .execute(@events, atom_request: request.format.atom?)
end
def user_actions
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 7d6fe6a0232..67057b5b126 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -25,8 +25,7 @@ class JwtController < ApplicationController
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
- if @authentication_result.failed? ||
- (@authentication_result.actor.present? && !@authentication_result.actor.is_a?(User))
+ if @authentication_result.failed?
render_unauthorized
end
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 3d27ae18b17..ac71f72e624 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -53,13 +53,19 @@ class ProfilesController < Profiles::ApplicationController
def update_username
result = Users::UpdateService.new(current_user, user: @user, username: username_param).execute
- options = if result[:status] == :success
- { notice: "Username successfully changed" }
- else
- { alert: "Username change failed - #{result[:message]}" }
- end
+ respond_to do |format|
+ if result[:status] == :success
+ message = s_("Profiles|Username successfully changed")
- redirect_back_or_default(default: { action: 'show' }, options: options)
+ format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
+ format.json { render json: { message: message }, status: :ok }
+ else
+ message = s_("Profiles|Username change failed - %{message}") % { message: result[:message] }
+
+ format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: message }) }
+ format.json { render json: { message: message }, status: :unprocessable_entity }
+ end
+ end
end
private
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 6d9b42a2c04..032bb2267e7 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -1,5 +1,6 @@
class Projects::ApplicationController < ApplicationController
include RoutableActions
+ include ChecksCollaboration
skip_before_action :authenticate_user!
before_action :project
@@ -31,14 +32,6 @@ class Projects::ApplicationController < ApplicationController
@repository ||= project.repository
end
- def can_collaborate_with_project?(project = nil, ref: nil)
- project ||= @project
-
- can?(current_user, :push_code, project) ||
- (current_user && current_user.already_forked?(project)) ||
- user_access(project).can_push_to_branch?(ref)
- end
-
def authorize_action!(action)
unless can?(current_user, action, project)
return access_denied!
@@ -91,9 +84,4 @@ class Projects::ApplicationController < ApplicationController
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
-
- def user_access(project)
- @user_access ||= {}
- @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
- end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index effb484ef0f..b7f548e0e63 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -34,6 +34,7 @@ class Projects::CommitController < Projects::ApplicationController
def pipelines
@pipelines = @commit.pipelines.order(id: :desc)
+ @pipelines = @pipelines.where(ref: params[:ref]) if params[:ref]
respond_to do |format|
format.html
diff --git a/app/controllers/projects/deploy_tokens_controller.rb b/app/controllers/projects/deploy_tokens_controller.rb
new file mode 100644
index 00000000000..2f91b8f36de
--- /dev/null
+++ b/app/controllers/projects/deploy_tokens_controller.rb
@@ -0,0 +1,10 @@
+class Projects::DeployTokensController < Projects::ApplicationController
+ before_action :authorize_admin_project!
+
+ def revoke
+ @token = @project.deploy_tokens.find(params[:id])
+ @token.revoke!
+
+ redirect_to project_settings_repository_path(project)
+ end
+end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 7bc16214010..8e86af43fee 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -4,8 +4,8 @@ class Projects::DiscussionsController < Projects::ApplicationController
before_action :check_merge_requests_available!
before_action :merge_request
- before_action :discussion
- before_action :authorize_resolve_discussion!
+ before_action :discussion, only: [:resolve, :unresolve]
+ before_action :authorize_resolve_discussion!, only: [:resolve, :unresolve]
def resolve
Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion)
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index dd5e66f60e3..07249fe3182 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -7,6 +7,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
attr_reader :authentication_result, :redirected_path
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
+ delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result
alias_method :user, :actor
alias_method :authenticated_user, :actor
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 45910a9be44..1dcf837f78e 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -64,7 +64,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
@access ||= access_klass.new(access_actor, project,
'http', authentication_abilities: authentication_abilities,
namespace_path: params[:namespace_id], project_path: project_path,
- redirected_path: redirected_path)
+ redirected_path: redirected_path, auth_result_type: auth_result_type)
end
def access_actor
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b14939c4216..767e492f566 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -20,7 +20,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_update_issuable!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue
- before_action :authorize_create_merge_request!, only: [:create_merge_request]
+ before_action :authorize_create_merge_request_from!, only: [:create_merge_request]
respond_to :html
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 85e972d9731..dd12d30a085 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -2,7 +2,6 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
before_action :build, except: [:index, :cancel_all]
-
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
@@ -45,8 +44,11 @@ class Projects::JobsController < Projects::ApplicationController
end
def show
- @builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC')
- @builds = @builds.where("id not in (?)", @build.id)
+ @builds = @project.pipelines
+ .find_by_sha(@build.sha)
+ .builds
+ .order('id DESC')
+ .present(current_user: current_user)
@pipeline = @build.pipeline
respond_to do |format|
@@ -128,7 +130,7 @@ class Projects::JobsController < Projects::ApplicationController
if stream.file?
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
- render_404
+ send_data stream.raw, type: 'text/plain; charset=utf-8', disposition: 'inline', filename: 'job.log'
end
end
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 516198b1b8a..91016f6494e 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -150,7 +150,8 @@ class Projects::LabelsController < Projects::ApplicationController
end
def find_labels
- @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+ @available_labels ||=
+ LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups]).execute
end
def authorize_admin_labels!
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index c77f10ef1dd..ee4ed674110 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -41,7 +41,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
def existing_oids
@existing_oids ||= begin
- storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
+ project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
end
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 2515e4b9a17..ebde0df1f7b 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -31,7 +31,9 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
render plain: 'Unprocessable entity', status: 422
end
rescue ActiveRecord::RecordInvalid
- render_400
+ render_lfs_forbidden
+ rescue UploadedFile::InvalidPathError
+ render_lfs_forbidden
rescue ObjectStorage::RemoteStoreError
render_lfs_forbidden
end
@@ -66,10 +68,11 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
end
def create_file!(oid, size)
- LfsObject.new(oid: oid, size: size).tap do |object|
- object.file.store_workhorse_file!(params, :file)
- object.save!
- end
+ uploaded_file = UploadedFile.from_params(
+ params, :file, LfsObjectUploader.workhorse_local_upload_path)
+ return unless uploaded_file
+
+ LfsObject.create!(oid: oid, size: size, file: uploaded_file)
end
def link_to_project!(object)
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index a90030a8312..4a377fefc62 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -5,7 +5,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
skip_before_action :merge_request
before_action :whitelist_query_limiting, only: [:create]
- before_action :authorize_create_merge_request!
+ before_action :authorize_create_merge_request_from!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create]
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index dd41b9648e8..86c50d88a2a 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController
private
def render_json_with_notes_serializer
- Notes::RenderService.new(current_user).execute([note], project)
+ Notes::RenderService.new(current_user).execute([note])
render json: note_serializer.represent(note)
end
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 557671ab186..73c613b26f3 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -4,41 +4,4 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def show
redirect_to project_settings_ci_cd_path(@project, params: params)
end
-
- def update
- Projects::UpdateService.new(project, current_user, update_params).tap do |service|
- if service.execute
- flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
-
- run_autodevops_pipeline(service)
-
- redirect_to project_settings_ci_cd_path(@project)
- else
- render 'show'
- end
- end
- end
-
- private
-
- def run_autodevops_pipeline(service)
- return unless service.run_auto_devops_pipeline?
-
- if @project.empty_repo?
- flash[:warning] = "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch."
- return
- end
-
- CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
- flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
- end
-
- def update_params
- params.require(:project).permit(
- :runners_token, :builds_enabled, :build_allow_git_fetch,
- :build_timeout_in_minutes, :build_coverage_regex, :public_builds,
- :auto_cancel_pending_pipelines, :ci_config_path,
- auto_devops_attributes: [:id, :domain, :enabled]
- )
- end
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 2376f469213..48a09e1ddb8 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -25,7 +25,7 @@ class Projects::RefsController < Projects::ApplicationController
when "graphs_commits"
commits_project_graph_path(@project, @id)
when "badges"
- project_pipelines_settings_path(@project, ref: @id)
+ project_settings_ci_cd_path(@project, ref: @id)
else
project_commits_path(@project, @id)
end
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index d5af0341d18..937b0e39cbd 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -1,6 +1,9 @@
class Projects::RepositoriesController < Projects::ApplicationController
+ include ExtractsPath
+
# Authorize
before_action :require_non_empty_project, except: :create
+ before_action :assign_archive_vars, only: :archive
before_action :authorize_download_code!
before_action :authorize_admin_project!, only: :create
@@ -11,9 +14,26 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
- send_git_archive @repository, ref: params[:ref], format: params[:format]
+ append_sha = params[:append_sha]
+
+ if @ref
+ shortname = "#{@project.path}-#{@ref.tr('/', '-')}"
+ append_sha = false if @filename == shortname
+ end
+
+ send_git_archive @repository, ref: @ref, format: params[:format], append_sha: append_sha
rescue => ex
logger.error("#{self.class.name}: #{ex}")
return git_not_found!
end
+
+ def assign_archive_vars
+ @id = params[:id]
+
+ return unless @id
+
+ @ref, @filename = extract_ref(@id)
+ rescue InvalidPathError
+ render_404
+ end
end
diff --git a/app/controllers/projects/settings/badges_controller.rb b/app/controllers/projects/settings/badges_controller.rb
new file mode 100644
index 00000000000..f7b70dd4b7b
--- /dev/null
+++ b/app/controllers/projects/settings/badges_controller.rb
@@ -0,0 +1,13 @@
+module Projects
+ module Settings
+ class BadgesController < Projects::ApplicationController
+ include GrapeRouteHelpers::NamedRouteMatcher
+
+ before_action :authorize_admin_project!
+
+ def index
+ @badge_api_endpoint = 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 96125b549b7..d80ef8113aa 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -2,13 +2,24 @@ module Projects
module Settings
class CiCdController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
+ before_action :define_variables
def show
- define_runners_variables
- define_secret_variables
- define_triggers_variables
- define_badges_variables
- define_auto_devops_variables
+ end
+
+ def update
+ Projects::UpdateService.new(project, current_user, update_params).tap do |service|
+ result = service.execute
+ if result[:status] == :success
+ flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
+
+ run_autodevops_pipeline(service)
+
+ redirect_to project_settings_ci_cd_path(@project)
+ else
+ render 'show'
+ end
+ end
end
def reset_cache
@@ -25,6 +36,35 @@ module Projects
private
+ def update_params
+ params.require(:project).permit(
+ :runners_token, :builds_enabled, :build_allow_git_fetch,
+ :build_timeout_human_readable, :build_coverage_regex, :public_builds,
+ :auto_cancel_pending_pipelines, :ci_config_path,
+ auto_devops_attributes: [:id, :domain, :enabled]
+ )
+ end
+
+ def run_autodevops_pipeline(service)
+ return unless service.run_auto_devops_pipeline?
+
+ if @project.empty_repo?
+ flash[:warning] = "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch."
+ return
+ end
+
+ CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
+ flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
+ end
+
+ def define_variables
+ define_runners_variables
+ define_secret_variables
+ define_triggers_variables
+ define_badges_variables
+ define_auto_devops_variables
+ end
+
def define_runners_variables
@project_runners = @project.runners.ordered
@assignable_runners = current_user.ci_authorized_runners
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index dd9e4a2af3e..f17056f13e0 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -4,13 +4,31 @@ module Projects
before_action :authorize_admin_project!
def show
- @deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
+ render_show
+ end
- define_protected_refs
+ def create_deploy_token
+ @new_deploy_token = DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
+
+ if @new_deploy_token.persisted?
+ flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
+ end
+
+ render_show
end
private
+ def render_show
+ @deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
+ @deploy_tokens = @project.deploy_tokens.active
+
+ define_deploy_token
+ define_protected_refs
+
+ render 'show'
+ end
+
def define_protected_refs
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
@protected_tags = @project.protected_tags.order(:name).page(params[:page])
@@ -51,6 +69,14 @@ module Projects
gon.push(protectable_branches_for_dropdown)
gon.push(access_levels_options)
end
+
+ def define_deploy_token
+ @new_deploy_token ||= DeployToken.new
+ end
+
+ def deploy_token_params
+ params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry)
+ end
end
end
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 7c19aa7bb23..208a1d19862 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -5,6 +5,8 @@ class Projects::SnippetsController < Projects::ApplicationController
include SnippetsActions
include RendersBlob
+ skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
+
before_action :check_snippets_available!
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
@@ -71,6 +73,7 @@ class Projects::SnippetsController < Projects::ApplicationController
format.json do
render_blob_json(blob)
end
+ format.js { render 'shared/snippets/show'}
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ee197c75764..37f14230196 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -324,7 +324,7 @@ class ProjectsController < Projects::ApplicationController
:avatar,
:build_allow_git_fetch,
:build_coverage_regex,
- :build_timeout_in_minutes,
+ :build_timeout_human_readable,
:resolve_outdated_diff_discussions,
:container_registry_enabled,
:default_branch,
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index be2d3f638ff..3d51520ddf4 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -6,6 +6,8 @@ class SnippetsController < ApplicationController
include RendersBlob
include PreviewMarkdown
+ skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
+
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
@@ -77,6 +79,8 @@ class SnippetsController < ApplicationController
format.json do
render_blob_json(blob)
end
+
+ format.js { render 'shared/snippets/show' }
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 61c72aa22a8..7ed9b1fc6d0 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -159,7 +159,10 @@ class IssuableFinder
finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute
else
- ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
+ opts = { current_user: current_user }
+ opts[:project_ids_relation] = item_project_ids(items) if items
+
+ ProjectsFinder.new(opts).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
@@ -316,9 +319,9 @@ class IssuableFinder
def by_project(items)
items =
if project?
- items.of_projects(projects(items)).references_project
- elsif projects(items)
- items.merge(projects(items).reorder(nil)).join_project
+ items.of_projects(projects).references_project
+ elsif projects
+ items.merge(projects.reorder(nil)).join_project
else
items.none
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 780c0fdb03e..afd1f824b32 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -28,9 +28,10 @@ class LabelsFinder < UnionFinder
if project
if project.group.present?
labels_table = Label.arel_table
+ group_ids = group_ids_for(project.group)
label_ids << Label.where(
- labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or(
+ labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].in(group_ids)).or(
labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
)
)
@@ -38,11 +39,14 @@ class LabelsFinder < UnionFinder
label_ids << project.labels
end
end
- elsif only_group_labels?
- label_ids << Label.where(group_id: group_ids)
else
+ if group?
+ group = Group.find(params[:group_id])
+ label_ids << Label.where(group_id: group_ids_for(group))
+ end
+
label_ids << Label.where(group_id: projects.group_ids)
- label_ids << Label.where(project_id: projects.select(:id))
+ label_ids << Label.where(project_id: projects.select(:id)) unless only_group_labels?
end
label_ids
@@ -59,22 +63,33 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
- def group_ids
+ # Gets redacted array of group ids
+ # which can include the ancestors and descendants of the requested group.
+ def group_ids_for(group)
strong_memoize(:group_ids) do
- groups_user_can_read_labels(groups_to_include).map(&:id)
+ groups = groups_to_include(group)
+
+ groups_user_can_read_labels(groups).map(&:id)
end
end
- def groups_to_include
- group = Group.find(params[:group_id])
+ def groups_to_include(group)
groups = [group]
- groups += group.ancestors if params[:include_ancestor_groups].present?
- groups += group.descendants if params[:include_descendant_groups].present?
+ groups += group.ancestors if include_ancestor_groups?
+ groups += group.descendants if include_descendant_groups?
groups
end
+ def include_ancestor_groups?
+ params[:include_ancestor_groups]
+ end
+
+ def include_descendant_groups?
+ params[:include_descendant_groups]
+ end
+
def group?
params[:group_id].present?
end
diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb
index f358938344e..188ec447a94 100644
--- a/app/finders/merge_request_target_project_finder.rb
+++ b/app/finders/merge_request_target_project_finder.rb
@@ -12,6 +12,7 @@ class MergeRequestTargetProjectFinder
if @source_project.fork_network
@source_project.fork_network.projects
.public_or_visible_to_user(current_user)
+ .non_archived
.with_feature_available_for_user(:merge_requests, current_user)
else
Project.where(id: source_project)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 86ec500ceb3..228c8d2e8f9 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -228,9 +228,7 @@ module ApplicationHelper
scope: params[:scope],
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
- assignee_username: params[:assignee_username],
author_id: params[:author_id],
- author_username: params[:author_username],
search: params[:search],
label_name: params[:label_name]
}
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index b3b080e6dcf..3fbb32c5229 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -74,10 +74,12 @@ module ApplicationSettingsHelper
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
check_box_tag(checkbox_name, source, !disabled,
- autocomplete: 'off') + Gitlab::Auth::OAuth::Provider.label_for(source)
+ autocomplete: 'off',
+ id: name.tr(' ', '_')) + name
end
end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 2b440e4d584..866b8773db6 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -59,7 +59,7 @@ module BlobHelper
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
- elsif can?(current_user, :fork_project, project)
+ elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action)
end
end
@@ -280,7 +280,7 @@ module BlobHelper
options << link_to("submit an issue", new_project_issue_path(project))
end
- merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project))
+ merge_project = merge_request_source_project_for_project(@project)
if merge_project
options << link_to("create a merge request", project_new_merge_request_path(project))
end
@@ -334,7 +334,7 @@ module BlobHelper
# Web IDE (Beta) requires the user to have this feature enabled
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
edit_link_tag(text, edit_path, common_classes)
- elsif current_user && can?(current_user, :fork_project, project)
+ elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
end
end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 275e892b2e6..af878bcf9a0 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -53,10 +53,12 @@ module BoardsHelper
end
def board_list_data
+ include_descendant_groups = @group&.present?
+
{
toggle: "dropdown",
- list_labels_path: labels_filter_path(true),
- labels: labels_filter_path(true),
+ list_labels_path: labels_filter_path(true, include_ancestor_groups: true),
+ labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups),
labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path,
project_path: @project&.path,
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 636316da80a..f0afcac5986 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -94,7 +94,7 @@ module CiStatusHelper
def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left')
project = pipeline_status.project
- path = pipelines_project_commit_path(project, pipeline_status.sha)
+ path = pipelines_project_commit_path(project, pipeline_status.sha, ref: pipeline_status.ref)
render_status_with_link(
'commit',
@@ -105,7 +105,7 @@ module CiStatusHelper
def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left')
project = commit.project
- path = pipelines_project_commit_path(project, commit)
+ path = pipelines_project_commit_path(project, commit, ref: ref)
render_status_with_link(
'commit',
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 0333c29e2fd..98894b86551 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -93,25 +93,18 @@ module CommitsHelper
return unless current_controller?(:commits)
if @path.blank?
- return link_to(
- _("Browse Files"),
- project_tree_path(project, commit),
- class: "btn btn-default"
- )
+ url = project_tree_path(project, commit)
+ tooltip = _("Browse Files")
elsif @repo.blob_at(commit.id, @path)
- return link_to(
- _("Browse File"),
- project_blob_path(project,
- tree_join(commit.id, @path)),
- class: "btn btn-default"
- )
+ url = project_blob_path(project, tree_join(commit.id, @path))
+ tooltip = _("Browse File")
elsif @path.present?
- return link_to(
- _("Browse Directory"),
- project_tree_path(project,
- tree_join(commit.id, @path)),
- class: "btn btn-default"
- )
+ url = project_tree_path(project, tree_join(commit.id, @path))
+ tooltip = _("Browse Directory")
+ end
+
+ link_to url, class: "btn btn-default has-tooltip", title: tooltip, data: { container: "body" } do
+ sprite_icon('folder-open')
end
end
@@ -170,7 +163,7 @@ module CommitsHelper
tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
btn_class = "btn btn-#{btn_class}" unless btn_class.nil?
- if can_collaborate_with_project?
+ if can_collaborate_with_project?(@project)
link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index 8bf96c0905f..2df5b5d1695 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
- can?(current_user, :create_merge_request, project) &&
+ can?(current_user, :create_merge_request_from, project) &&
project.repository.branch_exists?(from) &&
project.repository.branch_exists?(to)
end
diff --git a/app/helpers/deploy_tokens_helper.rb b/app/helpers/deploy_tokens_helper.rb
new file mode 100644
index 00000000000..bd921322476
--- /dev/null
+++ b/app/helpers/deploy_tokens_helper.rb
@@ -0,0 +1,12 @@
+module DeployTokensHelper
+ def expand_deploy_tokens_section?(deploy_token)
+ deploy_token.persisted? ||
+ deploy_token.errors.present? ||
+ Rails.env.test?
+ end
+
+ def container_registry_enabled?(project)
+ Gitlab.config.registry.enabled &&
+ can?(current_user, :read_container_image, project)
+ end
+end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 16eceb3f48f..95fea2f18d1 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,6 +1,6 @@
module GroupsHelper
def group_nav_link_paths
- %w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
+ %w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
end
def group_sidebar_links
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index c5522ff7a69..2f304b040c7 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -43,6 +43,10 @@ module IconsHelper
content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
end
+ def external_snippet_icon(name)
+ content_tag(:span, "", class: "gl-snippet-icon gl-snippet-icon-#{name}")
+ end
+
def audit_icon(names, options = {})
case names
when "standard"
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 6d6b840f485..06c3e569c84 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -159,16 +159,18 @@ module IssuablesHelper
label_names.join(', ')
end
- def issuables_state_counter_text(issuable_type, state)
+ def issuables_state_counter_text(issuable_type, state, display_count)
titles = {
opened: "Open"
}
state_title = titles[state] || state.to_s.humanize
- count = issuables_count_for_state(issuable_type, state)
-
html = content_tag(:span, state_title)
- html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
+
+ if display_count
+ count = issuables_count_for_state(issuable_type, state)
+ html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
+ end
html.html_safe
end
@@ -191,24 +193,10 @@ module IssuablesHelper
end
end
- def issuable_filter_params
- [
- :search,
- :author_id,
- :assignee_id,
- :milestone_title,
- :label_name
- ]
- end
-
def issuable_reference(issuable)
@show_full_reference ? issuable.to_reference(full: true) : issuable.to_reference(@group || @project)
end
- def issuable_filter_present?
- issuable_filter_params.any? { |k| params.key?(k) }
- end
-
def issuable_initial_data(issuable)
data = {
endpoint: issuable_path(issuable),
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 0f25d401406..96dc7ae1185 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -82,8 +82,8 @@ module IssuesHelper
names.to_sentence
end
- def award_state_class(awards, current_user)
- if !current_user
+ def award_state_class(awardable, awards, current_user)
+ if !can?(current_user, :award_emoji, awardable)
"disabled"
elsif current_user && awards.find { |a| a.user_id == current_user.id }
"active"
@@ -126,6 +126,17 @@ module IssuesHelper
link_to link_text, path
end
+ def show_new_issue_link?(project)
+ return false unless project
+ return false if project.archived?
+
+ # We want to show the link to users that are not signed in, that way they
+ # get directed to the sign-in/sign-up flow and afterwards to the new issue page.
+ return true unless current_user
+
+ can?(current_user, :create_issue, project)
+ end
+
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
module_function :url_for_internal_issue
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 87ff607dc3f..c4a6a1e4bb3 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -129,13 +129,17 @@ module LabelsHelper
end
end
- def labels_filter_path(only_group_labels = false)
+ def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false)
project = @target_project || @project
+ options = {}
+ options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups
+ options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups
+
if project
- project_labels_path(project, :json)
+ project_labels_path(project, :json, options)
elsif @group
- options = { only_group_labels: only_group_labels } if only_group_labels
+ options[:only_group_labels] = only_group_labels if only_group_labels
group_labels_path(@group, :json, options)
else
dashboard_labels_path(:json)
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 2fe1927a189..39e7a7fd396 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -256,7 +256,7 @@ module MarkupHelper
return '' unless html.present?
context.merge!(
- current_user: (current_user if defined?(current_user)),
+ current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
commit: @commit,
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index fb4fe1c40b7..c19c5b9cc82 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -138,6 +138,18 @@ module MergeRequestsHelper
end
end
+ def merge_request_source_project_for_project(project = @project)
+ unless can?(current_user, :create_merge_request_in, project)
+ return nil
+ end
+
+ if can?(current_user, :create_merge_request_from, project)
+ project
+ else
+ current_user.fork_of(project)
+ end
+ end
+
def merge_params_ee(merge_request)
{}
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 27ed48fdbc7..7f67574a428 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -6,10 +6,6 @@ module NotesHelper
end
end
- def note_editable?(note)
- Ability.can_edit_note?(current_user, note)
- end
-
def note_supports_quick_actions?(note)
Notes::QuickActionsService.supported?(note)
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 15f48e43a28..a64b2acdd77 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -157,40 +157,6 @@ module ProjectsHelper
current_user&.recent_push(@project)
end
- def project_feature_access_select(field)
- # Don't show option "everyone with access" if project is private
- options = project_feature_options
-
- level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend
-
- if @project.private?
- disabled_option = ProjectFeature::ENABLED
- highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
- end
-
- options = options_for_select(
- options.invert,
- selected: highest_available_option || level,
- disabled: disabled_option
- )
-
- content_tag :div, class: "select-wrapper" do
- concat(
- content_tag(
- :select,
- options,
- name: "project[project_feature_attributes][#{field}]",
- id: "project_project_feature_attributes_#{field}",
- class: "pull-right form-control select-control #{repo_children_classes(field)} ",
- data: { field: field }
- )
- )
- concat(
- icon('chevron-down')
- )
- end.html_safe
- end
-
def link_to_autodeploy_doc
link_to _('About auto deploy'), help_page_path('ci/autodeploy/index'), target: '_blank'
end
@@ -274,16 +240,6 @@ module ProjectsHelper
private
- def repo_children_classes(field)
- needs_repo_check = [:merge_requests_access_level, :builds_access_level]
- return unless needs_repo_check.include?(field)
-
- classes = "project-repo-select js-repo-select"
- classes << " disabled" unless @project.feature_available?(:repository, current_user)
-
- classes
- end
-
def get_project_nav_tabs(project, current_user)
nav_tabs = [:home]
@@ -447,14 +403,6 @@ module ProjectsHelper
filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end
- def project_feature_options
- {
- ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'),
- ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'),
- ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access')
- }
- end
-
def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end
@@ -463,20 +411,6 @@ module ProjectsHelper
IssuesFinder.new(current_user, project_id: project.id).execute
end
- def visibility_select_options(project, selected_level)
- level_options = Gitlab::VisibilityLevel.values.each_with_object([]) do |level, level_options|
- next if restricted_levels.include?(level)
-
- level_options << [
- visibility_level_label(level),
- { data: { description: visibility_level_description(level, project) } },
- level
- ]
- end
-
- options_for_select(level_options, selected_level)
- end
-
def restricted_levels
return [] if current_user.admin?
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index f435c80c656..f872990122e 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -1,4 +1,29 @@
module ServicesHelper
+ def service_event_description(event)
+ case event
+ when "push", "push_events"
+ "Event will be triggered by a push to the repository"
+ when "tag_push", "tag_push_events"
+ "Event will be triggered when a new tag is pushed to the repository"
+ when "note", "note_events"
+ "Event will be triggered when someone adds a comment"
+ when "confidential_note", "confidential_note_events"
+ "Event will be triggered when someone adds a comment on a confidential issue"
+ when "issue", "issue_events"
+ "Event will be triggered when an issue is created/updated/closed"
+ when "confidential_issue", "confidential_issues_events"
+ "Event will be triggered when a confidential issue is created/updated/closed"
+ when "merge_request", "merge_request_events"
+ "Event will be triggered when a merge request is created/updated/merged"
+ when "pipeline", "pipeline_events"
+ "Event will be triggered when a pipeline status changes"
+ when "wiki_page", "wiki_page_events"
+ "Event will be triggered when a wiki page is created/updated"
+ when "commit", "commit_events"
+ "Event will be triggered when a commit is created/updated"
+ end
+ end
+
def service_event_field_name(event)
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 00e7e4230b9..733832c1bbb 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -101,4 +101,39 @@ module SnippetsHelper
# Return snippet with chunk array
{ snippet_object: snippet, snippet_chunks: snippet_chunks }
end
+
+ def snippet_embed
+ "<script src=\"#{url_for(only_path: false, overwrite_params: nil)}.js\"></script>"
+ end
+
+ def embedded_snippet_raw_button
+ blob = @snippet.blob
+ return if blob.empty? || blob.raw_binary? || blob.stored_externally?
+
+ snippet_raw_url = if @snippet.is_a?(PersonalSnippet)
+ raw_snippet_url(@snippet)
+ else
+ raw_project_snippet_url(@snippet.project, @snippet)
+ end
+
+ link_to external_snippet_icon('doc_code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw'
+ end
+
+ def embedded_snippet_download_button
+ download_url = if @snippet.is_a?(PersonalSnippet)
+ raw_snippet_url(@snippet, inline: false)
+ else
+ raw_project_snippet_url(@snippet.project, @snippet, inline: false)
+ end
+
+ link_to external_snippet_icon('download'), download_url, class: 'btn', target: '_blank', title: 'Download', rel: 'noopener noreferrer'
+ end
+
+ def public_snippet?
+ if @snippet.project_id?
+ can?(nil, :read_project_snippet, @snippet)
+ else
+ can?(nil, :read_personal_snippet, @snippet)
+ end
+ end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index b64be89c181..dc42caa70e5 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -90,7 +90,7 @@ module TreeHelper
end
def commit_in_single_accessible_branch
- branch_name = html_escape(selected_branch)
+ branch_name = ERB::Util.html_escape(selected_branch)
message = _("Your changes can be committed to %{branch_name} because a merge "\
"request is open.") % { branch_name: "<strong>#{branch_name}</strong>" }
@@ -123,7 +123,7 @@ module TreeHelper
# returns the relative path of the first subdir that doesn't have only one directory descendant
def flatten_tree(root_path, tree)
- return tree.flat_path.sub(%r{\A#{root_path}/}, '') if tree.flat_path.present?
+ return tree.flat_path.sub(%r{\A#{Regexp.escape(root_path)}/}, '') if tree.flat_path.present?
subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
if subtree.count == 1 && subtree.first.dir?
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 88f374be1e5..9f78b80c71d 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -24,8 +24,8 @@ module WorkhorseHelper
end
# Archive a Git repository and send it through Workhorse
- def send_git_archive(repository, ref:, format:)
- headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
+ def send_git_archive(repository, **kwargs)
+ headers.store(*Gitlab::Workhorse.send_git_archive(repository, **kwargs))
head :ok
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 6dae49f38dc..618d4af4272 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -46,10 +46,6 @@ class Ability
end
end
- def can_edit_note?(user, note)
- allowed?(user, :edit_note, note)
- end
-
def allowed?(user, action, subject = :global, opts = {})
if subject.is_a?(Hash)
opts, subject = subject, :global
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 2a6406d63c7..fb66dd0b766 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -16,7 +16,7 @@ class Appearance < ActiveRecord::Base
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- CACHE_KEY = 'current_appearance'.freeze
+ CACHE_KEY = "current_appearance:#{Gitlab::VERSION}".freeze
after_commit :flush_redis_cache
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 0b561203914..4aa236555cb 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -19,7 +19,7 @@ class BroadcastMessage < ActiveRecord::Base
after_commit :flush_redis_cache
def self.current
- messages = Rails.cache.fetch(CACHE_KEY) { current_and_future_messages.to_a }
+ messages = Rails.cache.fetch(CACHE_KEY, expires_in: cache_expires_in) { current_and_future_messages.to_a }
return messages if messages.empty?
@@ -36,6 +36,10 @@ class BroadcastMessage < ActiveRecord::Base
where('ends_at > :now', now: Time.zone.now).order_id_asc
end
+ def self.cache_expires_in
+ nil
+ end
+
def active?
started? && !ended?
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 18e96389199..4aa65bf4273 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -90,6 +90,7 @@ module Ci
before_save :ensure_token
before_destroy { unscoped_project }
+ before_create :ensure_metadata
after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 62d768cc6cf..44cb583e1bd 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -4,7 +4,7 @@ module Ci
include HasVariable
include Presentable
- belongs_to :group
+ belongs_to :group, class_name: "::Group"
alias_attribute :secret_value, :value
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index df57b4f65e3..fbb95fe16df 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -7,6 +7,7 @@ module Ci
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
+ before_save :update_file_store
before_save :set_size, if: :file_changed?
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
@@ -21,6 +22,10 @@ module Ci
trace: 3
}
+ def update_file_store
+ self.file_store = file.object_store
+ end
+
def self.artifacts_size_for(project)
self.where(project: project).sum(:size)
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 5a4c56ec0dc..ee0d8df8eb7 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -13,7 +13,7 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, through: :runner_projects
+ has_many :projects, -> { auto_include(false) }, through: :runner_projects
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 77947d515c1..e4a06f3f976 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -15,7 +15,7 @@ module Clusters
belongs_to :user
has_many :cluster_projects, class_name: 'Clusters::Project'
- has_many :projects, through: :cluster_projects, class_name: '::Project'
+ has_many :projects, -> { auto_include(false) }, through: :cluster_projects, class_name: '::Project'
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
diff --git a/app/models/commit.rb b/app/models/commit.rb
index b64462fb768..de860df4b9c 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -30,9 +30,12 @@ class Commit
MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
+ # Used by GFM to match and present link extensions on node texts and hrefs.
+ LINK_EXTENSION_PATTERN = /(patch)/.freeze
def banzai_render_context(field)
- context = { pipeline: :single_line, project: self.project }
+ pipeline = field == :description ? :commit_description : :single_line
+ context = { pipeline: pipeline, project: self.project }
context[:author] = self.author if self.author
context
@@ -142,7 +145,8 @@ class Commit
end
def self.link_reference_pattern
- @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
+ @link_reference_pattern ||=
+ super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})?(\.(?<extension>#{LINK_EXTENSION_PATTERN}))?/)
end
def to_reference(from = nil, full: false)
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index d8394415362..fce37e7f78e 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -79,11 +79,7 @@ module Awardable
end
def user_can_award?(current_user, name)
- if user_authored?(current_user)
- !awardable_votes?(normalize_name(name))
- else
- true
- end
+ awardable_by_user?(current_user, name) && Ability.allowed?(current_user, :award_emoji, self)
end
def user_authored?(current_user)
@@ -119,4 +115,12 @@ module Awardable
def normalize_name(name)
Gitlab::Emoji.normalize_emoji_name(name)
end
+
+ def awardable_by_user?(current_user, name)
+ if user_authored?(current_user)
+ !awardable_votes?(normalize_name(name))
+ else
+ true
+ end
+ end
end
diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb
index fa1eafb1d7a..593a9b3d71d 100644
--- a/app/models/concerns/chronic_duration_attribute.rb
+++ b/app/models/concerns/chronic_duration_attribute.rb
@@ -8,14 +8,14 @@ module ChronicDurationAttribute
end
end
- def chronic_duration_attr_writer(virtual_attribute, source_attribute)
+ def chronic_duration_attr_writer(virtual_attribute, source_attribute, parameters = {})
chronic_duration_attr_reader(virtual_attribute, source_attribute)
define_method("#{virtual_attribute}=") do |value|
- chronic_duration_attributes[virtual_attribute] = value.presence || ''
+ chronic_duration_attributes[virtual_attribute] = value.presence || parameters[:default].presence.to_s
begin
- new_value = ChronicDuration.parse(value).to_i if value.present?
+ new_value = value.present? ? ChronicDuration.parse(value).to_i : parameters[:default].presence
assign_attributes(source_attribute => new_value)
rescue ChronicDuration::DurationParseError
# ignore error as it will be caught by validation
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index b45395343cc..d9416352f9c 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -48,7 +48,7 @@ module Issuable
end
has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :labels, through: :label_links
+ has_many :labels, -> { auto_include(false) }, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :metrics
diff --git a/app/models/concerns/presentable.rb b/app/models/concerns/presentable.rb
index 7b33b837004..bc4fbd19a02 100644
--- a/app/models/concerns/presentable.rb
+++ b/app/models/concerns/presentable.rb
@@ -1,4 +1,12 @@
module Presentable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def present(attributes)
+ all.map { |klass_object| klass_object.present(attributes) }
+ end
+ end
+
def present(**attributes)
Gitlab::View::Presenter::Factory
.new(self, attributes)
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index 7c236369793..399abb67c9d 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -102,7 +102,7 @@ module ResolvableDiscussion
yield(notes_relation)
# Set the notes array to the updated notes
- @notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @notes = notes_relation.fresh.auto_include(false).to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
self.class.memoized_values.each do |name|
clear_memoization(name)
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 89a74b7dcb1..858b7ef533e 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -2,7 +2,7 @@ class DeployKey < Key
include IgnorableColumn
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, through: :deploy_keys_projects
+ has_many :projects, -> { auto_include(false) }, through: :deploy_keys_projects
scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) }
scope :are_public, -> { where(public: true) }
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
new file mode 100644
index 00000000000..8dae821a10e
--- /dev/null
+++ b/app/models/deploy_token.rb
@@ -0,0 +1,61 @@
+class DeployToken < ActiveRecord::Base
+ include Expirable
+ include TokenAuthenticatable
+ add_authentication_token_field :token
+
+ AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
+
+ default_value_for(:expires_at) { Forever.date }
+
+ has_many :project_deploy_tokens, inverse_of: :deploy_token
+ has_many :projects, -> { auto_include(false) }, through: :project_deploy_tokens
+
+ validate :ensure_at_least_one_scope
+ before_save :ensure_token
+
+ accepts_nested_attributes_for :project_deploy_tokens
+
+ scope :active, -> { where("revoked = false AND expires_at >= NOW()") }
+
+ def revoke!
+ update!(revoked: true)
+ end
+
+ def active?
+ !revoked
+ end
+
+ def scopes
+ AVAILABLE_SCOPES.select { |token_scope| read_attribute(token_scope) }
+ end
+
+ def username
+ "gitlab+deploy-token-#{id}"
+ end
+
+ def has_access_to?(requested_project)
+ active? && project == requested_project
+ end
+
+ # This is temporal. Currently we limit DeployToken
+ # to a single project, later we're going to extend
+ # that to be for multiple projects and namespaces.
+ def project
+ projects.first
+ end
+
+ def expires_at
+ expires_at = read_attribute(:expires_at)
+ expires_at != Forever.date ? expires_at : nil
+ end
+
+ def expires_at=(value)
+ write_attribute(:expires_at, value.presence || Forever.date)
+ end
+
+ private
+
+ def ensure_at_least_one_scope
+ errors.add(:base, "Scopes can't be blank") unless read_repository || read_registry
+ end
+end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 9517723d9d9..fddb269af4b 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -224,7 +224,7 @@ class Environment < ActiveRecord::Base
end
def deployment_platform
- project.deployment_platform(environment: self)
+ project.deployment_platform(environment: self.name)
end
private
diff --git a/app/models/event.rb b/app/models/event.rb
index 3805f6cf857..741a84194e2 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -110,7 +110,10 @@ class Event < ActiveRecord::Base
end
end
+ # Remove this method when removing Gitlab.rails5? code.
def subclass_from_attributes(attrs)
+ return super if Gitlab.rails5?
+
# Without this Rails will keep calling this method on the returned class,
# resulting in an infinite loop.
return unless self == Event
diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb
index 7f1728e8c77..aad3509b895 100644
--- a/app/models/fork_network.rb
+++ b/app/models/fork_network.rb
@@ -1,7 +1,7 @@
class ForkNetwork < ActiveRecord::Base
belongs_to :root_project, class_name: 'Project'
has_many :fork_network_members
- has_many :projects, through: :fork_network_members
+ has_many :projects, -> { auto_include(false) }, through: :fork_network_members
after_create :add_root_as_member, if: :root_project
diff --git a/app/models/group.rb b/app/models/group.rb
index 3cfe21ac93b..202988d743d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -12,9 +12,9 @@ class Group < Namespace
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
- has_many :users, through: :group_members
+ has_many :users, -> { auto_include(false) }, through: :group_members
has_many :owners,
- -> { where(members: { access_level: Gitlab::Access::OWNER }) },
+ -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) },
through: :group_members,
source: :user
@@ -23,7 +23,7 @@ class Group < Namespace
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :shared_projects, through: :project_group_links, source: :project
+ has_many :shared_projects, -> { auto_include(false) }, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
@@ -286,6 +286,10 @@ class Group < Namespace
false
end
+ def refresh_project_authorizations
+ refresh_members_authorized_projects(blocking: false)
+ end
+
private
def update_two_factor_requirement
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index b6dd39b860b..ec072882cc9 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -7,6 +7,7 @@ class ProjectHook < WebHook
:issue_hooks,
:confidential_issue_hooks,
:note_hooks,
+ :confidential_note_hooks,
:merge_request_hooks,
:job_hooks,
:pipeline_hooks,
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index cbec735c2dd..96a43006642 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -23,9 +23,12 @@ class InternalId < ActiveRecord::Base
#
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
# As such, the increment is atomic and safe to be called concurrently.
- def increment_and_save!
+ #
+ # If a `maximum_iid` is passed in, this overrides the incremented value if it's
+ # greater than that. This can be used to correct the increment value if necessary.
+ def increment_and_save!(maximum_iid)
lock!
- self.last_value = (last_value || 0) + 1
+ self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max
save!
last_value
end
@@ -89,7 +92,16 @@ class InternalId < ActiveRecord::Base
# and increment its last value
#
# Note this will acquire a ROW SHARE lock on the InternalId record
- (lookup || create_record).increment_and_save!
+
+ # Note we always calculate the maximum iid present here and
+ # pass it in to correct the InternalId entry if it's last_value is off.
+ #
+ # This can happen in a transition phase where both `AtomicInternalId` and
+ # `NonatomicInternalId` code runs (e.g. during a deploy).
+ #
+ # This is subject to be cleaned up with the 10.8 release:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/45389.
+ (lookup || create_record).increment_and_save!(maximum_iid)
end
end
@@ -115,11 +127,15 @@ class InternalId < ActiveRecord::Base
InternalId.create!(
**scope,
usage: usage_value,
- last_value: init.call(subject) || 0
+ last_value: maximum_iid
)
end
rescue ActiveRecord::RecordNotUnique
lookup
end
+
+ def maximum_iid
+ @maximum_iid ||= init.call(subject) || 0
+ end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 13abc6c1a0d..c1ffe6512ea 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -34,7 +34,7 @@ class Issue < ActiveRecord::Base
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
- has_many :assignees, class_name: "User", through: :issue_assignees
+ has_many :assignees, -> { auto_include(false) }, class_name: "User", through: :issue_assignees
validates :project, presence: true
@@ -272,11 +272,17 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- if options.key?(:sidebar_endpoints) && project
+ if options.key?(:issue_endpoints) && project
url_helper = Gitlab::Routing.url_helpers
- json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
- toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self))
+ issue_reference = options[:include_full_project_path] ? to_reference(full: true) : to_reference
+
+ json.merge!(
+ reference_path: issue_reference,
+ real_path: url_helper.project_issue_path(project, self),
+ issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
+ toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self)
+ )
end
if options.key?(:labels)
diff --git a/app/models/label.rb b/app/models/label.rb
index de7f1d56c64..f3496884cff 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -18,8 +18,8 @@ class Label < ActiveRecord::Base
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
- has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
+ has_many :issues, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'Issue'
+ has_many :merge_requests, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'MergeRequest'
before_validation :strip_whitespace_from_title_and_color
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index b7de46fa202..ed95613ee59 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -3,7 +3,7 @@ class LfsObject < ActiveRecord::Base
include ObjectStorage::BackgroundMove
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, through: :lfs_objects_projects
+ has_many :projects, -> { auto_include(false) }, through: :lfs_objects_projects
scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index b75387e236e..1c2e57bb01f 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -17,7 +17,7 @@ class MergeRequestDiffCommit < ActiveRecord::Base
commit_hash.merge(
merge_request_diff_id: merge_request_diff_id,
relative_order: index,
- sha: sha_attribute.type_cast_for_database(sha),
+ sha: sha_attribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize
authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]),
committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date])
)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index dafae58d121..8e33bab81c1 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -22,7 +22,7 @@ class Milestone < ActiveRecord::Base
belongs_to :group
has_many :issues
- has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
+ has_many :labels, -> { distinct.reorder('labels.title').auto_include(false) }, through: :issues
has_many :merge_requests
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -34,8 +34,8 @@ class Milestone < ActiveRecord::Base
scope :for_projects_and_groups, -> (project_ids, group_ids) do
conditions = []
- conditions << arel_table[:project_id].in(project_ids) if project_ids.compact.any?
- conditions << arel_table[:group_id].in(group_ids) if group_ids.compact.any?
+ conditions << arel_table[:project_id].in(project_ids) if project_ids&.compact&.any?
+ conditions << arel_table[:group_id].in(group_ids) if group_ids&.compact&.any?
where(conditions.reduce(:or))
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index e350b675639..2b63aa33222 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -252,6 +252,10 @@ class Namespace < ActiveRecord::Base
[]
end
+ def refresh_project_authorizations
+ owner.refresh_authorized_projects
+ end
+
private
def path_or_parent_changed?
diff --git a/app/models/note.rb b/app/models/note.rb
index 0f5fb529a87..e426f84832b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -268,6 +268,10 @@ class Note < ActiveRecord::Base
self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
end
+ def confidential?
+ noteable.try(:confidential?)
+ end
+
def editable?
!system?
end
@@ -313,6 +317,10 @@ class Note < ActiveRecord::Base
!system? && !for_snippet?
end
+ def can_create_notification?
+ true
+ end
+
def discussion_class(noteable = nil)
# When commit notes are rendered on an MR's Discussion page, they are
# displayed in one discussion instead of individually.
diff --git a/app/models/project.rb b/app/models/project.rb
index 714a15ade9c..ffd78d3ab70 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -21,6 +21,7 @@ class Project < ActiveRecord::Base
include Gitlab::SQL::Pattern
include DeploymentPlatform
include ::Gitlab::Utils::StrongMemoize
+ include ChronicDurationAttribute
extend Gitlab::ConfigHelper
@@ -137,11 +138,11 @@ class Project < ActiveRecord::Base
has_one :packagist_service
# TODO: replace these relations with the fork network versions
- has_one :forked_project_link, foreign_key: "forked_to_project_id"
- has_one :forked_from_project, through: :forked_project_link
+ has_one :forked_project_link, foreign_key: "forked_to_project_id"
+ has_one :forked_from_project, -> { auto_include(false) }, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id"
- has_many :forks, through: :forked_project_links, source: :forked_to_project
+ has_many :forks, -> { auto_include(false) }, through: :forked_project_links, source: :forked_to_project
# TODO: replace these relations with the fork network versions
has_one :root_of_fork_network,
@@ -149,7 +150,7 @@ class Project < ActiveRecord::Base
inverse_of: :root_project,
class_name: 'ForkNetwork'
has_one :fork_network_member
- has_one :fork_network, through: :fork_network_member
+ has_one :fork_network, -> { auto_include(false) }, through: :fork_network_member
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
@@ -166,27 +167,27 @@ class Project < ActiveRecord::Base
has_many :protected_tags
has_many :project_authorizations
- has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
+ has_many :authorized_users, -> { auto_include(false) }, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :project_members
- has_many :users, through: :project_members
+ has_many :users, -> { auto_include(false) }, through: :project_members
has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects
- has_many :deploy_keys, through: :deploy_keys_projects
+ has_many :deploy_keys, -> { auto_include(false) }, through: :deploy_keys_projects
has_many :users_star_projects
- has_many :starrers, through: :users_star_projects, source: :user
+ has_many :starrers, -> { auto_include(false) }, through: :users_star_projects, source: :user
has_many :releases
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :lfs_objects, through: :lfs_objects_projects
+ has_many :lfs_objects, -> { auto_include(false) }, through: :lfs_objects_projects
has_many :lfs_file_locks
has_many :project_group_links
- has_many :invited_groups, through: :project_group_links, source: :group
+ has_many :invited_groups, -> { auto_include(false) }, through: :project_group_links, source: :group
has_many :pages_domains
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
@@ -198,7 +199,7 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project'
- has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
+ has_many :clusters, -> { auto_include(false) }, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -215,14 +216,16 @@ class Project < ActiveRecord::Base
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :runner_projects, class_name: 'Ci::RunnerProject'
- has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
+ has_many :runners, -> { auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
+ has_many :project_deploy_tokens
+ has_many :deploy_tokens, -> { auto_include(false) }, through: :project_deploy_tokens
- has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
+ has_many :active_runners, -> { active.auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
@@ -325,6 +328,12 @@ class Project < ActiveRecord::Base
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
+ chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
+
+ validates :build_timeout, allow_nil: true,
+ numericality: { greater_than_or_equal_to: 600,
+ message: 'needs to be at least 10 minutes' }
+
# Returns a collection of projects that is either public or visible to the
# logged in user.
def self.public_or_visible_to_user(user = nil)
@@ -630,7 +639,7 @@ class Project < ActiveRecord::Base
end
def create_or_update_import_data(data: nil, credentials: nil)
- return unless import_url.present? && valid_import_url?
+ return if data.nil? && credentials.nil?
project_import_data = import_data || build_import_data
if data
@@ -1066,6 +1075,16 @@ class Project < ActiveRecord::Base
end
end
+ # This will return all `lfs_objects` that are accessible to the project.
+ # So this might be `self.lfs_objects` if the project is not part of a fork
+ # network, or it is the base of the fork network.
+ #
+ # TODO: refactor this to get the correct lfs objects when implementing
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
+ def all_lfs_objects
+ lfs_storage_project.lfs_objects
+ end
+
def personal?
!group
end
@@ -1299,14 +1318,6 @@ class Project < ActiveRecord::Base
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
- def build_timeout_in_minutes
- build_timeout / 60
- end
-
- def build_timeout_in_minutes=(value)
- self.build_timeout = value.to_i * 60
- end
-
def open_issues_count
Projects::OpenIssuesCountService.new(self).count
end
@@ -1463,7 +1474,9 @@ class Project < ActiveRecord::Base
end
def rename_repo_notify!
- send_move_instructions(full_path_was)
+ # 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.
+ send_move_instructions(full_path_was) unless started?
expires_full_path_cache
self.old_path_with_namespace = full_path_was
@@ -1478,6 +1491,7 @@ class Project < ActiveRecord::Base
remove_import_jid
update_project_counter_caches
after_create_default_branch
+ refresh_markdown_cache!
end
def update_project_counter_caches
diff --git a/app/models/project_deploy_token.rb b/app/models/project_deploy_token.rb
new file mode 100644
index 00000000000..ab4482f0c0b
--- /dev/null
+++ b/app/models/project_deploy_token.rb
@@ -0,0 +1,8 @@
+class ProjectDeployToken < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :deploy_token, inverse_of: :project_deploy_tokens
+
+ validates :deploy_token, presence: true
+ validates :project, presence: true
+ validates :deploy_token_id, uniqueness: { scope: [:project_id] }
+end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index dab0ea1a681..7591ab4f478 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -21,8 +21,16 @@ class ChatNotificationService < Service
end
end
+ def confidential_issue_channel
+ properties['confidential_issue_channel'].presence || properties['issue_channel']
+ end
+
+ def confidential_note_channel
+ properties['confidential_note_channel'].presence || properties['note_channel']
+ end
+
def self.supported_events
- %w[push issue confidential_issue merge_request note tag_push
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
@@ -55,7 +63,9 @@ class ChatNotificationService < Service
return false unless message
- channel_name = get_channel_field(object_kind).presence || channel
+ event_type = data[:event_type] || object_kind
+
+ channel_name = get_channel_field(event_type).presence || channel
opts = {}
opts[:channel] = channel_name if channel_name
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index f31c3f02af2..dce878e485f 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -46,7 +46,7 @@ class HipchatService < Service
end
def self.supported_events
- %w(push issue confidential_issue merge_request note tag_push pipeline)
+ %w(push issue confidential_issue merge_request note confidential_note tag_push pipeline)
end
def execute(data)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index fd1afafe4df..5bdaa7f0720 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -331,6 +331,7 @@ class Repository
return unless empty?
expire_method_caches(%i(has_visible_content?))
+ raw_repository.expire_has_local_branches_cache
end
def lookup_cache
diff --git a/app/models/service.rb b/app/models/service.rb
index 7424cef0fc0..f7e3f7590ad 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -14,6 +14,7 @@ class Service < ActiveRecord::Base
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
+ default_value_for :confidential_note_events, true
default_value_for :job_events, true
default_value_for :pipeline_events, true
default_value_for :wiki_page_events, true
@@ -42,6 +43,7 @@ class Service < ActiveRecord::Base
scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
+ scope :confidential_note_hooks, -> { where(confidential_note_events: true, active: true) }
scope :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
@@ -168,8 +170,10 @@ class Service < ActiveRecord::Base
def self.prop_accessor(*args)
args.each do |arg|
class_eval %{
- def #{arg}
- properties['#{arg}']
+ unless method_defined?(arg)
+ def #{arg}
+ properties['#{arg}']
+ end
end
def #{arg}=(value)
@@ -202,7 +206,11 @@ class Service < ActiveRecord::Base
args.each do |arg|
class_eval %{
def #{arg}?
- ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg})
+ if Gitlab.rails5?
+ !ActiveModel::Type::Boolean::FALSE_VALUES.include?(#{arg})
+ else
+ ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg})
+ end
end
}
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index a2ab405fdbe..aad2c1dac4e 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -22,7 +22,7 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
- belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :target, -> { auto_include(false) }, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true
diff --git a/app/models/user.rb b/app/models/user.rb
index ba51595e6a3..d5c5c0964c5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -96,23 +96,23 @@ class User < ActiveRecord::Base
# Groups
has_many :members
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
- has_many :groups, through: :group_members
- has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
- has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
+ has_many :groups, -> { auto_include(false) }, through: :group_members
+ has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) }, through: :group_members, source: :group
+ has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }).auto_include(false) }, through: :group_members, source: :group
# Projects
- has_many :groups_projects, through: :groups, source: :projects
- has_many :personal_projects, through: :namespace, source: :projects
+ has_many :groups_projects, -> { auto_include(false) }, through: :groups, source: :projects
+ has_many :personal_projects, -> { auto_include(false) }, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) }
- has_many :projects, through: :project_members
- has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
+ has_many :projects, -> { auto_include(false) }, through: :project_members
+ has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :starred_projects, through: :users_star_projects, source: :project
+ has_many :starred_projects, -> { auto_include(false) }, through: :users_star_projects, source: :project
has_many :project_authorizations
- has_many :authorized_projects, through: :project_authorizations, source: :project
+ has_many :authorized_projects, -> { auto_include(false) }, through: :project_authorizations, source: :project
has_many :user_interacted_projects
- has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
+ has_many :project_interactions, -> { auto_include(false) }, through: :user_interacted_projects, source: :project, class_name: 'Project'
has_many :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
@@ -132,7 +132,7 @@ class User < ActiveRecord::Base
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
- has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
+ has_many :assigned_issues, -> { auto_include(false) }, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
@@ -164,12 +164,15 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
+ before_save :set_notification_email, if: :email_changed? # in case validation is skipped
before_validation :set_public_email, if: :public_email_changed?
+ before_save :set_public_email, if: :public_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) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
before_validation :ensure_namespace_correct
+ before_save :ensure_namespace_correct # in case validation is skipped
after_validation :set_username_errors
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
@@ -408,7 +411,6 @@ class User < ActiveRecord::Base
unique_internal(where(ghost: true), 'ghost', email) do |u|
u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
u.name = 'Ghost User'
- u.notification_email = email
end
end
end
@@ -698,10 +700,6 @@ class User < ActiveRecord::Base
projects_limit - personal_projects_count
end
- def personal_projects_count
- @personal_projects_count ||= personal_projects.count
- end
-
def recent_push(project = nil)
service = Users::LastPushEventService.new(self)
@@ -995,7 +993,7 @@ class User < ActiveRecord::Base
def ci_authorized_runners
@ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject
- .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ .where(project: authorized_projects(Gitlab::Access::MASTER))
.select(:runner_id)
Ci::Runner.specific.where(id: runner_ids)
end
@@ -1044,9 +1042,10 @@ class User < ActiveRecord::Base
end
end
- def update_cache_counts
- assigned_open_merge_requests_count(force: true)
- assigned_open_issues_count(force: true)
+ def personal_projects_count(force: false)
+ Rails.cache.fetch(['users', id, 'personal_projects_count'], force: force, expires_in: 24.hours, raw: true) do
+ personal_projects.count
+ end.to_i
end
def update_todos_count_cache
@@ -1059,6 +1058,7 @@ class User < ActiveRecord::Base
invalidate_merge_request_cache_counts
invalidate_todos_done_count
invalidate_todos_pending_count
+ invalidate_personal_projects_count
end
def invalidate_issue_cache_counts
@@ -1077,6 +1077,10 @@ class User < ActiveRecord::Base
Rails.cache.delete(['users', id, 'todos_pending_count'])
end
+ def invalidate_personal_projects_count
+ Rails.cache.delete(['users', id, 'personal_projects_count'])
+ end
+
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
# flow means we don't call that automatically (and can't conveniently do so).
#
@@ -1200,15 +1204,6 @@ class User < ActiveRecord::Base
], remove_duplicates: false)
end
- def ci_projects_union
- scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
- groups = groups_projects.where(members: scope)
- other = projects.where(members: scope)
-
- Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id),
- other.select(:id)])
- end
-
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
def send_devise_notification(notification, *args)
return true unless can?(:receive_notifications)
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 1ab391a5a9d..808a81cbbf9 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -11,7 +11,7 @@ module Ci
end
condition(:owner_of_job) do
- can?(:developer_access) && @subject.triggered_by?(@user)
+ @subject.triggered_by?(@user)
end
rule { protected_ref }.policy do
@@ -19,6 +19,6 @@ module Ci
prevent :erase_build
end
- rule { can?(:master_access) | owner_of_job }.enable :erase_build
+ rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
end
end
diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb
index dc7a4aed577..ecba0488d3c 100644
--- a/app/policies/ci/pipeline_schedule_policy.rb
+++ b/app/policies/ci/pipeline_schedule_policy.rb
@@ -7,23 +7,17 @@ module Ci
end
condition(:owner_of_schedule) do
- can?(:developer_access) && pipeline_schedule.owned_by?(@user)
+ pipeline_schedule.owned_by?(@user)
end
- condition(:non_owner_of_schedule) do
- !pipeline_schedule.owned_by?(@user)
- end
-
- rule { can?(:developer_access) }.policy do
- enable :play_pipeline_schedule
- end
+ rule { can?(:create_pipeline) }.enable :play_pipeline_schedule
- rule { can?(:master_access) | owner_of_schedule }.policy do
+ rule { can?(:admin_pipeline) | (can?(:update_build) & owner_of_schedule) }.policy do
enable :update_pipeline_schedule
enable :admin_pipeline_schedule
end
- rule { can?(:master_access) & non_owner_of_schedule }.policy do
+ rule { can?(:admin_pipeline_schedule) & ~owner_of_schedule }.policy do
enable :take_ownership_pipeline_schedule
end
diff --git a/app/policies/deploy_token_policy.rb b/app/policies/deploy_token_policy.rb
new file mode 100644
index 00000000000..7aa9106e8b1
--- /dev/null
+++ b/app/policies/deploy_token_policy.rb
@@ -0,0 +1,11 @@
+class DeployTokenPolicy < BasePolicy
+ with_options scope: :subject, score: 0
+ condition(:master) { @subject.project.team.master?(@user) }
+
+ rule { anonymous }.prevent_all
+
+ rule { master }.policy do
+ enable :create_deploy_token
+ enable :update_deploy_token
+ end
+end
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index 3f6d7d04667..b431d376e3d 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -2,20 +2,6 @@ class IssuablePolicy < BasePolicy
delegate { @subject.project }
condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? }
-
- # We aren't checking `:read_issue` or `:read_merge_request` in this case
- # because it could be possible for a user to see an issuable-iid
- # (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be allowed
- # to read the actual issue after a more expensive `:read_issue` check.
- #
- # `:read_issue` & `:read_issue_iid` could diverge in gitlab-ee.
- condition(:visible_to_user, score: 4) do
- Project.where(id: @subject.project)
- .public_or_visible_to_user(@user)
- .with_feature_available_for_user(@subject, @user)
- .any?
- end
-
condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) }
desc "User is the assignee or author"
@@ -32,9 +18,7 @@ class IssuablePolicy < BasePolicy
rule { locked & ~is_project_member }.policy do
prevent :create_note
- prevent :update_note
prevent :admin_note
prevent :resolve_note
- prevent :edit_note
end
end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index ed499511999..263c6e3039c 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -17,6 +17,4 @@ class IssuePolicy < IssuablePolicy
prevent :update_issue
prevent :admin_issue
end
-
- rule { can?(:read_issue) | visible_to_user }.enable :read_issue_iid
end
diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb
index e003376d219..c3fe857f8a2 100644
--- a/app/policies/merge_request_policy.rb
+++ b/app/policies/merge_request_policy.rb
@@ -1,3 +1,2 @@
class MergeRequestPolicy < IssuablePolicy
- rule { can?(:read_merge_request) | visible_to_user }.enable :read_merge_request_iid
end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index d4cb5a77e63..077a6761ee6 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -1,26 +1,21 @@
class NotePolicy < BasePolicy
delegate { @subject.project }
- delegate { @subject.noteable if @subject.noteable.lockable? }
+ delegate { @subject.noteable if DeclarativePolicy.has_policy?(@subject.noteable) }
condition(:is_author) { @user && @subject.author == @user }
- condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? }
condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id }
condition(:editable, scope: :subject) { @subject.editable? }
- rule { ~editable | anonymous }.prevent :edit_note
-
- rule { is_author | admin }.enable :edit_note
- rule { can?(:master_access) }.enable :edit_note
+ rule { ~editable }.prevent :admin_note
rule { is_author }.policy do
enable :read_note
- enable :update_note
enable :admin_note
enable :resolve_note
end
- rule { for_merge_request & is_noteable_author }.policy do
+ rule { is_noteable_author }.policy do
enable :resolve_note
end
end
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index cac0530b9f7..c1a84727cfa 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -25,4 +25,6 @@ class PersonalSnippetPolicy < BasePolicy
end
rule { anonymous }.prevent :comment_personal_snippet
+
+ rule { can?(:comment_personal_snippet) }.enable :award_emoji
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 57ab0c23dcd..3529d0aa60c 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -1,12 +1,26 @@
class ProjectPolicy < BasePolicy
- def self.create_read_update_admin(name)
- [
- :"create_#{name}",
- :"read_#{name}",
- :"update_#{name}",
- :"admin_#{name}"
- ]
- end
+ extend ClassMethods
+
+ READONLY_FEATURES_WHEN_ARCHIVED = %i[
+ issue
+ list
+ merge_request
+ label
+ milestone
+ project_snippet
+ wiki
+ note
+ pipeline
+ pipeline_schedule
+ build
+ trigger
+ environment
+ deployment
+ commit_status
+ container_image
+ pages
+ cluster
+ ].freeze
desc "User is a project owner"
condition :owner do
@@ -15,7 +29,7 @@ class ProjectPolicy < BasePolicy
end
desc "Project has public builds enabled"
- condition(:public_builds, scope: :subject) { project.public_builds? }
+ condition(:public_builds, scope: :subject, score: 0) { project.public_builds? }
# For guest access we use #team_member? so we can use
# project.members, which gets cached in subject scope.
@@ -35,7 +49,7 @@ class ProjectPolicy < BasePolicy
condition(:master) { team_access_level >= Gitlab::Access::MASTER }
desc "Project is public"
- condition(:public_project, scope: :subject) { project.public? }
+ condition(:public_project, scope: :subject, score: 0) { project.public? }
desc "Project is visible to internal users"
condition(:internal_access) do
@@ -46,7 +60,7 @@ class ProjectPolicy < BasePolicy
condition(:group_member, scope: :subject) { project_group_member? }
desc "Project is archived"
- condition(:archived, scope: :subject) { project.archived? }
+ condition(:archived, scope: :subject, score: 0) { project.archived? }
condition(:default_issues_tracker, scope: :subject) { project.default_issues_tracker? }
@@ -56,16 +70,32 @@ class ProjectPolicy < BasePolicy
end
desc "Project has an external wiki"
- condition(:has_external_wiki, scope: :subject) { project.has_external_wiki? }
+ condition(:has_external_wiki, scope: :subject, score: 0) { project.has_external_wiki? }
desc "Project has request access enabled"
- condition(:request_access_enabled, scope: :subject) { project.request_access_enabled }
+ condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled }
desc "Has merge requests allowing pushes to user"
condition(:has_merge_requests_allowing_pushes, scope: :subject) do
project.merge_requests_allowing_push_to_user(user).any?
end
+ # We aren't checking `:read_issue` or `:read_merge_request` in this case
+ # because it could be possible for a user to see an issuable-iid
+ # (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
+ # allowed to read the actual issue after a more expensive `:read_issue`
+ # check. These checks are intended to be used alongside
+ # `:read_project_for_iids`.
+ #
+ # `:read_issue` & `:read_issue_iid` could diverge in gitlab-ee.
+ condition(:issues_visible_to_user, score: 4) do
+ @subject.feature_available?(:issues, @user)
+ end
+
+ condition(:merge_requests_visible_to_user, score: 4) do
+ @subject.feature_available?(:merge_requests, @user)
+ end
+
features = %w[
merge_requests
issues
@@ -81,6 +111,10 @@ class ProjectPolicy < BasePolicy
condition(:"#{f}_disabled", score: 32) { !feature_available?(f.to_sym) }
end
+ # `:read_project` may be prevented in EE, but `:read_project_for_iids` should
+ # not.
+ rule { guest | admin }.enable :read_project_for_iids
+
rule { guest }.enable :guest_access
rule { reporter }.enable :reporter_access
rule { developer }.enable :developer_access
@@ -106,6 +140,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:guest_access) }.policy do
enable :read_project
+ enable :create_merge_request_in
enable :read_board
enable :read_list
enable :read_wiki
@@ -120,10 +155,11 @@ class ProjectPolicy < BasePolicy
enable :create_note
enable :upload_file
enable :read_cycle_analytics
+ enable :award_emoji
end
# These abilities are not allowed to admins that are not members of the project,
- # that's why they are defined separatly.
+ # that's why they are defined separately.
rule { guest & can?(:download_code) }.enable :build_download_code
rule { guest & can?(:read_container_image) }.enable :build_read_container_image
@@ -150,6 +186,7 @@ class ProjectPolicy < BasePolicy
# where we enable or prevent it based on other coditions.
rule { (~anonymous & public_project) | internal_access }.policy do
enable :public_user_access
+ enable :read_project_for_iids
end
rule { can?(:public_user_access) }.policy do
@@ -176,7 +213,7 @@ class ProjectPolicy < BasePolicy
enable :create_pipeline
enable :update_pipeline
enable :create_pipeline_schedule
- enable :create_merge_request
+ enable :create_merge_request_from
enable :create_wiki
enable :push_code
enable :resolve_note
@@ -187,7 +224,7 @@ class ProjectPolicy < BasePolicy
end
rule { can?(:master_access) }.policy do
- enable :delete_protected_branch
+ enable :push_to_delete_protected_branch
enable :update_project_snippet
enable :update_environment
enable :update_deployment
@@ -210,37 +247,50 @@ class ProjectPolicy < BasePolicy
end
rule { archived }.policy do
- prevent :create_merge_request
prevent :push_code
- prevent :delete_protected_branch
- prevent :update_merge_request
- prevent :admin_merge_request
+ prevent :push_to_delete_protected_branch
+ prevent :request_access
+ prevent :upload_file
+ prevent :resolve_note
+ prevent :create_merge_request_from
+ prevent :create_merge_request_in
+ prevent :award_emoji
+
+ READONLY_FEATURES_WHEN_ARCHIVED.each do |feature|
+ prevent(*create_update_admin_destroy(feature))
+ end
+ end
+
+ rule { issues_disabled }.policy do
+ prevent(*create_read_update_admin_destroy(:issue))
end
rule { merge_requests_disabled | repository_disabled }.policy do
- prevent(*create_read_update_admin(:merge_request))
+ prevent :create_merge_request_in
+ prevent :create_merge_request_from
+ prevent(*create_read_update_admin_destroy(:merge_request))
end
rule { issues_disabled & merge_requests_disabled }.policy do
- prevent(*create_read_update_admin(:label))
- prevent(*create_read_update_admin(:milestone))
+ prevent(*create_read_update_admin_destroy(:label))
+ prevent(*create_read_update_admin_destroy(:milestone))
end
rule { snippets_disabled }.policy do
- prevent(*create_read_update_admin(:project_snippet))
+ prevent(*create_read_update_admin_destroy(:project_snippet))
end
rule { wiki_disabled & ~has_external_wiki }.policy do
- prevent(*create_read_update_admin(:wiki))
+ prevent(*create_read_update_admin_destroy(:wiki))
prevent(:download_wiki_code)
end
rule { builds_disabled | repository_disabled }.policy do
- prevent(*create_read_update_admin(:build))
- prevent(*(create_read_update_admin(:pipeline) - [:read_pipeline]))
- prevent(*create_read_update_admin(:pipeline_schedule))
- prevent(*create_read_update_admin(:environment))
- prevent(*create_read_update_admin(:deployment))
+ prevent(*create_update_admin_destroy(:pipeline))
+ prevent(*create_read_update_admin_destroy(:build))
+ prevent(*create_read_update_admin_destroy(:pipeline_schedule))
+ prevent(*create_read_update_admin_destroy(:environment))
+ prevent(*create_read_update_admin_destroy(:deployment))
end
rule { repository_disabled }.policy do
@@ -251,11 +301,15 @@ class ProjectPolicy < BasePolicy
end
rule { container_registry_disabled }.policy do
- prevent(*create_read_update_admin(:container_image))
+ prevent(*create_read_update_admin_destroy(:container_image))
end
rule { anonymous & ~public_project }.prevent_all
- rule { public_project }.enable(:public_access)
+
+ rule { public_project }.policy do
+ enable :public_access
+ enable :read_project_for_iids
+ end
rule { can?(:public_access) }.policy do
enable :read_project
@@ -289,13 +343,6 @@ class ProjectPolicy < BasePolicy
enable :read_pipeline_schedule
end
- rule { issues_disabled }.policy do
- prevent :create_issue
- prevent :update_issue
- prevent :admin_issue
- prevent :read_issue
- end
-
# These rules are included to allow maintainers of projects to push to certain
# to run pipelines for the branches they have access to.
rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
@@ -305,6 +352,14 @@ class ProjectPolicy < BasePolicy
enable :update_pipeline
end
+ rule do
+ (can?(:read_project_for_iids) & issues_visible_to_user) | can?(:read_issue)
+ end.enable :read_issue_iid
+
+ rule do
+ (can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
+ end.enable :read_merge_request_iid
+
private
def team_member?
diff --git a/app/policies/project_policy/class_methods.rb b/app/policies/project_policy/class_methods.rb
new file mode 100644
index 00000000000..60e5aba00ba
--- /dev/null
+++ b/app/policies/project_policy/class_methods.rb
@@ -0,0 +1,19 @@
+class ProjectPolicy
+ module ClassMethods
+ def create_read_update_admin_destroy(name)
+ [
+ :"read_#{name}",
+ *create_update_admin_destroy(name)
+ ]
+ end
+
+ def create_update_admin_destroy(name)
+ [
+ :"create_#{name}",
+ :"update_#{name}",
+ :"admin_#{name}",
+ :"destroy_#{name}"
+ ]
+ end
+ end
+end
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 255475e1fe6..9afebda19be 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -15,6 +15,8 @@ module Ci
def status_title
if auto_canceled?
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
+ else
+ tooltip_for_badge
end
end
@@ -28,5 +30,19 @@ module Ci
trigger_request.user_variables
end
end
+
+ def tooltip_message
+ "#{subject.name} - #{detailed_status.status_tooltip}"
+ end
+
+ private
+
+ def tooltip_for_badge
+ detailed_status.badge_tooltip.capitalize
+ end
+
+ def detailed_status
+ @detailed_status ||= subject.detailed_status(user)
+ end
end
end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 9f3f2637183..4b4132af2d0 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -3,6 +3,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper
include MarkupHelper
include TreeHelper
+ include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
presents :merge_request
@@ -152,11 +153,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def can_revert_on_current_merge_request?
- user_can_collaborate_with_project? && cached_can_be_reverted?
+ can_collaborate_with_project?(project) && cached_can_be_reverted?
end
def can_cherry_pick_on_current_merge_request?
- user_can_collaborate_with_project? && can_be_cherry_picked?
+ can_collaborate_with_project?(project) && can_be_cherry_picked?
end
def can_push_to_source_branch?
@@ -195,12 +196,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end.sort.to_sentence
end
- def user_can_collaborate_with_project?
- can?(current_user, :push_code, project) ||
- (current_user && current_user.already_forked?(project)) ||
- can_push_to_source_branch?
- end
-
def user_can_fork_project?
can?(current_user, :fork_project, project)
end
diff --git a/app/serializers/build_metadata_entity.rb b/app/serializers/build_metadata_entity.rb
index 39f429aa6c3..f16f3badffa 100644
--- a/app/serializers/build_metadata_entity.rb
+++ b/app/serializers/build_metadata_entity.rb
@@ -1,8 +1,5 @@
class BuildMetadataEntity < Grape::Entity
- expose :timeout_human_readable do |metadata|
- metadata.timeout_human_readable unless metadata.timeout.nil?
- end
-
+ expose :timeout_human_readable
expose :timeout_source do |metadata|
metadata.present.timeout_source
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index b5e2334b6e3..840fdbcbf14 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -29,6 +29,10 @@ class IssueEntity < IssuableEntity
expose :can_update do |issue|
can?(request.current_user, :update_issue, issue)
end
+
+ expose :can_award_emoji do |issue|
+ can?(request.current_user, :award_emoji, issue)
+ end
end
expose :create_note_path do |issue|
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index c964aa9c99b..06d603b277e 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -15,7 +15,11 @@ class NoteEntity < API::Entities::Note
expose :current_user do
expose :can_edit do |note|
- Ability.can_edit_note?(request.current_user, note)
+ Ability.allowed?(request.current_user, :admin_note, note)
+ end
+
+ expose :can_award_emoji do |note|
+ Ability.allowed?(request.current_user, :award_emoji, note)
end
end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index a7c2e21e92b..8e8bda2f9df 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -2,7 +2,7 @@ class StatusEntity < Grape::Entity
include RequestAwareEntity
expose :icon, :text, :label, :group
-
+ expose :status_tooltip, as: :tooltip
expose :has_details?, as: :has_details
expose :details_path
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 2b77f6be72a..f28cddb2af3 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -109,7 +109,7 @@ module Auth
case requested_action
when 'pull'
- build_can_pull?(requested_project) || user_can_pull?(requested_project)
+ build_can_pull?(requested_project) || user_can_pull?(requested_project) || deploy_token_can_pull?(requested_project)
when 'push'
build_can_push?(requested_project) || user_can_push?(requested_project)
when '*'
@@ -123,22 +123,34 @@ module Auth
Gitlab.config.registry
end
+ def can_user?(ability, project)
+ user = current_user.is_a?(User) ? current_user : nil
+ can?(user, ability, project)
+ end
+
def build_can_pull?(requested_project)
# Build can:
# 1. pull from its own project (for ex. a build)
# 2. read images from dependent projects if creator of build is a team member
has_authentication_ability?(:build_read_container_image) &&
- (requested_project == project || can?(current_user, :build_read_container_image, requested_project))
+ (requested_project == project || can_user?(:build_read_container_image, requested_project))
end
def user_can_admin?(requested_project)
has_authentication_ability?(:admin_container_image) &&
- can?(current_user, :admin_container_image, requested_project)
+ can_user?(:admin_container_image, requested_project)
end
def user_can_pull?(requested_project)
has_authentication_ability?(:read_container_image) &&
- can?(current_user, :read_container_image, requested_project)
+ can_user?(:read_container_image, requested_project)
+ end
+
+ def deploy_token_can_pull?(requested_project)
+ has_authentication_ability?(:read_container_image) &&
+ current_user.is_a?(DeployToken) &&
+ current_user.has_access_to?(requested_project) &&
+ current_user.read_registry?
end
##
@@ -154,7 +166,7 @@ module Auth
def user_can_push?(requested_project)
has_authentication_ability?(:create_container_image) &&
- can?(current_user, :create_container_image, requested_project)
+ can_user?(:create_container_image, requested_project)
end
def error(code, status:, message: '')
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index ecd74b74f8a..ac70a99c2c5 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -35,6 +35,7 @@ module Boards
def filter_params
set_parent
set_state
+ set_scope
params
end
@@ -51,6 +52,10 @@ module Boards
params[:state] = list && list.closed? ? 'closed' : 'opened'
end
+ def set_scope
+ params[:include_subgroups] = board.group_board?
+ end
+
def board_label_ids
@board_label_ids ||= board.lists.movable.pluck(:label_id)
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 15fed7d17c1..3ceab209f3f 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -42,7 +42,10 @@ module Boards
)
end
- attrs[:move_between_ids] = move_between_ids if move_between_ids
+ if move_between_ids
+ attrs[:move_between_ids] = move_between_ids
+ attrs[:board_group_id] = board.group&.id
+ end
attrs
end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index bebc90c7a8d..02f1c709374 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -12,11 +12,15 @@ module Boards
private
def available_labels_for(board)
+ options = { include_ancestor_groups: true }
+
if board.group_board?
- parent.labels
+ options.merge!(group_id: parent.id, only_group_labels: true)
else
- LabelsFinder.new(current_user, project_id: parent.id).execute
+ options[:project_id] = parent.id
end
+
+ LabelsFinder.new(current_user, options).execute
end
def next_position(board)
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index 15ab2d54404..84944e95542 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -13,7 +13,7 @@ module Clusters
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
rescue ActiveRecord::RecordInvalid => e
- provider.make_errored!("Failed to configure GKE Cluster: #{e.message}")
+ provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}")
end
private
diff --git a/app/services/deploy_tokens/create_service.rb b/app/services/deploy_tokens/create_service.rb
new file mode 100644
index 00000000000..52f545947af
--- /dev/null
+++ b/app/services/deploy_tokens/create_service.rb
@@ -0,0 +1,7 @@
+module DeployTokens
+ class CreateService < BaseService
+ def execute
+ @project.deploy_tokens.create(params)
+ end
+ end
+end
diff --git a/app/services/events/render_service.rb b/app/services/events/render_service.rb
index 0b62d8aedf1..bb72d7685dd 100644
--- a/app/services/events/render_service.rb
+++ b/app/services/events/render_service.rb
@@ -1,15 +1,17 @@
module Events
class RenderService < BaseRenderer
def execute(events, atom_request: false)
- events.map(&:note).compact.group_by(&:project).each do |project, notes|
- render_notes(notes, project, atom_request)
- end
+ notes = events.map(&:note).compact
+
+ render_notes(notes, atom_request)
end
private
- def render_notes(notes, project, atom_request)
- Notes::RenderService.new(current_user).execute(notes, project, render_options(atom_request))
+ def render_notes(notes, atom_request)
+ Notes::RenderService
+ .new(current_user)
+ .execute(notes, render_options(atom_request))
end
def render_options(atom_request)
diff --git a/app/services/issuable/destroy_service.rb b/app/services/issuable/destroy_service.rb
index 7197a426a72..0b1a33518c6 100644
--- a/app/services/issuable/destroy_service.rb
+++ b/app/services/issuable/destroy_service.rb
@@ -4,6 +4,7 @@ module Issuable
TodoService.new.destroy_target(issuable) do |issuable|
if issuable.destroy
issuable.update_project_counter_caches
+ issuable.assignees.each(&:invalidate_cache_counts)
end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 02fb48108fb..1f67e3ecf9d 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -51,9 +51,10 @@ class IssuableBaseService < BaseService
return unless milestone_id
params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE
+ group_ids = project.group&.self_and_ancestors&.pluck(:id)
milestone =
- Milestone.for_projects_and_groups([project.id], [project.group&.id]).find_by_id(milestone_id)
+ Milestone.for_projects_and_groups([project.id], group_ids).find_by_id(milestone_id)
params[:milestone_id] = '' unless milestone
end
@@ -106,7 +107,7 @@ class IssuableBaseService < BaseService
end
def available_labels
- @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+ @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: true).execute
end
def handle_quick_actions_on_create(issuable)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index d7aa7e2347e..1374f10c586 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -55,9 +55,10 @@ module Issues
return unless params[:move_between_ids]
after_id, before_id = params.delete(:move_between_ids)
+ board_group_id = params.delete(:board_group_id)
- issue_before = get_issue_if_allowed(issue.project, before_id) if before_id
- issue_after = get_issue_if_allowed(issue.project, after_id) if after_id
+ issue_before = get_issue_if_allowed(before_id, board_group_id)
+ issue_after = get_issue_if_allowed(after_id, board_group_id)
issue.move_between(issue_before, issue_after)
end
@@ -84,8 +85,16 @@ module Issues
private
- def get_issue_if_allowed(project, id)
- issue = project.issues.find(id)
+ def get_issue_if_allowed(id, board_group_id = nil)
+ return unless id
+
+ issue =
+ if board_group_id
+ IssuesFinder.new(current_user, group_id: board_group_id, include_subgroups: true).find_by(id: id)
+ else
+ project.issues.find(id)
+ end
+
issue if can?(current_user, :update_issue, issue)
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index c57a2445341..fe1ac70781e 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -71,8 +71,8 @@ module MergeRequests
params.delete(:source_project_id)
params.delete(:target_project_id)
- unless can?(current_user, :read_project, @source_project) &&
- can?(current_user, :read_project, @project)
+ unless can?(current_user, :create_merge_request_from, @source_project) &&
+ can?(current_user, :create_merge_request_in, @project)
raise Gitlab::Access::AccessDeniedError
end
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index ad3dcc5010b..199b8028dbc 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -11,7 +11,7 @@ module Notes
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
- return unless @note.for_project_noteable?
+ return if @note.for_personal_snippet?
@note.create_cross_references!
execute_note_hooks
@@ -23,9 +23,13 @@ module Notes
end
def execute_note_hooks
+ return unless @note.project
+
note_data = hook_data
- @note.project.execute_hooks(note_data, :note_hooks)
- @note.project.execute_services(note_data, :note_hooks)
+ hooks_scope = @note.confidential? ? :confidential_note_hooks : :note_hooks
+
+ @note.project.execute_hooks(note_data, hooks_scope)
+ @note.project.execute_services(note_data, hooks_scope)
end
end
end
diff --git a/app/services/notes/render_service.rb b/app/services/notes/render_service.rb
index a77e98c2b07..efc9d6da2aa 100644
--- a/app/services/notes/render_service.rb
+++ b/app/services/notes/render_service.rb
@@ -3,19 +3,18 @@ module Notes
# Renders a collection of Note instances.
#
# notes - The notes to render.
- # project - The project to use for redacting.
- # user - The user viewing the notes.
-
+ #
# Possible options:
+ #
# requested_path - The request path.
# project_wiki - The project's wiki.
# ref - The current Git reference.
# only_path - flag to turn relative paths into absolute ones.
# xhtml - flag to save the html in XHTML
- def execute(notes, project, **opts)
- renderer = Banzai::ObjectRenderer.new(project, current_user, **opts)
-
- renderer.render(notes, :note)
+ def execute(notes, options = {})
+ Banzai::ObjectRenderer
+ .new(user: current_user, redaction_context: options)
+ .render(notes, :note)
end
end
end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index e4be953e810..b82d9c64296 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -54,8 +54,7 @@ module NotificationRecipientService
users = users.includes(:notification_settings)
end
- users = Array(users)
- users.compact!
+ users = Array(users).compact
recipients.concat(users.map { |u| make_recipient(u, type, reason) })
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index e61ecb696d0..346971138b1 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -21,7 +21,8 @@ module Projects
end
def labels(target = nil)
- labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title])
+ labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true)
+ .execute.select([:color, :title])
return labels unless target&.respond_to?(:labels)
diff --git a/app/services/projects/base_move_relations_service.rb b/app/services/projects/base_move_relations_service.rb
new file mode 100644
index 00000000000..e8fd3ef57e5
--- /dev/null
+++ b/app/services/projects/base_move_relations_service.rb
@@ -0,0 +1,22 @@
+module Projects
+ class BaseMoveRelationsService < BaseService
+ attr_reader :source_project
+ def execute(source_project, remove_remaining_elements: true)
+ return if source_project.blank?
+
+ @source_project = source_project
+
+ true
+ end
+
+ private
+
+ def prepare_relation(relation, id_param = :id)
+ if Gitlab::Database.postgresql?
+ relation
+ else
+ relation.model.where("#{id_param}": relation.pluck(id_param))
+ end
+ end
+ end
+end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 633e2c8236c..d361d070993 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -96,6 +96,8 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create)
setup_authorizations
+
+ current_user.invalidate_personal_projects_count
end
# Refresh the current user's authorizations inline (so they can access the
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 4b8f955ae69..aa14206db3b 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -34,6 +34,8 @@ module Projects
system_hook_service.execute_hooks_for(project, :destroy)
log_info("Project \"#{project.full_path}\" was removed")
+ current_user.invalidate_personal_projects_count
+
true
rescue => error
attempt_rollback(project, error.message)
@@ -44,6 +46,20 @@ module Projects
raise
end
+ def attempt_repositories_rollback
+ return unless @project
+
+ flush_caches(@project)
+
+ unless mv_repository(removal_path(repo_path), repo_path)
+ raise_error('Failed to restore project repository. Please contact the administrator.')
+ end
+
+ unless mv_repository(removal_path(wiki_path), wiki_path)
+ raise_error('Failed to restore wiki repository. Please contact the administrator.')
+ end
+ end
+
private
def repo_path
@@ -68,12 +84,9 @@ module Projects
# Skip repository removal. We use this flag when remove user or group
return true if params[:skip_repo] == true
- # There is a possibility project does not have repository or wiki
- return true unless gitlab_shell.exists?(project.repository_storage_path, path + '.git')
-
new_path = removal_path(path)
- if gitlab_shell.mv_repository(project.repository_storage_path, path, new_path)
+ if mv_repository(path, new_path)
log_info("Repository \"#{path}\" moved to \"#{new_path}\"")
project.run_after_commit do
@@ -85,6 +98,13 @@ module Projects
end
end
+ def mv_repository(from_path, to_path)
+ # There is a possibility project does not have repository or wiki
+ return true unless gitlab_shell.exists?(project.repository_storage_path, from_path + '.git')
+
+ gitlab_shell.mv_repository(project.repository_storage_path, from_path, to_path)
+ end
+
def attempt_rollback(project, message)
return unless project
diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb
index a68ecb4abe1..a16268f4fd2 100644
--- a/app/services/projects/gitlab_projects_import_service.rb
+++ b/app/services/projects/gitlab_projects_import_service.rb
@@ -5,8 +5,8 @@ module Projects
class GitlabProjectsImportService
attr_reader :current_user, :params
- def initialize(user, params)
- @current_user, @params = user, params.dup
+ def initialize(user, import_params, override_params = nil)
+ @current_user, @params, @override_params = user, import_params.dup, override_params
end
def execute
@@ -15,8 +15,18 @@ module Projects
file = params.delete(:file)
FileUtils.copy_entry(file.path, import_upload_path)
+ @overwrite = params.delete(:overwrite)
+ data = {}
+ data[:override_params] = @override_params if @override_params
+
+ if overwrite_project?
+ data[:original_path] = params[:path]
+ params[:path] += "-#{tmp_filename}"
+ end
+
params[:import_type] = 'gitlab_project'
params[:import_source] = import_upload_path
+ params[:import_data] = { data: data } if data.present?
::Projects::CreateService.new(current_user, params).execute
end
@@ -30,5 +40,17 @@ module Projects
def tmp_filename
SecureRandom.hex
end
+
+ def overwrite_project?
+ @overwrite && project_with_same_full_path?
+ end
+
+ def project_with_same_full_path?
+ Project.find_by_full_path("#{current_namespace.full_path}/#{params[:path]}").present?
+ end
+
+ def current_namespace
+ @current_namespace ||= Namespace.find_by(id: params[:namespace_id])
+ end
end
end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 402cddd3ec1..7bf0b90b491 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -28,7 +28,7 @@ module Projects
end
def save_services
- [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+ [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver].all?(&:save)
end
def version_saver
@@ -55,6 +55,10 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end
+ def lfs_saver
+ Gitlab::ImportExport::LfsSaver.new(project: project, shared: @shared)
+ end
+
def cleanup_and_notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
diff --git a/app/services/projects/move_access_service.rb b/app/services/projects/move_access_service.rb
new file mode 100644
index 00000000000..3af3a22d486
--- /dev/null
+++ b/app/services/projects/move_access_service.rb
@@ -0,0 +1,25 @@
+module Projects
+ class MoveAccessService < BaseMoveRelationsService
+ def execute(source_project, remove_remaining_elements: true)
+ return unless super
+
+ @project.with_transaction_returning_status do
+ if @project.namespace != source_project.namespace
+ @project.run_after_commit do
+ source_project.namespace.refresh_project_authorizations
+ self.namespace.refresh_project_authorizations
+ end
+ end
+
+ ::Projects::MoveProjectMembersService.new(@project, @current_user)
+ .execute(source_project, remove_remaining_elements: remove_remaining_elements)
+ ::Projects::MoveProjectGroupLinksService.new(@project, @current_user)
+ .execute(source_project, remove_remaining_elements: remove_remaining_elements)
+ ::Projects::MoveProjectAuthorizationsService.new(@project, @current_user)
+ .execute(source_project, remove_remaining_elements: remove_remaining_elements)
+
+ success
+ end
+ 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
new file mode 100644
index 00000000000..dde420655b0
--- /dev/null
+++ b/app/services/projects/move_deploy_keys_projects_service.rb
@@ -0,0 +1,31 @@
+module Projects
+ class MoveDeployKeysProjectsService < BaseMoveRelationsService
+ def execute(source_project, remove_remaining_elements: true)
+ return unless super
+
+ Project.transaction(requires_new: true) do
+ move_deploy_keys_projects
+ remove_remaining_deploy_keys_projects if remove_remaining_elements
+
+ success
+ end
+ end
+
+ private
+
+ def move_deploy_keys_projects
+ prepare_relation(non_existent_deploy_keys_projects)
+ .update_all(project_id: @project.id)
+ end
+
+ def non_existent_deploy_keys_projects
+ source_project.deploy_keys_projects
+ .joins(:deploy_key)
+ .where.not(keys: { fingerprint: @project.deploy_keys.select(:fingerprint) })
+ end
+
+ def remove_remaining_deploy_keys_projects
+ source_project.deploy_keys_projects.destroy_all
+ end
+ end
+end
diff --git a/app/services/projects/move_forks_service.rb b/app/services/projects/move_forks_service.rb
new file mode 100644
index 00000000000..d2901ea1457
--- /dev/null
+++ b/app/services/projects/move_forks_service.rb
@@ -0,0 +1,42 @@
+module Projects
+ class MoveForksService < BaseMoveRelationsService
+ def execute(source_project, remove_remaining_elements: true)
+ return unless super && source_project.fork_network
+
+ Project.transaction(requires_new: true) do
+ move_forked_project_links
+ move_fork_network_members
+ update_root_project
+ refresh_forks_count
+
+ success
+ end
+ end
+
+ private
+
+ def move_forked_project_links
+ # Update ancestor
+ ForkedProjectLink.where(forked_to_project: source_project)
+ .update_all(forked_to_project_id: @project.id)
+
+ # Update the descendants
+ ForkedProjectLink.where(forked_from_project: source_project)
+ .update_all(forked_from_project_id: @project.id)
+ end
+
+ 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
+
+ def update_root_project
+ # Update root network project
+ ForkNetwork.where(root_project: source_project).update_all(root_project_id: @project.id)
+ end
+
+ def refresh_forks_count
+ Projects::ForksCountService.new(@project).refresh_cache
+ end
+ end
+end
diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb
new file mode 100644
index 00000000000..298da5f1a82
--- /dev/null
+++ b/app/services/projects/move_lfs_objects_projects_service.rb
@@ -0,0 +1,29 @@
+module Projects
+ class MoveLfsObjectsProjectsService < BaseMoveRelationsService
+ def execute(source_project, remove_remaining_elements: true)
+ return unless super
+
+ Project.transaction(requires_new: true) do
+ move_lfs_objects_projects
+ remove_remaining_lfs_objects_project if remove_remaining_elements
+
+ success
+ end
+ end
+
+ private
+
+ def move_lfs_objects_projects
+ prepare_relation(non_existent_lfs_objects_projects)
+ .update_all(project_id: @project.lfs_storage_project.id)
+ end
+
+ def remove_remaining_lfs_objects_project
+ source_project.lfs_objects_projects.destroy_all
+ end
+
+ def non_existent_lfs_objects_projects
+ source_project.lfs_objects_projects.where.not(lfs_object: @project.lfs_objects)
+ end
+ end
+end
diff --git a/app/services/projects/move_notification_settings_service.rb b/app/services/projects/move_notification_settings_service.rb
new file mode 100644
index 00000000000..f7be461a5da
--- /dev/null
+++ b/app/services/projects/move_notification_settings_service.rb
@@ -0,0 +1,38 @@
+module Projects
+ class MoveNotificationSettingsService < BaseMoveRelationsService
+ def execute(source_project, remove_remaining_elements: true)
+ return unless super
+
+ Project.transaction(requires_new: true) do
+ move_notification_settings
+ remove_remaining_notification_settings if remove_remaining_elements
+
+ success
+ end
+ end
+
+ private
+
+ def move_notification_settings
+ prepare_relation(non_existent_notifications)
+ .update_all(source_id: @project.id)
+ end
+
+ # Remove remaining notification settings from source_project
+ def remove_remaining_notification_settings
+ source_project.notification_settings.destroy_all
+ end
+
+ # Get users of current notification_settings
+ def users_in_target_project
+ @project.notification_settings.select(:user_id)
+ end
+
+ # Look for notification_settings in source_project that are not in the target project
+ def non_existent_notifications
+ source_project.notification_settings
+ .select(:id)
+ .where.not(user_id: users_in_target_project)
+ end
+ end
+end
diff --git a/app/services/projects/move_project_authorizations_service.rb b/app/services/projects/move_project_authorizations_service.rb
new file mode 100644
index 00000000000..5ef12fc49e5
--- /dev/null
+++ b/app/services/projects/move_project_authorizations_service.rb
@@ -0,0 +1,40 @@
+# NOTE: This service cannot be used directly because it is part of a
+# a bigger process. Instead, use the service MoveAccessService which moves
+# project memberships, project group links, authorizations and refreshes
+# the authorizations if neccessary
+module Projects
+ class MoveProjectAuthorizationsService < BaseMoveRelationsService
+ def execute(source_project, remove_remaining_elements: true)
+ return unless super
+
+ Project.transaction(requires_new: true) do
+ move_project_authorizations
+
+ remove_remaining_authorizations if remove_remaining_elements
+
+ success
+ end
+ end
+
+ private
+
+ def move_project_authorizations
+ prepare_relation(non_existent_authorization, :user_id)
+ .update_all(project_id: @project.id)
+ end
+
+ def remove_remaining_authorizations
+ # I think because the Project Authorization table does not have a primary key
+ # it brings a lot a problems/bugs. First, Rails raises PG::SyntaxException if we use
+ # destroy_all instead of delete_all.
+ source_project.project_authorizations.delete_all(:delete_all)
+ end
+
+ # Look for authorizations in source_project that are not in the target project
+ def non_existent_authorization
+ source_project.project_authorizations
+ .select(:user_id)
+ .where.not(user: @project.authorized_users)
+ end
+ end
+end
diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb
new file mode 100644
index 00000000000..dbeffd7dae9
--- /dev/null
+++ b/app/services/projects/move_project_group_links_service.rb
@@ -0,0 +1,40 @@
+# NOTE: This service cannot be used directly because it is part of a
+# a bigger process. Instead, use the service MoveAccessService which moves
+# project memberships, project group links, authorizations and refreshes
+# the authorizations if neccessary
+module Projects
+ class MoveProjectGroupLinksService < BaseMoveRelationsService
+ def execute(source_project, remove_remaining_elements: true)
+ return unless super
+
+ Project.transaction(requires_new: true) do
+ move_group_links
+ remove_remaining_project_group_links if remove_remaining_elements
+
+ success
+ end
+ end
+
+ private
+
+ def move_group_links
+ prepare_relation(non_existent_group_links)
+ .update_all(project_id: @project.id)
+ end
+
+ # Remove remaining project group links from source_project
+ def remove_remaining_project_group_links
+ source_project.reload.project_group_links.destroy_all
+ end
+
+ def group_links_in_target_project
+ @project.project_group_links.select(:group_id)
+ end
+
+ # Look for groups in source_project that are not in the target project
+ def non_existent_group_links
+ source_project.project_group_links
+ .where.not(group_id: group_links_in_target_project)
+ end
+ end
+end
diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb
new file mode 100644
index 00000000000..22a5f0a3fe6
--- /dev/null
+++ b/app/services/projects/move_project_members_service.rb
@@ -0,0 +1,40 @@
+# NOTE: This service cannot be used directly because it is part of a
+# a bigger process. Instead, use the service MoveAccessService which moves
+# project memberships, project group links, authorizations and refreshes
+# the authorizations if neccessary
+module Projects
+ class MoveProjectMembersService < BaseMoveRelationsService
+ def execute(source_project, remove_remaining_elements: true)
+ return unless super
+
+ Project.transaction(requires_new: true) do
+ move_project_members
+ remove_remaining_members if remove_remaining_elements
+
+ success
+ end
+ end
+
+ private
+
+ def move_project_members
+ prepare_relation(non_existent_members).update_all(source_id: @project.id)
+ end
+
+ def remove_remaining_members
+ # Remove remaining members and authorizations from source_project
+ source_project.project_members.destroy_all
+ end
+
+ def project_members_in_target_project
+ @project.project_members.select(:user_id)
+ end
+
+ # Look for members in source_project that are not in the target project
+ def non_existent_members
+ source_project.members
+ .select(:id)
+ .where.not(user_id: @project.project_members.select(:user_id))
+ end
+ end
+end
diff --git a/app/services/projects/move_users_star_projects_service.rb b/app/services/projects/move_users_star_projects_service.rb
new file mode 100644
index 00000000000..079fd5b9685
--- /dev/null
+++ b/app/services/projects/move_users_star_projects_service.rb
@@ -0,0 +1,20 @@
+module Projects
+ class MoveUsersStarProjectsService < BaseMoveRelationsService
+ def execute(source_project, remove_remaining_elements: true)
+ return unless super
+
+ user_stars = source_project.users_star_projects
+
+ return unless user_stars.any?
+
+ Project.transaction(requires_new: true) do
+ user_stars.update_all(project_id: @project.id)
+
+ Project.reset_counters @project.id, :users_star_projects
+ Project.reset_counters source_project.id, :users_star_projects
+
+ success
+ end
+ end
+ end
+end
diff --git a/app/services/projects/overwrite_project_service.rb b/app/services/projects/overwrite_project_service.rb
new file mode 100644
index 00000000000..ce94f147aa9
--- /dev/null
+++ b/app/services/projects/overwrite_project_service.rb
@@ -0,0 +1,69 @@
+module Projects
+ class OverwriteProjectService < BaseService
+ def execute(source_project)
+ return unless source_project && source_project.namespace == @project.namespace
+
+ Project.transaction do
+ move_before_destroy_relationships(source_project)
+ destroy_old_project(source_project)
+ rename_project(source_project.name, source_project.path)
+
+ @project
+ end
+ # Projects::DestroyService can raise Exceptions, but we don't want
+ # to pass that kind of exception to the caller. Instead, we change it
+ # for a StandardError exception
+ rescue Exception => e # rubocop:disable Lint/RescueException
+ attempt_restore_repositories(source_project)
+
+ if e.class == Exception
+ raise StandardError, e.message
+ else
+ raise
+ end
+ end
+
+ private
+
+ def move_before_destroy_relationships(source_project)
+ options = { remove_remaining_elements: false }
+
+ ::Projects::MoveUsersStarProjectsService.new(@project, @current_user).execute(source_project, options)
+ ::Projects::MoveAccessService.new(@project, @current_user).execute(source_project, options)
+ ::Projects::MoveDeployKeysProjectsService.new(@project, @current_user).execute(source_project, options)
+ ::Projects::MoveNotificationSettingsService.new(@project, @current_user).execute(source_project, options)
+ ::Projects::MoveForksService.new(@project, @current_user).execute(source_project, options)
+ ::Projects::MoveLfsObjectsProjectsService.new(@project, @current_user).execute(source_project, options)
+ add_source_project_to_fork_network(source_project)
+ end
+
+ def destroy_old_project(source_project)
+ # Delete previous project (synchronously) and unlink relations
+ ::Projects::DestroyService.new(source_project, @current_user).execute
+ end
+
+ def rename_project(name, path)
+ # Update de project's name and path to the original name/path
+ ::Projects::UpdateService.new(@project,
+ @current_user,
+ { name: name, path: path })
+ .execute
+ end
+
+ def attempt_restore_repositories(project)
+ ::Projects::DestroyService.new(project, @current_user).attempt_repositories_rollback
+ end
+
+ def add_source_project_to_fork_network(source_project)
+ return unless @project.fork_network
+
+ # Because he have moved all references in the fork network from the source_project
+ # we won't be able to query the database (only through its cached data),
+ # for its former relationships. That's why we're adding it to the network
+ # as a fork of the target project
+ ForkNetworkMember.create!(fork_network: @project.fork_network,
+ project: source_project,
+ forked_from_project: @project)
+ end
+ end
+end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 26765e5c3f3..5a23f0f0a62 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -24,6 +24,8 @@ module Projects
transfer(project)
+ current_user.invalidate_personal_projects_count
+
true
rescue Projects::TransferService::TransferError => ex
project.reload
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 7e228d1833d..de77f6bf585 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -74,25 +74,13 @@ module Projects
end
def extract_archive!(temp_path)
- if artifacts.ends_with?('.tar.gz') || artifacts.ends_with?('.tgz')
- extract_tar_archive!(temp_path)
- elsif artifacts.ends_with?('.zip')
+ if artifacts.ends_with?('.zip')
extract_zip_archive!(temp_path)
else
raise InvaildStateError, 'unsupported artifacts format'
end
end
- def extract_tar_archive!(temp_path)
- build.artifacts_file.use_file do |artifacts_path|
- results = Open3.pipeline(%W(gunzip -c #{artifacts_path}),
- %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
- %W(tar -x -C #{temp_path} #{SITE_PATH}),
- err: '/dev/null')
- raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?)
- end
- end
-
def extract_zip_archive!(temp_path)
raise InvaildStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index cba49faac31..6cc51b6ee1b 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -200,7 +200,7 @@ module QuickActions
end
params '~label1 ~"label 2"'
condition do
- available_labels = LabelsFinder.new(current_user, project_id: project.id).execute
+ available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any?
@@ -562,7 +562,7 @@ module QuickActions
def find_labels(labels_param)
extract_references(labels_param, :label) |
- LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute
+ LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split, include_ancestor_groups: true).execute
end
def find_label_references(labels_param)
@@ -593,6 +593,7 @@ module QuickActions
def extract_references(arg, type)
ext = Gitlab::ReferenceExtractor.new(project, current_user)
+
ext.analyze(arg, author: current_user)
ext.references(type)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 2253d638e93..00bf5434b7f 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -429,7 +429,7 @@ module SystemNoteService
def cross_reference(noteable, mentioner, author)
return if cross_reference_disallowed?(noteable, mentioner)
- gfm_reference = mentioner.gfm_reference(noteable.project)
+ gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
body = cross_reference_note_content(gfm_reference)
if noteable.is_a?(ExternalIssue)
@@ -582,7 +582,7 @@ module SystemNoteService
text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize)
else
- gfm_reference = mentioner.gfm_reference(noteable.project)
+ gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
text = cross_reference_note_content(gfm_reference)
notes.where(note: [text, text.capitalize])
end
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index ef0f8acefd6..dd86753479d 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -2,6 +2,8 @@ class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
+ ObjectNotReadyError = Class.new(StandardError)
+
storage_options Gitlab.config.artifacts
def size
@@ -25,6 +27,8 @@ class JobArtifactUploader < GitlabUploader
private
def dynamic_segment
+ raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id
+
creation_date = model.created_at.utc.strftime('%Y_%m_%d')
File.join(disk_hash[0..1], disk_hash[2..3], disk_hash,
diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb
index b726b053493..efb7893d153 100644
--- a/app/uploaders/legacy_artifact_uploader.rb
+++ b/app/uploaders/legacy_artifact_uploader.rb
@@ -2,6 +2,8 @@ class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
+ ObjectNotReadyError = Class.new(StandardError)
+
storage_options Gitlab.config.artifacts
def store_dir
@@ -11,6 +13,8 @@ class LegacyArtifactUploader < GitlabUploader
private
def dynamic_segment
+ raise ObjectNotReadyError, 'Build is not ready' unless model.id
+
File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.id.to_s)
end
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index 4028b052768..bd258e04d3f 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -128,7 +128,7 @@ module ObjectStorage
end
def direct_upload_enabled?
- object_store_options.direct_upload
+ object_store_options&.direct_upload
end
def background_upload_enabled?
@@ -156,11 +156,10 @@ module ObjectStorage
end
def workhorse_authorize
- if options = workhorse_remote_upload_options
- { RemoteObject: options }
- else
- { TempPath: workhorse_local_upload_path }
- end
+ {
+ RemoteObject: workhorse_remote_upload_options,
+ TempPath: workhorse_local_upload_path
+ }.compact
end
def workhorse_local_upload_path
@@ -184,6 +183,14 @@ module ObjectStorage
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
}
end
+
+ def default_object_store
+ if self.object_store_enabled? && self.direct_upload_enabled?
+ Store::REMOTE
+ else
+ Store::LOCAL
+ end
+ end
end
# allow to configure and overwrite the filename
@@ -204,12 +211,12 @@ module ObjectStorage
end
def object_store
- @object_store ||= model.try(store_serialization_column) || Store::LOCAL
+ @object_store ||= model.try(store_serialization_column) || self.class.default_object_store
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def object_store=(value)
- @object_store = value || Store::LOCAL
+ @object_store = value || self.class.default_object_store
@storage = storage_for(object_store)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -285,16 +292,14 @@ module ObjectStorage
}
end
- def store_workhorse_file!(params, identifier)
- filename = params["#{identifier}.name"]
-
- if remote_object_id = params["#{identifier}.remote_id"]
- store_remote_file!(remote_object_id, filename)
- elsif local_path = params["#{identifier}.path"]
- store_local_file!(local_path, filename)
- else
- raise RemoteStoreError, 'Bad file'
+ def cache!(new_file = sanitized_file)
+ # We intercept ::UploadedFile which might be stored on remote storage
+ # We use that for "accelerated" uploads, where we store result on remote storage
+ if new_file.is_a?(::UploadedFile) && new_file.remote_id
+ return cache_remote_file!(new_file.remote_id, new_file.original_filename)
end
+
+ super
end
private
@@ -305,36 +310,29 @@ module ObjectStorage
self.file_storage?
end
- def store_remote_file!(remote_object_id, filename)
- raise RemoteStoreError, 'Missing filename' unless filename
-
+ def cache_remote_file!(remote_object_id, original_filename)
file_path = File.join(TMP_UPLOAD_PATH, remote_object_id)
file_path = Pathname.new(file_path).cleanpath.to_s
raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(TMP_UPLOAD_PATH + '/')
- self.object_store = Store::REMOTE
-
# TODO:
# This should be changed to make use of `tmp/cache` mechanism
# instead of using custom upload directory,
# using tmp/cache makes this implementation way easier than it is today
- CarrierWave::Storage::Fog::File.new(self, storage, file_path).tap do |file|
+ CarrierWave::Storage::Fog::File.new(self, storage_for(Store::REMOTE), file_path).tap do |file|
raise RemoteStoreError, 'Missing file' unless file.exists?
- self.filename = filename
- self.file = storage.store!(file)
- end
- end
-
- def store_local_file!(local_path, filename)
- raise RemoteStoreError, 'Missing filename' unless filename
+ # Remote stored file, we force to store on remote storage
+ self.object_store = Store::REMOTE
- root_path = File.realpath(self.class.workhorse_local_upload_path)
- file_path = File.realpath(local_path)
- raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(root_path)
-
- self.object_store = Store::LOCAL
- self.store!(UploadedFile.new(file_path, filename))
+ # TODO:
+ # We store file internally and force it to be considered as `cached`
+ # This makes CarrierWave to store file in permament location (copy/delete)
+ # once this object is saved, but not sooner
+ @cache_id = "force-to-use-cache" # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @file = file # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @filename = original_filename # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
end
# this is a hack around CarrierWave. The #migrate method needs to be
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
new file mode 100644
index 00000000000..6c89f1c4e98
--- /dev/null
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -0,0 +1,26 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :email_author_in_body do
+ = f.check_box :email_author_in_body
+ Include author name in notification email body
+ .help-block
+ Some email servers do not support overriding the email sender name.
+ Enable this option to include the name of the author of the issue,
+ merge request or comment in the email body instead.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :html_emails_enabled do
+ = f.check_box :html_emails_enabled
+ Enable HTML emails
+ .help-block
+ By default GitLab sends emails in HTML and plain text formats so mail
+ clients can choose what format to use. Disable this option if you only
+ want to send emails in plain text format.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
deleted file mode 100644
index 9ab2c2892b2..00000000000
--- a/app/views/admin/application_settings/_form.html.haml
+++ /dev/null
@@ -1,173 +0,0 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
- = form_errors(@application_setting)
-
- - if Gitlab.config.registry.enabled
- %fieldset
- %legend Container Registry
- .form-group
- = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :container_registry_token_expire_delay, class: 'form-control'
-
- - if koding_enabled?
- %fieldset
- %legend Koding
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :koding_enabled do
- = f.check_box :koding_enabled
- Enable Koding
- .help-block
- Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
- .form-group
- = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
- .help-block
- Koding has integration enabled out of the box for the
- %strong gitlab
- team, and you need to provide that team's URL here. Learn more in the
- = succeed "." do
- = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
-
- %fieldset
- %legend PlantUML
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :plantuml_enabled do
- = f.check_box :plantuml_enabled
- Enable PlantUML
- .form-group
- = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
- .help-block
- Allow rendering of
- = link_to "PlantUML", "http://plantuml.com"
- diagrams in Asciidoc documents using an external PlantUML service.
-
- %fieldset
- %legend#usage-statistics Usage statistics
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :version_check_enabled do
- = f.check_box :version_check_enabled
- Enable version check
- .help-block
- GitLab will inform you if a new version is available.
- = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
- about what information is shared with GitLab Inc.
- .form-group
- .col-sm-offset-2.col-sm-10
- - can_be_configured = @application_setting.usage_ping_can_be_configured?
- .checkbox
- = f.label :usage_ping_enabled do
- = f.check_box :usage_ping_enabled, disabled: !can_be_configured
- Enable usage ping
- .help-block
- - 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', admin_cohorts_path(anchor: 'usage-ping')
- to see the JSON payload sent.
- - 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')
-
- %fieldset
- %legend Email
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :email_author_in_body do
- = f.check_box :email_author_in_body
- Include author name in notification email body
- .help-block
- Some email servers do not support overriding the email sender name.
- Enable this option to include the name of the author of the issue,
- merge request or comment in the email body instead.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :html_emails_enabled do
- = f.check_box :html_emails_enabled
- Enable HTML emails
- .help-block
- By default GitLab sends emails in HTML and plain text formats so mail
- clients can choose what format to use. Disable this option if you only
- want to send emails in plain text format.
-
- %fieldset
- %legend Gitaly Timeouts
- .form-group
- = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :gitaly_timeout_default, class: 'form-control'
- .help-block
- Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
- for git fetch/push operations or Sidekiq jobs.
- .form-group
- = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :gitaly_timeout_fast, class: 'form-control'
- .help-block
- Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
- If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
- can help maintain the stability of the GitLab instance.
- .form-group
- = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :gitaly_timeout_medium, class: 'form-control'
- .help-block
- Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
-
- %fieldset
- %legend Web terminal
- .form-group
- = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :terminal_max_session_time, class: 'form-control'
- .help-block
- Maximum time for web terminal websocket connection (in seconds).
- 0 for unlimited.
-
- %fieldset
- %legend Real-time features
- .form-group
- = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :polling_interval_multiplier, class: 'form-control'
- .help-block
- Change this value to influence how frequently the GitLab UI polls for updates.
- If you set the value to 2 all polling intervals are multiplied
- by 2, which means that polling happens half as frequently.
- The multiplier can also have a decimal value.
- The default value (1) is a reasonable choice for the majority of GitLab
- installations. Set to 0 to completely disable polling.
- = link_to icon('question-circle'), help_page_path('administration/polling')
-
- %fieldset
- %legend Performance optimization
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :authorized_keys_enabled do
- = f.check_box :authorized_keys_enabled
- Write to "authorized_keys" file
- .help-block
- By default, we write to the "authorized_keys" file to support Git
- over SSH without additional configuration. GitLab can be optimized
- to authenticate SSH keys via the database file. Only uncheck this
- if you have configured your OpenSSH server to use the
- AuthorizedKeysCommand. Click on the help icon for more details.
- = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
-
- .form-actions
- = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml
new file mode 100644
index 00000000000..4acc5b3a0c5
--- /dev/null
+++ b/app/views/admin/application_settings/_gitaly.html.haml
@@ -0,0 +1,27 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_default, class: 'form-control'
+ .help-block
+ Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
+ for git fetch/push operations or Sidekiq jobs.
+ .form-group
+ = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_fast, class: 'form-control'
+ .help-block
+ Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
+ If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
+ can help maintain the stability of the GitLab instance.
+ .form-group
+ = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_medium, class: 'form-control'
+ .help-block
+ Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml
new file mode 100644
index 00000000000..17358cf775b
--- /dev/null
+++ b/app/views/admin/application_settings/_koding.html.haml
@@ -0,0 +1,24 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :koding_enabled do
+ = f.check_box :koding_enabled
+ Enable Koding
+ .help-block
+ Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
+ .form-group
+ = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+ .help-block
+ Koding has integration enabled out of the box for the
+ %strong gitlab
+ team, and you need to provide that team's URL here. Learn more in the
+ = succeed "." do
+ = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
new file mode 100644
index 00000000000..01d5a31aa9f
--- /dev/null
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -0,0 +1,19 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :authorized_keys_enabled do
+ = f.check_box :authorized_keys_enabled
+ Write to "authorized_keys" file
+ .help-block
+ By default, we write to the "authorized_keys" file to support Git
+ over SSH without additional configuration. GitLab can be optimized
+ to authenticate SSH keys via the database file. Only uncheck this
+ if you have configured your OpenSSH server to use the
+ AuthorizedKeysCommand. Click on the help icon for more details.
+ = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
new file mode 100644
index 00000000000..56764b3fb81
--- /dev/null
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -0,0 +1,20 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :plantuml_enabled do
+ = f.check_box :plantuml_enabled
+ Enable PlantUML
+ .form-group
+ = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
+ .help-block
+ Allow rendering of
+ = link_to "PlantUML", "http://plantuml.com"
+ diagrams in Asciidoc documents using an external PlantUML service.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml
new file mode 100644
index 00000000000..0a53a75119e
--- /dev/null
+++ b/app/views/admin/application_settings/_realtime.html.haml
@@ -0,0 +1,19 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :polling_interval_multiplier, class: 'form-control'
+ .help-block
+ Change this value to influence how frequently the GitLab UI polls for updates.
+ If you set the value to 2 all polling intervals are multiplied
+ by 2, which means that polling happens half as frequently.
+ The multiplier can also have a decimal value.
+ The default value (1) is a reasonable choice for the majority of GitLab
+ installations. Set to 0 to completely disable polling.
+ = link_to icon('question-circle'), help_page_path('administration/polling')
+
+ = f.submit 'Save changes', class: "btn btn-success"
+
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
new file mode 100644
index 00000000000..3451ef62458
--- /dev/null
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -0,0 +1,10 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :container_registry_token_expire_delay, class: 'form-control'
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 864e64b5fa9..48331c40bca 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -24,6 +24,7 @@
- if omniauth_enabled? && button_based_providers.any?
.form-group
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
+ = hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
.col-sm-10
.btn-group{ data: { toggle: 'buttons' } }
- oauth_providers_checkboxes.each do |source|
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
new file mode 100644
index 00000000000..36d8838803f
--- /dev/null
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -0,0 +1,13 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :terminal_max_session_time, class: 'form-control'
+ .help-block
+ Maximum time for web terminal websocket connection (in seconds).
+ 0 for unlimited.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
new file mode 100644
index 00000000000..7684e2cfdd1
--- /dev/null
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -0,0 +1,37 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :version_check_enabled do
+ = f.check_box :version_check_enabled
+ Enable version check
+ .help-block
+ GitLab will inform you if a new version is available.
+ = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
+ about what information is shared with GitLab Inc.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ - can_be_configured = @application_setting.usage_ping_can_be_configured?
+ .checkbox
+ = f.label :usage_ping_enabled do
+ = f.check_box :usage_ping_enabled, disabled: !can_be_configured
+ Enable usage ping
+ .help-block
+ - 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', admin_cohorts_path(anchor: 'usage-ping')
+ to see the JSON payload sent.
+ - 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')
+
+ = f.submit 'Save changes', class: "btn btn-success"
+
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index cbc779548f6..a75dd90fe6b 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -32,6 +32,7 @@
.form-group
= f.label :import_sources, class: 'control-label col-sm-2'
.col-sm-10
+ = hidden_field_tag 'application_setting[import_sources][]'
- import_sources_checkboxes('import-sources-help').each do |source|
.checkbox= source
%span.help-block#import-sources-help
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index f4320513aff..caaa93aa1e2 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -76,7 +76,7 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
- = _('Auto DevOps, runners amd job artifacts')
+ = _('Auto DevOps, runners and job artifacts')
.settings-content
= render 'ci_cd'
@@ -102,7 +102,7 @@
.settings-content
= render 'prometheus'
-%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
+%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Profiling - Performance bar')
@@ -180,6 +180,107 @@
.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.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) }
+ .settings-header
+ %h4
+ = _('Koding')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? '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.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.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.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.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure Gitaly timeouts.')
+ .settings-content
+ = render 'gitaly'
+
+%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Web terminal')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? '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.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.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
@@ -201,6 +302,3 @@
= _('Allow requests to the local network from hooks and services.')
.settings-content
= render 'outbound'
-
-.prepend-top-20
- = render 'form'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 05c41082882..bbf0e0fb95c 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -126,6 +126,7 @@
GitLab
%span.pull-right
= Gitlab::VERSION
+ = "(#{Gitlab::REVISION})"
%p
GitLab Shell
%span.pull-right
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index c47b8a88f56..aeba9788fda 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -101,7 +101,7 @@
- if @project.archived?
%li
%span.light archived:
- %strong repository is read-only
+ %strong project is read-only
%li
%span.light access:
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index bbfeceff5b9..2ff4221efbd 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -33,7 +33,7 @@
= link_to 'Block', block_admin_user_path(user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put
- if user.access_locked?
%li
- = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
+ = link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') }
- if can?(current_user, :destroy_user, user)
%li.divider
- if user.can_be_removed?
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 5f07d2720c2..4b3c52af16a 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -3,13 +3,13 @@
.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",
- class: [(award_state_class(awards, current_user)), (award_user_authored_class(emoji) if user_authored)],
+ class: [(award_state_class(awardable, awards, current_user)), (award_user_authored_class(emoji) if user_authored)],
data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji)
%span.award-control-text.js-counter
= awards.count
- - if current_user
+ - if can?(current_user, :award_emoji, awardable)
.award-menu-holder.js-award-holder
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': 'Add reaction',
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
index 35a3563dff1..5114387984b 100644
--- a/app/views/ci/status/_badge.html.haml
+++ b/app/views/ci/status/_badge.html.haml
@@ -4,10 +4,10 @@
- css_classes = "ci-status ci-#{status.group} #{'has-tooltip' if title.present?}"
- if link && status.has_details?
- = link_to status.details_path, class: css_classes, title: title do
+ = link_to status.details_path, class: css_classes, title: title, data: { html: title.present? } do
= sprite_icon(status.icon)
= status.text
- else
- %span{ class: css_classes, title: title }
+ %span{ class: css_classes, title: title, data: { html: title.present? } }
= sprite_icon(status.icon)
= status.text
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index c5b4439e273..db2040110fa 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -3,14 +3,15 @@
- subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user)
- klass = "ci-status-icon ci-status-icon-#{status.group}"
-- tooltip = "#{subject.name} - #{status.label}"
+- tooltip = "#{subject.name} - #{status.status_tooltip}"
- if status.has_details?
- = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
+ = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, html: true, container: 'body' } do
%span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
+
- else
- .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
+ .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', html: true, title: tooltip, container: 'body' } }
%span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 3e85535dae0..bb472b4c900 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -1,15 +1,19 @@
- @hide_top_links = true
-- page_title "Issues"
-- header_title "Issues", issues_dashboard_path(assignee_id: current_user.id)
+- page_title _("Issues")
+- @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id)
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
.top-area
- = render 'shared/issuable/nav', type: :issues
+ = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
.nav-controls
= link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
= icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
= render 'shared/issuable/filter', type: :issues
-= render 'shared/issues'
+
+- if current_user && @no_filters_set
+ = render 'shared/dashboard/no_filter_selected'
+- else
+ = render 'shared/issues'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index 53cd1130299..61aae31be60 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -1,11 +1,15 @@
- @hide_top_links = true
-- page_title "Merge Requests"
-- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id)
+- page_title _("Merge Requests")
+- @breadcrumb_link = merge_requests_dashboard_path(assignee_id: current_user.id)
.top-area
- = render 'shared/issuable/nav', type: :merge_requests
+ = render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set
.nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests
= render 'shared/issuable/filter', type: :merge_requests
-= render 'shared/merge_requests'
+
+- if current_user && @no_filters_set
+ = render 'shared/dashboard/no_filter_selected'
+- else
+ = render 'shared/merge_requests'
diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml
index f943d25e41a..7bd414d64c3 100644
--- a/app/views/devise/shared/_tab_single.html.haml
+++ b/app/views/devise/shared/_tab_single.html.haml
@@ -1,3 +1,3 @@
-%ul.nav-links.nav-tabs.new-session-tabs.single-tab
+%ul.nav-links.new-session-tabs.single-tab
%li.active
%a= tab_title
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 270191f9452..f50e0724e09 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -1,4 +1,4 @@
-%ul.new-session-tabs.nav-links.nav-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
+%ul.nav-links.new-session-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
- if crowd_enabled?
%li.active
= link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index 1ba6d390875..fa3c3df7f60 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -1,4 +1,4 @@
-%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' }
+%ul.nav-links.new-session-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
%a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if allow_signup?
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 8680ec2e298..646e89e9bd1 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -7,7 +7,7 @@
- unless expanded
- diff_data = { lines_path: project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) }
-.diff-file.file-holder{ class: diff_file_class, data: diff_data }
+.diff-file.file-holder.js-lazy-load-discussion{ class: diff_file_class, data: diff_data }
.js-file-title.file-title.file-title-flex-parent
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
@@ -28,8 +28,11 @@
%tr.line_holder.line-holder-placeholder
%td.old_line.diff-line-num
%td.new_line.diff-line-num
- %td.line_content
+ %td.line_content.js-success-lazy-load
.js-code-placeholder
+ %td.js-error-lazy-load-diff.hidden.diff-loading-error-block
+ - button = button_tag(_("Try again"), class: "btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button")
+ = _("Unable to load the diff. %{button_try_again}").html_safe % { button_try_again: button}
= render "discussions/diff_discussion", discussions: [discussion], expanded: true
- else
- partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff'
diff --git a/app/views/email_rejection_mailer/rejection.text.haml b/app/views/email_rejection_mailer/rejection.text.haml
index 6693e6f90e8..af518b5b583 100644
--- a/app/views/email_rejection_mailer/rejection.text.haml
+++ b/app/views/email_rejection_mailer/rejection.text.haml
@@ -1,4 +1,3 @@
Unfortunately, your email message to GitLab could not be processed.
-
-
+\
= @reason
diff --git a/app/views/groups/settings/badges/index.html.haml b/app/views/groups/settings/badges/index.html.haml
new file mode 100644
index 00000000000..c7afb25d0f8
--- /dev/null
+++ b/app/views/groups/settings/badges/index.html.haml
@@ -0,0 +1,4 @@
+- breadcrumb_title _('Project Badges')
+- page_title _('Project Badges')
+
+= render 'shared/badges/badge_settings'
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index eb32f393310..6f53f5ac1ae 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -19,8 +19,8 @@
%li.dropdown-bold-header GitLab
- if @project&.persisted?
- - create_project_issue = can?(current_user, :create_issue, @project)
- - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - create_project_issue = show_new_issue_link?(@project)
+ - merge_project = merge_request_source_project_for_project(@project)
- create_project_snippet = can?(current_user, :create_project_snippet, @project)
- if create_project_issue || merge_project || create_project_snippet
%li.dropdown-bold-header This project
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 5ea19c9882d..517d9aa3d99 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -112,7 +112,7 @@
%span.nav-item-name
Settings
%ul.sidebar-sub-level-items
- = nav_link(path: %w[groups#projects groups#edit ci_cd#show], html_options: { class: "fly-out-top-item" } ) do
+ = 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
#{ _('Settings') }
@@ -122,6 +122,12 @@
%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/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 5c90d13420f..196db08cebd 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -13,7 +13,7 @@
.nav-icon-container
= sprite_icon('project')
%span.nav-item-name
- Overview
+ Project
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
@@ -258,7 +258,7 @@
#{ _('Snippets') }
- if project_nav_tab? :settings
- = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do
+ = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show]) do
= link_to edit_project_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
= sprite_icon('settings')
@@ -268,7 +268,7 @@
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
- if can_edit
- = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show], html_options: { class: "fly-out-top-item" } ) do
= link_to edit_project_path(@project) do
%strong.fly-out-top-item-name
#{ _('Settings') }
@@ -282,6 +282,11 @@
%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/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml
index 4c507c08ed7..67744ec1cee 100644
--- a/app/views/notify/push_to_merge_request_email.html.haml
+++ b/app/views/notify/push_to_merge_request_email.html.haml
@@ -7,7 +7,7 @@
- count = @existing_commits.size
%ul
%li
- - if count.one?
+ - if count == 1
- commit_id = @existing_commits.first[:short_id]
= link_to(commit_id, project_commit_url(@merge_request.target_project, commit_id))
- else
diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml
index 553f771f1a6..95759d127e2 100644
--- a/app/views/notify/push_to_merge_request_email.text.haml
+++ b/app/views/notify/push_to_merge_request_email.text.haml
@@ -4,7 +4,7 @@
\
- if @existing_commits.any?
- count = @existing_commits.size
- - commits_id = count.one? ? @existing_commits.first[:short_id] : "#{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}"
+ - commits_id = count == 1 ? @existing_commits.first[:short_id] : "#{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}"
- commits_text = "#{count} commit".pluralize(count)
* #{commits_id} - #{commits_text} from branch `#{@merge_request.target_branch}`
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 02263095599..9c95b6281ba 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -57,20 +57,8 @@
= succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank'
.col-lg-8
- = form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
- .form-group
- = f.label :username, "Path", class: "label-light"
- .input-group
- .input-group-addon
- = root_url
- = f.text_field :username, required: true, class: 'form-control'
- .help-block
- Current path:
- #{root_url}#{current_user.username}
- .prepend-top-default
- = f.button class: "btn btn-warning", type: "submit" do
- = icon "spinner spin", class: "hidden loading-username"
- Update username
+ - data = { initial_username: current_user.username, root_url: root_url, action_url: update_username_profile_path(format: :json) }
+ #update-username{ data: data }
%hr
.row.prepend-top-default
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 78848542810..9b87a7aaca8 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -2,7 +2,6 @@
- page_title "Personal Access Tokens"
- @content_class = "limit-container-width" unless fluid_layout
-
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
@@ -19,8 +18,10 @@
%h5.prepend-top-0
Your New Personal Access Token
.form-group
- = text_field_tag 'created-personal-access-token', @new_personal_access_token, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block"
- = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left")
+ .input-group
+ = text_field_tag 'created-personal-access-token', @new_personal_access_token, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block"
+ %span.input-group-btn
+ = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left", class: "btn-default btn-clipboard")
%span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
%hr
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 1bd10018b40..d1eae05c46c 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -20,7 +20,7 @@
- else
%p
Download the Google Authenticator application from App Store or Google Play Store and scan this code.
- More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}.
+ More information is available in the #{link_to('documentation', help_page_path('user/profile/account/two_factor_authentication'))}.
.row.append-bottom-10
.col-md-4
= raw @qr_code
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 825bfd0707f..1e7d9444986 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -21,11 +21,11 @@
%li Project uploads
%li Project configuration including web hooks and services
%li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
+ %li LFS objects
%p
The following items will NOT be exported:
%ul
%li Job traces and artifacts
- %li LFS objects
%li Container registry images
%li CI variables
%li Any encrypted tokens
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index a2ecfddb163..043057e79ee 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -23,11 +23,14 @@
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_source_name(@project) }
- .project-badges
+ .project-badges.prepend-top-default.append-bottom-default
- @project.badges.each do |badge|
- - badge_link_url = badge.rendered_link_url(@project)
- %a{ href: badge_link_url, target: '_blank', rel: 'noopener noreferrer' }
- %img{ src: badge.rendered_image_url(@project), alt: badge_link_url }
+ %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
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 6a1035d2dc7..f6d396c8127 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -13,6 +13,7 @@
#{time_ago_with_tooltip(event.created_at)}
- .flex-right
- = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do
- #{ _('Create merge request') }
+ - if can?(current_user, :create_merge_request_in, event.project.default_merge_request_target)
+ .flex-right
+ = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do
+ #{ _('Create merge request') }
diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml
deleted file mode 100644
index 4026b9e3c46..00000000000
--- a/app/views/projects/_visibility_select.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- if can_change_visibility_level?(@project, current_user)
- .select-wrapper
- = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select select-control')
- = icon('chevron-down')
-- else
- .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } }
- = visibility_level_icon(@project.visibility_level)
- %strong
- = visibility_level_label(@project.visibility_level)
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index 3124443b4e4..9c760c81527 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -2,6 +2,7 @@
- render_error = viewer.render_error
- rich_type = viewer.type == :rich ? viewer.partial_name : nil
- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
+- external_embed = local_assigns.fetch(:external_embed, false)
- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
.blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) }
@@ -9,6 +10,8 @@
= render 'projects/blob/render_error', viewer: viewer
- elsif load_async
= render viewer.loading_partial_path, viewer: viewer
+ - elsif external_embed
+ = render 'projects/blob/viewers/highlight_embed', blob: viewer.blob
- else
- viewer.prepare!
diff --git a/app/views/projects/blob/viewers/_highlight_embed.html.haml b/app/views/projects/blob/viewers/_highlight_embed.html.haml
new file mode 100644
index 00000000000..9bd4ef6ad0b
--- /dev/null
+++ b/app/views/projects/blob/viewers/_highlight_embed.html.haml
@@ -0,0 +1,7 @@
+.file-content.code.js-syntax-highlight
+ .line-numbers
+ - if blob.data.present?
+ - blob.data.each_line.each_with_index do |_, index|
+ %span.diff-line-num= index + 1
+ .blob-content{ data: { blob_id: blob.id } }
+ = highlight(blob.path, blob.data, repository: nil, plain: blob.no_highlighting?)
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 883dfb3e6c8..71176acd12d 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -4,7 +4,7 @@
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
- number_commits_behind = diverging_commit_counts[:behind]
- number_commits_ahead = diverging_commit_counts[:ahead]
-- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+- merge_project = merge_request_source_project_for_project(@project)
%li{ class: "branch-item js-branch-#{branch.name}" }
.branch-info
.branch-title
@@ -61,7 +61,7 @@
title: s_('Branches|The default branch cannot be deleted') }
= icon("trash-o")
- elsif protected_branch?(@project, branch)
- - if can?(current_user, :delete_protected_branch, @project)
+ - if can?(current_user, :push_to_delete_protected_branch, @project)
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
title: s_('Branches|Delete protected branch'),
data: { toggle: "modal",
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index fa9a9bfc8f7..f49f6e630d2 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,6 +1,7 @@
- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) }
- if !project.empty_repo? && can?(current_user, :download_code, project)
+ - archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
.project-action-button.dropdown.inline>
%button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') }
= sprite_icon('download')
@@ -10,16 +11,16 @@
%li.dropdown-header
#{ _('Source code') }
%li
- = link_to archive_project_repository_path(project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do
+ = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'zip'), rel: 'nofollow', download: '' do
%span= _('Download zip')
%li
- = link_to archive_project_repository_path(project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do
+ = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar.gz'), rel: 'nofollow', download: '' do
%span= _('Download tar.gz')
%li
- = link_to archive_project_repository_path(project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do
+ = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar.bz2'), rel: 'nofollow', download: '' do
%span= _('Download tar.bz2')
%li
- = link_to archive_project_repository_path(project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do
+ = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar'), rel: 'nofollow', download: '' do
%span= _('Download tar')
- if pipeline && pipeline.latest_builds_with_artifacts.any?
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 18e948ce35a..2e86a7d36d7 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,13 +1,17 @@
-- if current_user
+- can_create_issue = show_new_issue_link?(@project)
+- can_create_project_snippet = can?(current_user, :create_project_snippet, @project)
+- can_push_code = can?(current_user, :push_code, @project)
+- create_mr_from_new_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
+- merge_project = merge_request_source_project_for_project(@project)
+
+- show_menu = can_create_issue || can_create_project_snippet || can_push_code || create_mr_from_new_fork || merge_project
+
+- if show_menu
.project-action-button.dropdown.inline
%a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') }
= icon('plus')
= icon("caret-down")
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
- - can_create_issue = can?(current_user, :create_issue, @project)
- - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- - can_create_project_snippet = can?(current_user, :create_project_snippet, @project)
-
- if can_create_issue || merge_project || can_create_project_snippet
%li.dropdown-header= _('This project')
@@ -20,17 +24,17 @@
- if can_create_project_snippet
%li= link_to _('New snippet'), new_project_snippet_path(@project)
- - if can?(current_user, :push_code, @project)
+ - if can_push_code
%li.dropdown-header= _('This repository')
- - if can?(current_user, :push_code, @project)
+ - if can_push_code
%li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
- unless @project.empty_repo?
%li= link_to _('New branch'), new_project_branch_path(@project)
%li= link_to _('New tag'), new_project_tag_path(@project)
- - elsif current_user && current_user.already_forked?(@project)
+ - elsif can_collaborate_with_project?(@project)
%li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
- - elsif can?(current_user, :fork_project, @project)
+ - elsif create_mr_from_new_fork
- continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml
index 112dde66ff7..5f49d03b1bb 100644
--- a/app/views/projects/clusters/_empty_state.html.haml
+++ b/app/views/projects/clusters/_empty_state.html.haml
@@ -7,5 +7,6 @@
- link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
- .text-center
- = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
+ - if can?(current_user, :create_cluster, @project)
+ .text-center
+ = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index ebb7d247125..e004966bdcc 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -8,6 +8,6 @@
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
%p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab')
- = link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ = link_to s_('ClusterIntegration|Create on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
= link_to s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 461129a3e0e..213c4c90a0e 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,3 +1,5 @@
+- can_collaborate = can_collaborate_with_project?(@project)
+
.page-content-header.js-commit-box{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) }
.header-main-content
= render partial: 'signature', object: @commit.signature
@@ -32,12 +34,13 @@
%li.visible-xs-block.visible-sm-block
= link_to project_tree_path(@project, @commit) do
#{ _('Browse Files') }
- - unless @commit.has_been_reverted?(current_user)
+ - if can_collaborate && !@commit.has_been_reverted?(current_user)
%li.clearfix
= revert_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
- %li.clearfix
- = cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
- - if can_collaborate_with_project?
+ - if can_collaborate
+ %li.clearfix
+ = cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
+ - if can?(current_user, :push_code, @project)
%li.clearfix
= link_to s_("CreateTag|Tag"), new_project_tag_path(@project, ref: @commit)
%li.divider
@@ -49,10 +52,10 @@
.commit-box{ data: { project_path: project_path(@project) } }
%h3.commit-title
- = markdown(@commit.title, pipeline: :single_line, author: @commit.author)
+ = markdown_field(@commit, :title)
- if @commit.description.present?
%pre.commit-description
- = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
+ = preserve(markdown_field(@commit, :description))
.info-well
.well-segment.branch-info
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index abb292f8f27..541ae905246 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -17,6 +17,6 @@
.limited-width-notes
= render "shared/notes/notes_with_form", :autocomplete => true
- - if can_collaborate_with_project?
+ - if can_collaborate_with_project?(@project)
- %w(revert cherry-pick).each do |type|
= render "projects/commit/change", type: type, commit: @commit, title: @commit.title
diff --git a/app/views/projects/commits/_commit.atom.builder b/app/views/projects/commits/_commit.atom.builder
index 50f7e7a3a33..640b5ecf99e 100644
--- a/app/views/projects/commits/_commit.atom.builder
+++ b/app/views/projects/commits/_commit.atom.builder
@@ -10,5 +10,5 @@ xml.entry do
xml.email commit.author_email
end
- xml.summary markdown(commit.description, pipeline: :single_line), type: 'html'
+ xml.summary markdown_field(commit, :description), type: 'html'
end
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 078bd0eee63..289bfdd69bc 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -5,6 +5,7 @@
- link = commit_path(project, commit, merge_request: merge_request)
- cache_key = [project.full_path,
+ ref,
commit.id,
Gitlab::CurrentSettings.current_application_settings,
@path.presence,
@@ -22,7 +23,10 @@
.commit-detail.flex-list
.commit-content
- = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
+ - if view_details && merge_request
+ = link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
+ - else
+ = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
%span.commit-row-message.visible-xs-inline
&middot;
= commit.short_id
@@ -51,10 +55,10 @@
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
- .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } }
- = link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link"
- = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
- = link_to_browse_code(project, commit)
+ .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
- - if view_details && merge_request
- = link_to "View details", project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "btn btn-default"
+ .commit-sha-group
+ .label.label-monospace
+ = commit.short_id
+ = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"), class: "btn btn-default", container: "body")
+ = link_to_browse_code(project, commit)
diff --git a/app/views/projects/deploy_tokens/_form.html.haml b/app/views/projects/deploy_tokens/_form.html.haml
new file mode 100644
index 00000000000..f8db30df7b4
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_form.html.haml
@@ -0,0 +1,29 @@
+%p.profile-settings-content
+ = s_("DeployTokens|Pick a name for the application, and we'll give you a unique deploy token.")
+
+= form_for token, url: create_deploy_token_namespace_project_settings_repository_path(project.namespace, project), method: :post do |f|
+ = form_errors(token)
+
+ .form-group
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: 'form-control', required: true
+
+ .form-group
+ = f.label :expires_at, class: 'label-light'
+ = f.text_field :expires_at, class: 'datepicker form-control', value: f.object.expires_at
+
+ .form-group
+ = f.label :scopes, class: 'label-light'
+ %fieldset
+ = f.check_box :read_repository
+ = label_tag ("deploy_token_read_repository"), 'read_repository'
+ %span= s_('DeployTokens|Allows read-only access to the repository')
+
+ - if container_registry_enabled?(project)
+ %fieldset
+ = f.check_box :read_registry
+ = label_tag ("deploy_token_read_registry"), 'read_registry'
+ %span= s_('DeployTokens|Allows read-only access to the registry images')
+
+ .prepend-top-default
+ = f.submit s_('DeployTokens|Create deploy token'), class: 'btn btn-success'
diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml
new file mode 100644
index 00000000000..50e5950ced4
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_index.html.haml
@@ -0,0 +1,18 @@
+- expanded = expand_deploy_tokens_section?(@new_deploy_token)
+
+%section.settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4= s_('DeployTokens|Deploy Tokens')
+ %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = s_('DeployTokens|Deploy tokens allow read-only access to your repository and registry images.')
+ .settings-content
+ - if @new_deploy_token.persisted?
+ = render 'projects/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
+ - else
+ %h5.prepend-top-0
+ = s_('DeployTokens|Add a deploy token')
+ = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens
+ %hr
+ = render 'projects/deploy_tokens/table', project: @project, active_tokens: @deploy_tokens
diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
new file mode 100644
index 00000000000..1e715681e59
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
@@ -0,0 +1,14 @@
+.created-deploy-token-container
+ %h5.prepend-top-0
+ = s_('DeployTokens|Your New Deploy Token')
+
+ .form-group
+ = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
+ = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left')
+ %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.")
+
+ .form-group
+ = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
+ = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left')
+ %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.")
+%hr
diff --git a/app/views/projects/deploy_tokens/_revoke_modal.html.haml b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
new file mode 100644
index 00000000000..085964fe22e
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
@@ -0,0 +1,17 @@
+.modal{ id: "revoke-modal-#{token.id}" }
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %h4.modal-title.pull-left
+ = s_('DeployTokens|Revoke')
+ %b #{token.name}?
+ %button.close{ 'aria-label' => _('Close'), 'data-dismiss' => 'modal', type: 'button' }
+ %span{ 'aria-hidden' => 'true' } &times;
+ .modal-body
+ %p
+ = s_('DeployTokens|You are about to revoke')
+ %b #{token.name}.
+ = s_('DeployTokens|This action cannot be undone.')
+ .modal-footer
+ %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }= _('Cancel')
+ = link_to s_('DeployTokens|Revoke %{name}') % { name: token.name }, revoke_project_deploy_token_path(project, token), method: :put, class: 'btn btn-danger'
diff --git a/app/views/projects/deploy_tokens/_table.html.haml b/app/views/projects/deploy_tokens/_table.html.haml
new file mode 100644
index 00000000000..5013a9b250d
--- /dev/null
+++ b/app/views/projects/deploy_tokens/_table.html.haml
@@ -0,0 +1,31 @@
+%h5= s_("DeployTokens|Active Deploy Tokens (%{active_tokens})") % { active_tokens: active_tokens.length }
+
+- if active_tokens.present?
+ .table-responsive.deploy-tokens
+ %table.table
+ %thead
+ %tr
+ %th= s_('DeployTokens|Name')
+ %th= s_('DeployTokens|Username')
+ %th= s_('DeployTokens|Created')
+ %th= s_('DeployTokens|Expires')
+ %th= s_('DeployTokens|Scopes')
+ %th
+ %tbody
+ - active_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.username
+ %td= token.created_at.to_date.to_s(:medium)
+ %td
+ - if token.expires?
+ %span{ class: ('text-warning' if token.expires_soon?) }
+ In #{distance_of_time_in_words_to_now(token.expires_at)}
+ - else
+ %span.token-never-expires-label Never
+ %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
+ %td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger pull-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"}
+ = render 'projects/deploy_tokens/revoke_modal', token: token, project: project
+- else
+ .settings-message.text-center
+ = s_('DeployTokens|This project has no active Deploy Tokens.')
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 99eeb9551e3..0994498c6be 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -114,17 +114,18 @@
Archive project
- if @project.archived?
%p
- Unarchiving the project will mark its repository as active. The project can be committed to.
+ Unarchiving the project will restore people's ability to make changes to it.
+ The repository can be committed to, and issues, comments and other entities can be created.
%strong Once active this project shows up in the search and on the dashboard.
= link_to 'Unarchive project', unarchive_project_path(@project),
- data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." },
+ data: { confirm: "Are you sure that you want to unarchive this project?" },
method: :post, class: "btn btn-success"
- else
%p
- Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches.
- %strong Archived projects cannot be committed to!
+ Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches.
+ %strong The repository cannot be committed to, and no issues, comments or other entities can be created.
= link_to 'Archive project', archive_project_path(@project),
- data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
+ data: { confirm: "Are you sure that you want to archive this project?" },
method: :post, class: "btn btn-warning"
.sub-section.rename-respository
%h4.warning-title
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index cdfc3e232c5..816f2fa816d 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -8,4 +8,5 @@
%section.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@issue),
noteable_data: serialize_issuable(@issue),
+ noteable_type: 'issue',
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index 0d39edb7bfd..dd1a836fa20 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -2,9 +2,10 @@
= icon('rss')
- if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
-= 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",
- title: "New issue",
- id: "new_issue_link"
+- if show_new_issue_link?(@project)
+ = 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",
+ title: "New issue",
+ id: "new_issue_link"
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 36e24037214..4b8bf578b28 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,8 +1,8 @@
-- can_create_merge_request = can?(current_user, :create_merge_request, @project)
-- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
-- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
-
- if can?(current_user, :push_code, @project)
+ - can_create_merge_request = can?(current_user, :create_merge_request_in, @project)
+ - data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
+ - value = can_create_merge_request ? 'Create merge request' : 'Create branch'
+
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
- create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index ec7e87219f5..f1fc1c2316d 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -7,6 +7,7 @@
- can_update_issue = can?(current_user, :update_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
+- can_create_issue = show_new_issue_link?(@project)
.detail-page-header
.detail-page-header-body
@@ -42,16 +43,18 @@
%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'
- - if can_update_issue || can_report_spam
- %li.divider
- %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
+ - if can_create_issue
+ - if can_update_issue || can_report_spam
+ %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
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
- = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
- New issue
+ - if can_create_issue
+ = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
+ New issue
.issue-details.issuable-details
.detail-page-description.content-block
diff --git a/app/views/projects/jobs/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml
index c66313bdbf3..311934d9c33 100644
--- a/app/views/projects/jobs/_empty_state.html.haml
+++ b/app/views/projects/jobs/_empty_state.html.haml
@@ -1,7 +1,7 @@
- illustration = local_assigns.fetch(:illustration)
- illustration_size = local_assigns.fetch(:illustration_size)
- title = local_assigns.fetch(:title)
-- content = local_assigns.fetch(:content)
+- content = local_assigns.fetch(:content, nil)
- action = local_assigns.fetch(:action, nil)
.row.empty-state
@@ -11,7 +11,8 @@
.col-xs-12
.text-content
%h4.text-center= title
- %p= content
+ - 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
new file mode 100644
index 00000000000..e5198d047df
--- /dev/null
+++ b/app/views/projects/jobs/_empty_states.html.haml
@@ -0,0 +1,9 @@
+- 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/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index ecf186e3dc8..0b57ebedebd 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -1,5 +1,3 @@
-- builds = @build.pipeline.builds.to_a
-
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
@@ -91,7 +89,8 @@
- 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 } }
- = link_to project_job_path(@project, build) do
+ - tooltip = build.tooltip_message
+ = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: true, 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)
@@ -101,5 +100,4 @@
- else
= build.id
- if build.retried?
- %span.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
- = sprite_icon('retry', size:16, css_class: 'icon-retry')
+ = sprite_icon('retry', size:16, css_class: 'icon-retry')
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index fa27ded7cc2..cbbcc8f1db5 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -54,7 +54,8 @@
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.started?
+
+ - if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
.js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
@@ -88,26 +89,10 @@
%pre.build-trace#build-trace
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
- - elsif @build.playable?
- = render 'empty_state',
- illustration: 'illustrations/manual_action.svg',
- illustration_size: 'svg-394',
- title: _('This job requires a manual action'),
- content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments'),
- action: ( link_to _('Trigger this manual action'), play_project_job_path(@project, @build), method: :post, class: 'btn btn-primary', title: _('Trigger this manual action') )
- - elsif @build.created?
- = render 'empty_state',
- illustration: 'illustrations/job_not_triggered.svg',
- illustration_size: 'svg-306',
- title: _('This job has not been triggered yet'),
- content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered')
- else
- = render 'empty_state',
- illustration: 'illustrations/pending_job_empty.svg',
- illustration_size: 'svg-430',
- title: _('This job has not started yet'),
- content: _('This job is in pending state and is waiting to be picked by a runner')
- = render "sidebar"
+ = render "empty_states"
+
+ = render "sidebar", builds: @builds
.js-build-options{ data: javascript_build_options }
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index b2c0d9e1cfa..623380c9c61 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
-- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+- merge_project = merge_request_source_project_for_project(@project)
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests"
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 9866cc716ee..15a0e4d7ef5 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -80,6 +80,7 @@
- if has_vue_discussions_cookie?
#js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
noteable_data: serialize_issuable(@merge_request),
+ noteable_type: 'merge_request',
current_user_data: UserSerializer.new.represent(current_user).to_json} }
#commits.commits.tab-pane
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index 5ea653ccad5..b4fe1cabdfd 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -36,7 +36,7 @@
%template{ 'v-else' => '' }
= render 'shared/icons/icon_resolve_discussion.svg'
-- if current_user
+- if can?(current_user, :award_emoji, note)
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
.note-actions-item
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 75df92b05a7..27bbe52a714 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -1,28 +1,29 @@
+- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
+
- if can?(current_user, :update_pages, @project) && @domains.any?
.panel.panel-default
.panel-heading
Domains (#{@domains.count})
- %ul.well-list
- - verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
+ %ul.well-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
- @domains.each do |domain|
- %li
- .pull-right
+ %li.pages-domain-list-item.unstyled
+ - if verification_enabled
+ - tooltip, status = domain.unverified? ? [_('Unverified'), 'failed'] : [_('Verified'), 'success']
+ .domain-status.ci-status-icon.has-tooltip{ class: "ci-status-icon-#{status}", title: tooltip }
+ = sprite_icon("status_#{status}", size: 16 )
+ .domain-name
+ = link_to domain.url do
+ = domain.url
+ = icon('external-link')
+ - if domain.subject
+ %p
+ %span.label.label-gray Certificate: #{domain.subject}
+ - if domain.expired?
+ %span.label.label-danger Expired
+ %div
= link_to 'Details', project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped"
= link_to 'Remove', project_pages_domain_path(@project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
- .clearfix
- - if verification_enabled
- - tooltip, status = domain.unverified? ? ['Unverified', 'failed'] : ['Verified', 'success']
- = link_to domain.url, title: tooltip, class: 'has-tooltip' do
- = sprite_icon("status_#{status}", size: 16, css_class: "has-tooltip ci-status-icon ci-status-icon-#{status}")
- = domain.domain
- - else
- = link_to domain.domain, domain.url
- %p
- - if domain.subject
- %span.label.label-gray Certificate: #{domain.subject}
- - if domain.expired?
- %span.label.label-danger Expired
- if verification_enabled && domain.unverified?
%li.warning-row
#{domain.domain} is not verified. To learn how to verify ownership, visit your
- = link_to 'domain details', project_pages_domain_path(@project, domain)
+ #{link_to 'domain details', project_pages_domain_path(@project, domain)}.
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index f17d9d24db6..6adaea799b2 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -1,11 +1,10 @@
- page_title 'Pages'
-%h3.page_title
+%h3.page-title.with-button
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 pull-right', title: 'New Domain' do
- %i.fa.fa-plus
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 5645a4604bf..6c404990492 100644
--- a/app/views/projects/pages_domains/edit.html.haml
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -1,7 +1,7 @@
- add_to_breadcrumbs "Pages", project_pages_path(@project)
- breadcrumb_title @domain.domain
- page_title @domain.domain
-%h3.page_title
+%h3.page-title
= @domain.domain
%hr.clearfix
%div
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index e49163880c7..269df803a2b 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -1,6 +1,6 @@
- add_to_breadcrumbs "Pages", project_pages_path(@project)
- page_title 'New Pages Domain'
-%h3.page_title
+%h3.page-title
New Pages Domain
%hr.clearfix
%div
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index ba0713daee9..44d66f3b2d0 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -1,17 +1,19 @@
- add_to_breadcrumbs "Pages", project_pages_path(@project)
- breadcrumb_title @domain.domain
- page_title "#{@domain.domain}", 'Pages Domains'
+- dns_record = "#{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}."
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
+
- if verification_enabled && @domain.unverified?
- %p.alert.alert-warning
- %strong
- This domain is not verified. You will need to verify ownership before
- access is enabled.
+ = content_for :flash_message do
+ .alert.alert-warning
+ .container-fluid.container-limited
+ This domain is not verified. You will need to verify ownership before access is enabled.
-%h3.page-title
- Pages Domain
+%h3.page-title.with-button
= link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success pull-right'
+ Pages Domain
.table-holder
%table.table
@@ -19,31 +21,41 @@
%td
Domain
%td
- = link_to @domain.domain, @domain.url
+ = link_to @domain.url do
+ = @domain.url
+ = icon('external-link')
%tr
%td
DNS
%td
- %p
- To access this domain create a new DNS record:
- %pre
- #{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}.
+ .input-group
+ = text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
+ .input-group-btn
+ = clipboard_button(target: '#domain_dns', class: 'btn-default hidden-xs')
+ %p.help-block
+ To access this domain create a new DNS record
+
- if verification_enabled
+ - verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}"
%tr
%td
Verification status
%td
- %p
+ = form_tag verify_project_pages_domain_path(@project, @domain) do
+ .status-badge
+ - text, status = @domain.unverified? ? [_('Unverified'), 'label-danger'] : [_('Verified'), 'label-success']
+ .label{ class: status }
+ = text
+ %button.btn.has-tooltip{ type: "submit", data: { container: 'body' }, title: _("Retry verification") }
+ = sprite_icon('redo')
+ .input-group
+ = text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
+ .input-group-btn
+ = clipboard_button(target: '#domain_verification', class: 'btn-default hidden-xs')
+ %p.help-block
- help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
- To #{link_to 'verify ownership', help_link} of your domain, create
- this DNS record:
- %pre
- #{@domain.verification_domain} TXT #{@domain.keyed_verification_code}
- %p
- - if @domain.verified?
- #{@domain.domain} has been successfully verified.
- - else
- = button_to 'Verify ownership', verify_project_pages_domain_path(@project, @domain), class: 'btn btn-save btn-sm'
+ To #{link_to 'verify ownership', help_link} of your domain,
+ add the above key to a TXT record within to your DNS configuration.
%tr
%td
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 877101b05ca..8f2142af2ce 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,24 +1,25 @@
- breadcrumb_title "Pipelines"
-- page_title "New Pipeline"
+- page_title = s_("Pipeline|Run Pipeline")
%h3.page-title
- New Pipeline
+ = s_("Pipeline|Run Pipeline")
%hr
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group
- = f.label :ref, 'Create for', class: 'control-label'
+ = f.label :ref, s_('Pipeline|Run on'), class: 'control-label'
.col-sm-10
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
- filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search branches",
+ filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
- .help-block Existing branch name, tag
+ .help-block
+ = s_("Pipeline|Existing branch name, tag")
.form-actions
- = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
- = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel'
+ = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3
+ = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index a09c13176c3..300055a4207 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -1,4 +1,4 @@
-.panel.panel-default.protected-branches-list.js-protected-branches-list
+.protected-branches-list.js-protected-branches-list
- if @protected_branches.empty?
.panel-heading
%h3.panel-title
diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml
index 02908e16dc5..3ed82e51dbe 100644
--- a/app/views/projects/protected_tags/shared/_tags_list.html.haml
+++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml
@@ -1,4 +1,4 @@
-.panel.panel-default.protected-tags-list.js-protected-tags-list
+.protected-tags-list.js-protected-tags-list
- if @protected_tags.empty?
.panel-heading
%h3.panel-title
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 12d56e244ce..2c80f7c3fa3 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -29,6 +29,10 @@
docker login #{Gitlab.config.registry.host_port}
%br
%p
+ - deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
+ = s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
+ %br
+ %p
= s_('ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe }
%pre
:plain
diff --git a/app/views/projects/settings/badges/index.html.haml b/app/views/projects/settings/badges/index.html.haml
new file mode 100644
index 00000000000..b68ed70de89
--- /dev/null
+++ b/app/views/projects/settings/badges/index.html.haml
@@ -0,0 +1,4 @@
+- breadcrumb_title _('Badges')
+- page_title _('Badges')
+
+= render 'shared/badges/badge_settings'
diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml
index e8028059487..e8028059487 100644
--- a/app/views/projects/pipelines_settings/_badge.html.haml
+++ b/app/views/projects/settings/ci_cd/_badge.html.haml
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 646c01c0989..20868f9ba5d 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -1,6 +1,7 @@
.row.prepend-top-default
.col-lg-12
- = form_for @project, url: project_pipelines_settings_path(@project) do |f|
+ = form_for @project, url: project_settings_ci_cd_path(@project) do |f|
+ = form_errors(@project)
%fieldset.builds-feature
.form-group
%h5 Auto DevOps (Beta)
@@ -73,10 +74,10 @@
%hr
.form-group
- = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
- = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
+ = f.label :build_timeout_human_readable, 'Timeout', class: 'label-light'
+ = f.text_field :build_timeout_human_readable, class: 'form-control'
%p.help-block
- Per job in minutes. If a job passes this threshold, it will be marked as failed
+ Per job. If a job passes this threshold, it will be marked as failed
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
@@ -151,10 +152,13 @@
%li
excoveralls (Elixir) -
%code \[TOTAL\]\s+(\d+\.\d+)%
+ %li
+ JaCoCo (Java/Kotlin)
+ %code Total.*?([0-9]{1,3})%
= f.submit 'Save changes', class: "btn btn-save"
%hr
.row.prepend-top-default
- = render partial: 'projects/pipelines_settings/badge', collection: @badges
+ = render partial: 'badge', collection: @badges
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index d65341dbd40..09268c9943b 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -3,8 +3,9 @@
- page_title "CI / CD"
- expanded = Rails.env.test?
+- general_expanded = @project.errors.empty? ? expanded : true
-%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
%h4
General pipelines settings
@@ -13,7 +14,7 @@
%p
Update your CI/CD configuration, like job timeout or Auto DevOps.
.settings-content
- = render 'projects/pipelines_settings/show'
+ = render 'form'
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 6bef4d19434..f57590a908f 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -9,3 +9,4 @@
= render "projects/protected_branches/index"
= render "projects/protected_tags/index"
= render @deploy_keys
+= render "projects/deploy_tokens/index"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 94331a16abd..e28accd5b43 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -24,7 +24,7 @@
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
- #{ _('Archived project! Repository is read-only') }
+ #{ _('Archived project! Repository and other project resources are read-only') }
- view_path = @project.default_view
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 3d5f92f9aaa..98b4d6339da 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -31,6 +31,6 @@
= link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
= icon("pencil")
- - if can?(current_user, :admin_project, @project)
- = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
- = icon("trash-o")
+ - if can?(current_user, :admin_project, @project)
+ = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
+ = icon("trash-o")
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index dfe2c37ed8e..7a3469cdd26 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -28,7 +28,7 @@
= icon('history')
.btn-container.controls-item
= render 'projects/buttons/download', project: @project, ref: @tag.name
- - if can?(current_user, :admin_project, @project)
+ - if can?(current_user, :push_code, @project) && can?(current_user, :admin_project, @project)
.btn-container.controls-item-full
= link_to project_tag_path(@project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: @tag.name } } do
%i.fa.fa-trash-o
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 5ef5e9c09a2..8587d3b0c0d 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -1,3 +1,6 @@
+- can_collaborate = can_collaborate_with_project?(@project)
+- can_create_mr_from_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
+
.tree-ref-container
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
@@ -15,7 +18,7 @@
%li
= link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
- - if current_user
+ - if can_collaborate || can_create_mr_from_fork
%li
%a.btn.add-to-tree{ addtotree_toggle_attributes }
= sprite_icon('plus', size: 16, css_class: 'pull-left')
@@ -35,7 +38,7 @@
%li
= link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
#{ _('New directory') }
- - elsif can?(current_user, :fork_project, @project)
+ - elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
%li
- continue_params = { to: project_new_blob_path(@project, @id),
notice: edit_in_new_fork_notice,
@@ -61,23 +64,25 @@
= link_to fork_path, method: :post do
#{ _('New directory') }
- %li.divider
- %li.dropdown-header
- #{ _('This repository') }
- %li
- = link_to new_project_branch_path(@project) do
- #{ _('New branch') }
- %li
- = link_to new_project_tag_path(@project) do
- #{ _('New tag') }
+ - if can?(current_user, :push_code, @project)
+ %li.divider
+ %li.dropdown-header
+ #{ _('This repository') }
+ %li
+ = link_to new_project_branch_path(@project) do
+ #{ _('New branch') }
+ %li
+ = link_to new_project_tag_path(@project) do
+ #{ _('New tag') }
.tree-controls
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
= render 'projects/find_file_link'
- = succeed " " do
- = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do
- = _('Web IDE')
+ - if can_collaborate
+ = succeed " " do
+ = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do
+ = _('Web IDE')
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index 934d65e8b42..e9ac192f5f7 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -1,14 +1,14 @@
-.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
+.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20.prepend-top-10{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
.banner-graphic
= custom_icon('icon_autodevops')
- .prepend-top-10.prepend-left-10.append-bottom-10
- %h5= s_('AutoDevOps|Auto DevOps (Beta)')
+ .banner-body.prepend-left-10.append-bottom-10
+ %h5.banner-title= s_('AutoDevOps|Auto DevOps (Beta)')
%p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
%p
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
- .prepend-top-10
+ .banner-buttons
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout'
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 56403907844..836df57a3a2 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -47,20 +47,20 @@
class: 'text-danger'
.pull-right.hidden-xs.hidden-sm
- - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
- %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
- disabled: true,
- type: 'button',
- data: { url: promote_project_label_path(label.project, label),
- label_title: label.title,
- label_color: label.color,
- label_text_color: label.text_color,
- group_name: label.project.group.name,
- target: '#promote-label-modal',
- container: 'body',
- toggle: 'modal' } }
- = sprite_icon('level-up')
- if can?(current_user, :admin_label, label)
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
+ %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
+ disabled: true,
+ type: 'button',
+ data: { url: promote_project_label_path(label.project, label),
+ label_title: label.title,
+ label_color: label.color,
+ label_text_color: label.text_color,
+ group_name: label.project.group.name,
+ target: '#promote-label-modal',
+ container: 'body',
+ toggle: 'modal' } }
+ = sprite_icon('level-up')
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
= sprite_icon('pencil')
diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml
index 93a4301f366..a0ba1afc284 100644
--- a/app/views/shared/_recaptcha_form.html.haml
+++ b/app/views/shared/_recaptcha_form.html.haml
@@ -10,7 +10,7 @@
= hidden_field(resource_name, field, value: value)
= hidden_field_tag(:spam_log_id, spammable.spam_log.id)
= hidden_field_tag(:recaptcha_verification, true)
- = recaptcha_tags script: script, callback: 'recaptchaDialogCallback'
+ = recaptcha_tags script: script, callback: 'recaptchaDialogCallback' unless Rails.env.test?
-# Yields a block with given extra params.
= yield
diff --git a/app/views/shared/badges/_badge_settings.html.haml b/app/views/shared/badges/_badge_settings.html.haml
new file mode 100644
index 00000000000..b7c250d3b1c
--- /dev/null
+++ b/app/views/shared/badges/_badge_settings.html.haml
@@ -0,0 +1,4 @@
+#badge-settings{ data: { api_endpoint_url: @badge_api_endpoint,
+ docs_url: help_page_path('user/project/badges')} }
+ .text-center.prepend-top-default
+ = icon('spinner spin 2x')
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
index 8e5e32e9f16..b385cc3f962 100644
--- a/app/views/shared/boards/components/_sidebar.html.haml
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -22,6 +22,6 @@
= render "shared/boards/components/sidebar/labels"
= render "shared/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue",
- ":issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'",
+ ":issue-update" => "issue.sidebarInfoEndpoint",
":list" => "list",
"v-if" => "canRemove" }
diff --git a/app/views/shared/boards/components/sidebar/_assignee.html.haml b/app/views/shared/boards/components/sidebar/_assignee.html.haml
index 3d2e8471a60..1374da9d82c 100644
--- a/app/views/shared/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/shared/boards/components/sidebar/_assignee.html.haml
@@ -21,8 +21,7 @@
.dropdown
- dropdown_options = issue_assignees_dropdown_options
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data,
- ":data-issuable-id" => "issue.iid",
- ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
+ ":data-issuable-id" => "issue.iid" }
= dropdown_options[:title]
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
diff --git a/app/views/shared/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml
index db794d6f855..d13b998e6f4 100644
--- a/app/views/shared/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml
@@ -22,8 +22,7 @@
":value" => "issue.dueDate" }
.dropdown
%button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
- data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" },
- ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
+ data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" } }
%span.dropdown-toggle-text Due date
= icon('chevron-down')
.dropdown-menu.dropdown-menu-due-date
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index dfc0f9be321..87e6b52f46e 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -26,8 +26,7 @@
project_id: @project&.try(:id),
labels: labels_filter_path(false),
namespace_path: @namespace_path,
- project_path: @project.try(:path) },
- ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
+ project_path: @project.try(:path) } }
%span.dropdown-toggle-text
Label
= icon('chevron-down')
diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml
index d09c7c218e0..f51c4a97f2b 100644
--- a/app/views/shared/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml
@@ -18,8 +18,7 @@
.dropdown
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
":data-selected" => "milestoneTitle",
- ":data-issuable-id" => "issue.iid",
- ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
+ ":data-issuable-id" => "issue.iid" }
Milestone
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable
diff --git a/app/views/shared/dashboard/_no_filter_selected.html.haml b/app/views/shared/dashboard/_no_filter_selected.html.haml
new file mode 100644
index 00000000000..b2e6967f6aa
--- /dev/null
+++ b/app/views/shared/dashboard/_no_filter_selected.html.haml
@@ -0,0 +1,8 @@
+.row.empty-state.text-center
+ .col-xs-12
+ .svg-130.prepend-top-default
+ = image_tag 'illustrations/issue-dashboard_results-without-filter.svg'
+ .col-xs-12
+ .text-content
+ %h4
+ = _("Please select at least one filter to see results")
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 7704c88905b..1bd5b4164b1 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -24,12 +24,9 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
- - if issuable_filter_present?
- .filter-item.inline.reset-filters
- %a{ href: page_filter_path(without: issuable_filter_params) } Reset filters
-
- .pull-right
- = render 'shared/sort_dropdown'
+ - unless @no_filters_set
+ .pull-right
+ = render 'shared/sort_dropdown'
- has_labels = @labels && @labels.any?
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 4d8109eb90c..a5f40ea934b 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -1,22 +1,23 @@
- type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false)
+- display_count = local_assigns.fetch(:display_count, :true)
%ul.nav-links.issues-state-filters.mobile-separator
%li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
- #{issuables_state_counter_text(type, :opened)}
+ #{issuables_state_counter_text(type, :opened, display_count)}
- if type == :merge_requests
%li{ class: active_when(params[:state] == 'merged') }>
= link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do
- #{issuables_state_counter_text(type, :merged)}
+ #{issuables_state_counter_text(type, :merged, display_count)}
%li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do
- #{issuables_state_counter_text(type, :closed)}
+ #{issuables_state_counter_text(type, :closed, display_count)}
- else
%li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do
- #{issuables_state_counter_text(type, :closed)}
+ #{issuables_state_counter_text(type, :closed, display_count)}
- = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all)
+ = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count)
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 6afcd447f28..975b9cb4729 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -107,7 +107,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: (project_labels_path(@project, :json) if @project) } }
+ %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) } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 5868c52566d..fc634856061 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -8,7 +8,7 @@
%strong
= link_to group.full_name, group_path(group)
.cgray
- Joined #{time_ago_with_tooltip(group.created_at)}
+ Given access #{time_ago_with_tooltip(group_link.created_at)}
- if group_link.expires?
·
%span{ class: ('text-warning' if group_link.expires_soon?) }
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index ba57d922c6d..1c139827acf 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -29,7 +29,7 @@
Requested
= time_ago_with_tooltip(member.requested_at)
- else
- Joined #{time_ago_with_tooltip(member.created_at)}
+ Given access #{time_ago_with_tooltip(member.created_at)}
- if member.expires?
·
%span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) }
diff --git a/app/views/shared/milestones/_deprecation_message.html.haml b/app/views/shared/milestones/_deprecation_message.html.haml
new file mode 100644
index 00000000000..4a8f90937ea
--- /dev/null
+++ b/app/views/shared/milestones/_deprecation_message.html.haml
@@ -0,0 +1,14 @@
+.banner-callout.compact.milestone-deprecation-message.js-milestone-deprecation-message.prepend-top-20
+ .banner-graphic= image_tag 'illustrations/milestone_removing-page.svg'
+ .banner-body.prepend-left-10.append-right-10
+ %h5.banner-title.prepend-top-0= _('This page will be removed in a future release.')
+ %p.milestone-banner-text= _('Use group milestones to manage issues from multiple projects in the same milestone.')
+ = button_tag _('Promote these project milestones into a group milestone.'), class: 'btn btn-link js-popover-link text-align-left milestone-banner-link'
+ .milestone-banner-buttons.prepend-top-20= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-default', target: '_blank'
+
+ %template.js-milestone-deprecation-message-template
+ .milestone-popover-body
+ %ol.milestone-popover-instructions-list.append-bottom-0
+ %li= _('Click any <strong>project name</strong> in the project list below to navigate to the project milestone.').html_safe
+ %li= _('Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.').html_safe
+ .milestone-popover-footer= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-link prepend-left-0', target: '_blank'
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index a942ebc328b..ee134480705 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -72,7 +72,7 @@
.title.hide-collapsed
Issues
%span.badge= milestone.issues_visible_to_user(current_user).count
- - if project && can?(current_user, :create_issue, project)
+ - if show_new_issue_link?(project)
= link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do
New issue
.value.hide-collapsed.bold
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index f302299eb24..797ff034bb2 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -1,7 +1,8 @@
-- page_title @milestone.title
+- page_title milestone.title
- @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- group = local_assigns[:group]
+- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone?
.detail-page-header
%a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
@@ -31,21 +32,23 @@
- else
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
+= render 'shared/milestones/deprecation_message' if is_dynamic_milestone
+
.detail-page-description.milestone-detail
%h2.title
= markdown_field(milestone, :title)
- - if @milestone.group_milestone? && @milestone.description.present?
+ - if milestone.group_milestone? && milestone.description.present?
%div
.description
.wiki
- = markdown_field(@milestone, :description)
+ = markdown_field(milestone, :description)
- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
-- if @milestone.legacy_group_milestone? || @milestone.dashboard_milestone?
+- if is_dynamic_milestone
.table-holder
%table.table
%thead
@@ -68,7 +71,7 @@
Open
%td
= ms.expires_at
-- elsif @milestone.group_milestone?
+- elsif milestone.group_milestone?
%br
View
= link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title)
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index bf359774ead..893a7f26ebd 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -2,7 +2,7 @@
- return if note.cross_reference_not_visible_for?(current_user)
- show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false)
-- note_editable = note_editable?(note)
+- note_editable = can?(current_user, :admin_note, note)
- note_counter = local_assigns.fetch(:note_counter, 0)
%li.timeline-entry{ id: dom_id(note),
diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml
new file mode 100644
index 00000000000..2d93e51a2d9
--- /dev/null
+++ b/app/views/shared/snippets/_embed.html.haml
@@ -0,0 +1,24 @@
+- blob = @snippet.blob
+.gitlab-embed-snippets
+ .js-file-title.file-title-flex-parent
+ .file-header-content
+ = external_snippet_icon('doc_text')
+
+ %strong.file-title-name
+ %a.gitlab-embedded-snippets-title{ href: url_for(only_path: false, overwrite_params: nil) }
+ = blob.name
+
+ %small
+ = number_to_human_size(blob.raw_size)
+ %a.gitlab-logo{ href: url_for(only_path: false, overwrite_params: nil), title: 'view on gitlab' }
+ on &nbsp;
+ %span.logo-text
+ GitLab
+
+ .file-actions.hidden-xs
+ .btn-group{ role: "group" }<
+ = embedded_snippet_raw_button
+
+ = embedded_snippet_download_button
+ %article.file-holder.snippet-file-content
+ = render 'projects/blob/viewer', viewer: @snippet.blob.simple_viewer, load_async: false, external_embed: true
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 12df79a28c7..836230ae8ee 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -19,11 +19,32 @@
%h2.snippet-title.prepend-top-0.append-bottom-0
= markdown_field(@snippet, :title)
- - if @snippet.updated_at != @snippet.created_at
- = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true)
- if @snippet.description.present?
.description
.wiki
= markdown_field(@snippet, :description)
%textarea.hidden.js-task-list-field
= @snippet.description
+
+ - if @snippet.updated_at != @snippet.created_at
+ = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true)
+
+ - if public_snippet?
+ .embed-snippet
+ .input-group
+ .input-group-btn
+ %button.btn.embed-toggle{ 'data-toggle': 'dropdown', type: 'button' }
+ %span.js-embed-action= _("Embed")
+ = sprite_icon('angle-down', size: 12)
+ %ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list
+ %li
+ %button.js-embed-btn.btn.btn-transparent.is-active{ type: 'button' }
+ %strong.embed-toggle-list-item= _("Embed")
+ %li
+ %button.js-share-btn.btn.btn-transparent{ type: 'button' }
+ %strong.embed-toggle-list-item= _("Share")
+ %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
+ .input-group-btn
+ %button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' }
+ = sprite_icon('duplicate', size: 16)
+ .clearfix
diff --git a/app/views/shared/snippets/show.js.haml b/app/views/shared/snippets/show.js.haml
new file mode 100644
index 00000000000..a9af732bbb5
--- /dev/null
+++ b/app/views/shared/snippets/show.js.haml
@@ -0,0 +1,2 @@
+document.write('#{escape_javascript(stylesheet_link_tag "#{stylesheet_url 'snippets'}")}');
+document.write('#{escape_javascript(render 'shared/snippets/embed')}');
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index ad4d39b4aa1..d36ca032558 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -33,6 +33,13 @@
%p.light
This URL will be triggered when someone adds a comment
%li
+ = form.check_box :confidential_note_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :confidential_note_events, class: 'list-label' do
+ %strong Confidential Comments
+ %p.light
+ This URL will be triggered when someone adds a comment on a confidential issue
+ %li
= form.check_box :issues_events, class: 'pull-left'
.prepend-left-20
= form.label :issues_events, class: 'list-label' do
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index d7e24491516..8fe3619f6ee 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -2,6 +2,14 @@ class AuthorizedProjectsWorker
include ApplicationWorker
prepend WaitableWorker
+ # This is a workaround for a Ruby 2.3.7 bug. rspec-mocks cannot restore the
+ # visibility of prepended modules. See https://github.com/rspec/rspec-mocks/issues/1231
+ # for more details.
+ if Rails.env.test?
+ def self.bulk_perform_and_wait(args_list, timeout: 10)
+ end
+ end
+
def perform(user_id)
user = User.find_by(id: user_id)
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 67c54fbf10e..b925741934a 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -5,7 +5,7 @@ class NewNoteWorker
# old `NewNoteWorker` jobs (can remove later)
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
- NotificationService.new.new_note(note)
+ NotificationService.new.new_note(note) if note.can_create_notification?
Notes::PostProcessService.new(note).execute
else
Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")