summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-20 09:07:57 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-20 09:07:57 +0000
commit7881eb30eaa8b01dbcfe87faa09927c75c7d6e45 (patch)
tree298bc8d2c62b2f2c29cb8ecbcf3de3eaaa6466d9 /app/assets
parent64b66e0cb6d1bfd27abf24e06653f00bddb60597 (diff)
downloadgitlab-ce-7881eb30eaa8b01dbcfe87faa09927c75c7d6e45.tar.gz
Add latest changes from gitlab-org/gitlab@12-6-stable-ee
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/api.js29
-rw-r--r--app/assets/javascripts/autosave.js16
-rw-r--r--app/assets/javascripts/badges/components/badge.vue7
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue20
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue11
-rw-r--r--app/assets/javascripts/badges/empty_badge.js1
-rw-r--r--app/assets/javascripts/badges/store/actions.js10
-rw-r--r--app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/image.js2
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js2
-rw-r--r--app/assets/javascripts/blob/openapi/index.js19
-rw-r--r--app/assets/javascripts/blob/openapi_viewer.js3
-rw-r--r--app/assets/javascripts/blob/viewer/index.js3
-rw-r--r--app/assets/javascripts/boards/components/board.js26
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue5
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue3
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_count.vue36
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue2
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue4
-rw-r--r--app/assets/javascripts/boards/constants.js2
-rw-r--r--app/assets/javascripts/boards/index.js45
-rw-r--r--app/assets/javascripts/boards/mixins/is_wip_limits.js7
-rw-r--r--app/assets/javascripts/boards/models/list.js31
-rw-r--r--app/assets/javascripts/boards/services/board_service.js98
-rw-r--r--app/assets/javascripts/boards/stores/state.js1
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js24
-rw-r--r--app/assets/javascripts/clusters/components/crossplane_provider_stack.vue10
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue2
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue168
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue2
-rw-r--r--app/assets/javascripts/commit/image_file.js49
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue30
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js57
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue2
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue24
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue15
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue4
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/index.js22
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js148
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/actions.js13
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/cluster_dropdown/index.js4
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/index.js30
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js3
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/state.js2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue2
-rw-r--r--app/assets/javascripts/create_cluster/init_create_cluster.js20
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/banner.vue2
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue4
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue90
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue8
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue2
-rw-r--r--app/assets/javascripts/diffs/constants.js1
-rw-r--r--app/assets/javascripts/diffs/index.js4
-rw-r--r--app/assets/javascripts/diffs/store/actions.js76
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js41
-rw-r--r--app/assets/javascripts/diffs/store/utils.js7
-rw-r--r--app/assets/javascripts/emoji/no_emoji_validator.js2
-rw-r--r--app/assets/javascripts/environments/components/container.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue82
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue2
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue6
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue54
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue12
-rw-r--r--app/assets/javascripts/environments/index.js2
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js2
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue78
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue292
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace.vue2
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue63
-rw-r--r--app/assets/javascripts/error_tracking/details.js5
-rw-r--r--app/assets/javascripts/error_tracking/services/index.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/details/getters.js5
-rw-r--r--app/assets/javascripts/error_tracking/store/index.js2
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js49
-rw-r--r--app/assets/javascripts/error_tracking/store/list/getters.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutation_types.js9
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutations.js51
-rw-r--r--app/assets/javascripts/error_tracking/store/list/state.js7
-rw-r--r--app/assets/javascripts/error_tracking/utils.js13
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue10
-rw-r--r--app/assets/javascripts/filtered_search/.eslintrc.yml3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js2
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue2
-rw-r--r--app/assets/javascripts/frequent_items/store/mutations.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js68
-rw-r--r--app/assets/javascripts/gl_dropdown.js1661
-rw-r--r--app/assets/javascripts/grafana_integration/components/grafana_integration.vue2
-rw-r--r--app/assets/javascripts/groups/components/app.vue2
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue7
-rw-r--r--app/assets/javascripts/groups/mixins/is_project_pending_removal.js7
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js1
-rw-r--r--app/assets/javascripts/helpers/monitor_helper.js1
-rw-r--r--app/assets/javascripts/ide/.eslintrc.yml2
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue2
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue2
-rw-r--r--app/assets/javascripts/ide/lib/files.js3
-rw-r--r--app/assets/javascripts/ide/services/index.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions.js14
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/getters.js25
-rw-r--r--app/assets/javascripts/ide/stores/utils.js3
-rw-r--r--app/assets/javascripts/image_diff/.eslintrc.yml3
-rw-r--r--app/assets/javascripts/issuable_form.js54
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/item.vue2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue6
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue2
-rw-r--r--app/assets/javascripts/jobs/store/utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js4
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js22
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js162
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js1
-rw-r--r--app/assets/javascripts/lib/utils/logoutput_behaviours.js47
-rw-r--r--app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js38
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js104
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/monitoring/components/charts/anomaly.vue13
-rw-r--r--app/assets/javascripts/monitoring/components/charts/column.vue12
-rw-r--r--app/assets/javascripts/monitoring/components/charts/heatmap.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/charts/single_stat.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue42
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue152
-rw-r--r--app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue30
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue30
-rw-r--r--app/assets/javascripts/monitoring/components/group_empty_state.vue105
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue29
-rw-r--r--app/assets/javascripts/monitoring/constants.js47
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js2
-rw-r--r--app/assets/javascripts/monitoring/monitoring_tracking_helper.js10
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js85
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js62
-rw-r--r--app/assets/javascripts/monitoring/stores/index.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js8
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js143
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js5
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js89
-rw-r--r--app/assets/javascripts/monitoring/utils.js26
-rw-r--r--app/assets/javascripts/mr_popover/components/mr_popover.vue2
-rw-r--r--app/assets/javascripts/mr_tabs_popover/components/popover.vue64
-rw-r--r--app/assets/javascripts/mr_tabs_popover/index.js12
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue73
-rw-r--r--app/assets/javascripts/notes.js163
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue17
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue14
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue13
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue14
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue29
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue11
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js24
-rw-r--r--app/assets/javascripts/notes/mixins/issuable_state.js6
-rw-r--r--app/assets/javascripts/notes/stores/actions.js45
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue2
-rw-r--r--app/assets/javascripts/pages/dashboard/merge_requests/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js2
-rw-r--r--app/assets/javascripts/pages/groups/registry/repositories/index.js2
-rw-r--r--app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js (renamed from app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js)0
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js7
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/show/index.js (renamed from app/assets/javascripts/pages/projects/pages_domains/edit/index.js)0
-rw-r--r--app/assets/javascripts/pages/projects/project.js24
-rw-r--r--app/assets/javascripts/pages/projects/registry/repositories/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/snippets/show/index.js15
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue2
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js2
-rw-r--r--app/assets/javascripts/pages/snippets/show/index.js15
-rw-r--r--app/assets/javascripts/pages/users/index.js2
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue17
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue14
-rw-r--r--app/assets/javascripts/performance_bar/components/request_warning.vue2
-rw-r--r--app/assets/javascripts/performance_bar/index.js46
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js29
-rw-r--r--app/assets/javascripts/persistent_user_callout.js8
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_stop_modal.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.vue6
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/utils.js2
-rw-r--r--app/assets/javascripts/project_find_file.js5
-rw-r--r--app/assets/javascripts/projects/project_new.js20
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/registry/list/components/app.vue (renamed from app/assets/javascripts/registry/components/app.vue)2
-rw-r--r--app/assets/javascripts/registry/list/components/collapsible_container.vue (renamed from app/assets/javascripts/registry/components/collapsible_container.vue)9
-rw-r--r--app/assets/javascripts/registry/list/components/group_empty_state.vue (renamed from app/assets/javascripts/registry/components/group_empty_state.vue)0
-rw-r--r--app/assets/javascripts/registry/list/components/project_empty_state.vue (renamed from app/assets/javascripts/registry/components/project_empty_state.vue)0
-rw-r--r--app/assets/javascripts/registry/list/components/table_registry.vue (renamed from app/assets/javascripts/registry/components/table_registry.vue)9
-rw-r--r--app/assets/javascripts/registry/list/constants.js (renamed from app/assets/javascripts/registry/constants.js)2
-rw-r--r--app/assets/javascripts/registry/list/index.js (renamed from app/assets/javascripts/registry/index.js)2
-rw-r--r--app/assets/javascripts/registry/list/stores/actions.js (renamed from app/assets/javascripts/registry/stores/actions.js)0
-rw-r--r--app/assets/javascripts/registry/list/stores/getters.js (renamed from app/assets/javascripts/registry/stores/getters.js)0
-rw-r--r--app/assets/javascripts/registry/list/stores/index.js (renamed from app/assets/javascripts/registry/stores/index.js)0
-rw-r--r--app/assets/javascripts/registry/list/stores/mutation_types.js (renamed from app/assets/javascripts/registry/stores/mutation_types.js)0
-rw-r--r--app/assets/javascripts/registry/list/stores/mutations.js (renamed from app/assets/javascripts/registry/stores/mutations.js)0
-rw-r--r--app/assets/javascripts/registry/list/stores/state.js (renamed from app/assets/javascripts/registry/stores/state.js)0
-rw-r--r--app/assets/javascripts/registry/settings/components/registry_settings_app.vue43
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js24
-rw-r--r--app/assets/javascripts/registry/settings/stores/actions.js6
-rw-r--r--app/assets/javascripts/registry/settings/stores/index.js16
-rw-r--r--app/assets/javascripts/registry/settings/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/registry/settings/stores/mutations.js8
-rw-r--r--app/assets/javascripts/registry/settings/stores/state.js10
-rw-r--r--app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue2
-rw-r--r--app/assets/javascripts/releases/list/components/app.vue20
-rw-r--r--app/assets/javascripts/releases/list/components/evidence_block.vue76
-rw-r--r--app/assets/javascripts/releases/list/components/release_block.vue62
-rw-r--r--app/assets/javascripts/releases/list/components/release_block_footer.vue4
-rw-r--r--app/assets/javascripts/releases/list/components/release_block_milestone_info.vue136
-rw-r--r--app/assets/javascripts/releases/list/constants.js7
-rw-r--r--app/assets/javascripts/releases/list/store/actions.js13
-rw-r--r--app/assets/javascripts/releases/list/store/mutations.js6
-rw-r--r--app/assets/javascripts/releases/list/store/state.js1
-rw-r--r--app/assets/javascripts/reports/components/report_item.vue2
-rw-r--r--app/assets/javascripts/reports/components/summary_row.vue2
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue2
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue12
-rw-r--r--app/assets/javascripts/repository/index.js8
-rw-r--r--app/assets/javascripts/repository/log_tree.js10
-rw-r--r--app/assets/javascripts/repository/utils/commit.js3
-rw-r--r--app/assets/javascripts/repository/utils/dom.js9
-rw-r--r--app/assets/javascripts/serverless/components/area.vue2
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue17
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue13
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js2
-rw-r--r--app/assets/javascripts/snippet/snippet_embed.js27
-rw-r--r--app/assets/javascripts/snippets/components/app.vue50
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue241
-rw-r--r--app/assets/javascripts/snippets/fragments/author.fragment.graphql8
-rw-r--r--app/assets/javascripts/snippets/fragments/project.fragment.graphql6
-rw-r--r--app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql13
-rw-r--r--app/assets/javascripts/snippets/index.js34
-rw-r--r--app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql5
-rw-r--r--app/assets/javascripts/snippets/queries/projectPermissions.query.graphql7
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.query.graphql15
-rw-r--r--app/assets/javascripts/snippets/queries/userPermissions.query.graphql7
-rw-r--r--app/assets/javascripts/tracking.js27
-rw-r--r--app/assets/javascripts/user_popovers.js1
-rw-r--r--app/assets/javascripts/users_select.js138
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue245
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue108
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue98
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_stop_button.vue83
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue99
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue)18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/loading.vue29
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue51
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue77
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/bar_chart.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_countdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/memory_graph.vue129
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue138
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar/image.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue2
-rw-r--r--app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js6
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js2
-rw-r--r--app/assets/stylesheets/components/release_block_milestone_info.scss6
-rw-r--r--app/assets/stylesheets/components/toast.scss52
-rw-r--r--app/assets/stylesheets/framework/common.scss8
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/framework/files.scss9
-rw-r--r--app/assets/stylesheets/framework/filters.scss2
-rw-r--r--app/assets/stylesheets/framework/header.scss5
-rw-r--r--app/assets/stylesheets/framework/lists.scss5
-rw-r--r--app/assets/stylesheets/framework/memory_graph.scss18
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss13
-rw-r--r--app/assets/stylesheets/framework/wells.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss14
-rw-r--r--app/assets/stylesheets/pages/boards.scss4
-rw-r--r--app/assets/stylesheets/pages/dev_ops_score.scss (renamed from app/assets/stylesheets/pages/convdev_index.scss)36
-rw-r--r--app/assets/stylesheets/pages/diff.scss2
-rw-r--r--app/assets/stylesheets/pages/editor.scss3
-rw-r--r--app/assets/stylesheets/pages/environments.scss1
-rw-r--r--app/assets/stylesheets/pages/error_details.scss6
-rw-r--r--app/assets/stylesheets/pages/error_tracking_list.scss5
-rw-r--r--app/assets/stylesheets/pages/groups.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss2
-rw-r--r--app/assets/stylesheets/pages/issues.scss49
-rw-r--r--app/assets/stylesheets/pages/issues/issue_count_badge.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss5
-rw-r--r--app/assets/stylesheets/pages/profile.scss141
-rw-r--r--app/assets/stylesheets/pages/projects.scss6
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss1
-rw-r--r--app/assets/stylesheets/pages/tree.scss6
-rw-r--r--app/assets/stylesheets/utilities.scss20
360 files changed, 5600 insertions, 3254 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index aee9990bc0b..071ae8ca8cf 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,6 +5,8 @@ import { joinPaths } from './lib/utils/url_utility';
import flash from '~/flash';
import { __ } from '~/locale';
+const DEFAULT_PER_PAGE = 20;
+
const Api = {
groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id',
@@ -41,7 +43,7 @@ const Api = {
releasesPath: '/api/:version/projects/:id/releases',
releasePath: '/api/:version/projects/:id/releases/:tag_name',
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
- adminStatisticsPath: 'api/:version/application/statistics',
+ adminStatisticsPath: '/api/:version/application/statistics',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -66,7 +68,7 @@ const Api = {
params: Object.assign(
{
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
},
options,
),
@@ -90,7 +92,7 @@ const Api = {
.get(url, {
params: {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
},
})
.then(({ data }) => callback(data));
@@ -101,7 +103,7 @@ const Api = {
const url = Api.buildUrl(Api.projectsPath);
const defaults = {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
simple: true,
};
@@ -126,7 +128,7 @@ const Api = {
.get(url, {
params: {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
...options,
},
})
@@ -235,7 +237,7 @@ const Api = {
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
const defaults = {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
};
return axios
.get(url, {
@@ -325,7 +327,7 @@ const Api = {
params: Object.assign(
{
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
},
options,
),
@@ -355,7 +357,7 @@ const Api = {
const url = Api.buildUrl(Api.userProjectsPath).replace(':id', userId);
const defaults = {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
};
return axios
.get(url, {
@@ -371,7 +373,7 @@ const Api = {
return axios.get(url, {
params: {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
...options,
},
});
@@ -403,10 +405,15 @@ const Api = {
return axios.post(url);
},
- releases(id) {
+ releases(id, options = {}) {
const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(id));
- return axios.get(url);
+ return axios.get(url, {
+ params: {
+ per_page: DEFAULT_PER_PAGE,
+ ...options,
+ },
+ });
},
release(projectPath, tagName) {
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 7652b67ae1e..07d79ea1c70 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,9 +1,9 @@
-/* eslint-disable no-param-reassign, no-void, consistent-return */
+/* eslint-disable no-param-reassign, consistent-return */
import AccessorUtilities from './lib/utils/accessor';
export default class Autosave {
- constructor(field, key) {
+ constructor(field, key, fallbackKey) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
@@ -11,6 +11,7 @@ export default class Autosave {
key = key.join('/');
}
this.key = `autosave/${key}`;
+ this.fallbackKey = fallbackKey;
this.field.data('autosave', this);
this.restore();
this.field.on('input', () => this.save());
@@ -21,9 +22,12 @@ export default class Autosave {
if (!this.field.length) return;
const text = window.localStorage.getItem(this.key);
+ const fallbackText = window.localStorage.getItem(this.fallbackKey);
- if ((text != null ? text.length : void 0) > 0) {
+ if (text) {
this.field.val(text);
+ } else if (fallbackText) {
+ this.field.val(fallbackText);
}
this.field.trigger('input');
@@ -41,7 +45,10 @@ export default class Autosave {
const text = this.field.val();
- if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) {
+ if (this.isLocalStorageAvailable && text) {
+ if (this.fallbackKey) {
+ window.localStorage.setItem(this.fallbackKey, text);
+ }
return window.localStorage.setItem(this.key, text);
}
@@ -51,6 +58,7 @@ export default class Autosave {
reset() {
if (!this.isLocalStorageAvailable) return;
+ window.localStorage.removeItem(this.fallbackKey);
return window.localStorage.removeItem(this.key);
}
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index eb720f5380b..00c0334db77 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -1,6 +1,6 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
export default {
// name: 'Badge' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
@@ -14,6 +14,11 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
imageUrl: {
type: String,
required: true,
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index df74eb2c2f7..19668d7e232 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -1,10 +1,10 @@
<script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
+import { GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
@@ -16,6 +16,8 @@ export default {
Badge,
LoadingButton,
GlLoadingIcon,
+ GlFormInput,
+ GlFormGroup,
},
props: {
isEditing: {
@@ -64,6 +66,18 @@ export default {
renderedLinkUrl() {
return this.renderedBadge ? this.renderedBadge.renderedLinkUrl : '';
},
+ name: {
+ get() {
+ return this.badge ? this.badge.name : '';
+ },
+ set(name) {
+ const badge = this.badge || createEmptyBadge();
+ this.updateBadgeInForm({
+ ...badge,
+ name,
+ });
+ },
+ },
imageUrl: {
get() {
return this.badge ? this.badge.imageUrl : '';
@@ -154,6 +168,10 @@ export default {
novalidate
@submit.prevent.stop="onSubmit"
>
+ <gl-form-group :label="s__('Badges|Name')" label-for="badge-name">
+ <gl-form-input id="badge-name" v-model="name" />
+ </gl-form-group>
+
<div class="form-group">
<label for="badge-link-url" class="label-bold">{{ s__('Badges|Link') }}</label>
<p v-html="helpText"></p>
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index cad5611c8c5..bb363b8d85e 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapState } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
@@ -43,13 +43,14 @@ export default {
<badge
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
- class="table-section section-40"
+ class="table-section section-30"
/>
- <span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span>
- <div class="table-section section-15">
+ <div class="table-section section-30">
+ <label class="label-bold str-truncated mb-0">{{ badge.name }}</label>
<span class="badge badge-pill">{{ badgeKindText }}</span>
</div>
- <div class="table-section section-15 table-button-footer">
+ <span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span>
+ <div class="table-section section-10 table-button-footer">
<div v-if="canEditBadge" class="table-action-buttons">
<button
:disabled="badge.isDeleting"
diff --git a/app/assets/javascripts/badges/empty_badge.js b/app/assets/javascripts/badges/empty_badge.js
index 49a9b5e1be8..527f233bb33 100644
--- a/app/assets/javascripts/badges/empty_badge.js
+++ b/app/assets/javascripts/badges/empty_badge.js
@@ -1,4 +1,5 @@
export default () => ({
+ name: '',
imageUrl: '',
isDeleting: false,
linkUrl: '',
diff --git a/app/assets/javascripts/badges/store/actions.js b/app/assets/javascripts/badges/store/actions.js
index 5542278b3e0..806c2423e7e 100644
--- a/app/assets/javascripts/badges/store/actions.js
+++ b/app/assets/javascripts/badges/store/actions.js
@@ -1,13 +1,9 @@
import axios from '~/lib/utils/axios_utils';
import types from './mutation_types';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
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,
+ ...convertObjectPropsToCamelCase(badge, true),
isDeleting: false,
});
@@ -27,6 +23,7 @@ export default {
dispatch('requestNewBadge');
return axios
.post(endpoint, {
+ name: newBadge.name,
image_url: newBadge.imageUrl,
link_url: newBadge.linkUrl,
})
@@ -141,6 +138,7 @@ export default {
dispatch('requestUpdatedBadge');
return axios
.put(endpoint, {
+ name: badge.name,
image_url: badge.imageUrl,
link_url: badge.linkUrl,
})
diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
index b7200150925..6bbd2133344 100644
--- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
+++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { parseBoolean } from '~/lib/utils/common_utils';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
+import { parseBoolean } from '~/lib/utils/common_utils';
export default function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/image.js b/app/assets/javascripts/behaviors/markdown/nodes/image.js
index c225a5ed876..e839396330e 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/image.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/image.js
@@ -1,8 +1,8 @@
/* eslint-disable class-methods-use-this */
import { Image as BaseImage } from 'tiptap-extensions';
-import { placeholderImage } from '~/lazy_loader';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { placeholderImage } from '~/lazy_loader';
export default class Image extends BaseImage {
get schema() {
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index aedd8004ea5..2df7a84ead0 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -1,6 +1,6 @@
+import $ from 'jquery';
import Api from '~/api';
-import $ from 'jquery';
import Flash from '../flash';
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
new file mode 100644
index 00000000000..a6f28de799f
--- /dev/null
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -0,0 +1,19 @@
+import { SwaggerUIBundle } from 'swagger-ui-dist';
+import flash from '~/flash';
+import { __ } from '~/locale';
+
+export default () => {
+ const el = document.getElementById('js-openapi-viewer');
+
+ Promise.all([import(/* webpackChunkName: 'openapi' */ 'swagger-ui-dist/swagger-ui.css')])
+ .then(() => {
+ SwaggerUIBundle({
+ url: el.dataset.endpoint,
+ dom_id: '#js-openapi-viewer',
+ });
+ })
+ .catch(error => {
+ flash(__('Something went wrong while initializing the OpenAPI viewer'));
+ throw error;
+ });
+};
diff --git a/app/assets/javascripts/blob/openapi_viewer.js b/app/assets/javascripts/blob/openapi_viewer.js
new file mode 100644
index 00000000000..0cacc33571f
--- /dev/null
+++ b/app/assets/javascripts/blob/openapi_viewer.js
@@ -0,0 +1,3 @@
+import renderOpenApi from './openapi';
+
+export default renderOpenApi;
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 07e4dde41d9..742404da46c 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -39,6 +39,9 @@ export default class BlobViewer {
case 'notebook':
initViewer(import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer'));
break;
+ case 'openapi':
+ initViewer(import(/* webpackChunkName: 'openapi_viewer' */ '../openapi_viewer'));
+ break;
case 'pdf':
initViewer(import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer'));
break;
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 58759fd1efe..8ebdfede8f7 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,6 +1,8 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
+import { GlButtonGroup, GlButton, GlTooltip } from '@gitlab/ui';
+import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits';
import { n__, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
@@ -8,8 +10,10 @@ import AccessorUtilities from '../../lib/utils/accessor';
import BoardBlankState from './board_blank_state.vue';
import BoardDelete from './board_delete';
import BoardList from './board_list.vue';
+import IssueCount from './issue_count.vue';
import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
+import { ListType } from '../constants';
export default Vue.extend({
components: {
@@ -17,10 +21,15 @@ export default Vue.extend({
BoardDelete,
BoardList,
Icon,
+ GlButtonGroup,
+ IssueCount,
+ GlButton,
+ GlTooltip,
},
directives: {
Tooltip,
},
+ mixins: [isWipLimitsOn],
props: {
list: {
type: Object,
@@ -53,6 +62,11 @@ export default Vue.extend({
isLoggedIn() {
return Boolean(gon.current_user_id);
},
+ showListHeaderButton() {
+ return (
+ !this.disabled && this.list.type !== ListType.closed && this.list.type !== ListType.blank
+ );
+ },
counterTooltip() {
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
@@ -61,11 +75,19 @@ export default Vue.extend({
return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
},
isNewIssueShown() {
+ return this.list.type === ListType.backlog || this.showListHeaderButton;
+ },
+ isSettingsShown() {
return (
- this.list.type === 'backlog' ||
- (!this.disabled && this.list.type !== 'closed' && this.list.type !== 'blank')
+ this.list.type !== ListType.backlog &&
+ this.showListHeaderButton &&
+ this.list.isExpanded &&
+ this.isWipLimitsOn
);
},
+ showBoardListAndBoardInfo() {
+ return this.list.type !== ListType.blank && this.list.type !== ListType.promotion;
+ },
uniqueKey() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index 9a1da810ad0..afdf0290e8e 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -1,7 +1,7 @@
<script>
+import Cookies from 'js-cookie';
import { __ } from '~/locale';
import ListLabel from '~/boards/models/label';
-import Cookies from 'js-cookie';
import boardsStore from '../stores/boards_store';
export default {
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index b8439bc8741..1e54d4d6b7d 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -71,6 +71,9 @@ export default {
total: this.list.issuesSize,
});
},
+ issuesSizeExceedsMax() {
+ return this.list.maxIssueCount > 0 && this.list.issuesSize > this.list.maxIssueCount;
+ },
},
watch: {
filters: {
@@ -435,7 +438,7 @@ export default {
ref="list"
:data-board="list.id"
:data-board-type="list.type"
- :class="{ 'is-smaller': showIssueForm }"
+ :class="{ 'is-smaller': showIssueForm, 'bg-danger-100': issuesSizeExceedsMax }"
class="board-list w-100 h-100 list-unstyled mb-0 p-1 js-board-list"
>
<board-card
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 32491dfbcb6..5d7be0c705a 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -315,8 +315,7 @@ export default {
<gl-dropdown-item
v-if="showDelete"
- class="text-danger"
- data-qa-selector="delete_board_button"
+ class="text-danger js-delete-board"
@click.prevent="showPage('delete')"
>
{{ s__('IssueBoards|Delete board') }}
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index d37e49bab46..7f7510545c6 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -2,10 +2,10 @@
import _ from 'underscore';
import { mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
+import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
-import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
diff --git a/app/assets/javascripts/boards/components/issue_count.vue b/app/assets/javascripts/boards/components/issue_count.vue
new file mode 100644
index 00000000000..c50a3c1c0d3
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_count.vue
@@ -0,0 +1,36 @@
+<script>
+export default {
+ name: 'IssueCount',
+ props: {
+ maxIssueCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ issuesSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ computed: {
+ isMaxLimitSet() {
+ return this.maxIssueCount !== 0;
+ },
+ issuesExceedMax() {
+ return this.isMaxLimitSet && this.issuesSize > this.maxIssueCount;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issue-count">
+ <span class="js-issue-size" :class="{ 'text-danger': issuesExceedMax }">
+ {{ issuesSize }}
+ </span>
+ <span v-if="isMaxLimitSet" class="js-max-issue-size">
+ {{ maxIssueCount }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index 3bc7f13a9e6..a32ebdab5e1 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -35,10 +35,10 @@ export default {
title() {
const timeago = getTimeago();
const { timeDifference, standardDateFormat } = this;
- const formatedDate = standardDateFormat;
+ const formattedDate = standardDateFormat;
if (timeDifference >= -1 && timeDifference < 7) {
- return `${timeago.format(this.issueDueDate)} (${formatedDate})`;
+ return `${timeago.format(this.issueDueDate)} (${formattedDate})`;
}
return timeago.format(this.issueDueDate);
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 618c2ada1f8..20344b66140 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -1,5 +1,6 @@
<script>
/* global ListIssue */
+import { GlLoadingIcon } from '@gitlab/ui';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import boardsStore from '~/boards/stores/boards_store';
import ModalHeader from './header.vue';
@@ -7,7 +8,6 @@ import ModalList from './list.vue';
import ModalFooter from './footer.vue';
import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store';
-import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index e5ebb887ce0..4a50b1e2efc 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -1,9 +1,9 @@
<script>
-import { __ } from '~/locale';
import $ from 'jquery';
import _ from 'underscore';
-import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __ } from '~/locale';
import eventHub from '../eventhub';
import Api from '../../api';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 3c66c7a0660..dcecfe5e1bb 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -4,6 +4,8 @@ export const ListType = {
backlog: 'backlog',
closed: 'closed',
label: 'label',
+ promotion: 'promotion',
+ blank: 'blank',
};
export default {
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index e76e2341dfd..f1b481fc386 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,6 +1,22 @@
import $ from 'jquery';
import Vue from 'vue';
+import 'ee_else_ce/boards/models/issue';
+import 'ee_else_ce/boards/models/list';
+import Board from 'ee_else_ce/boards/components/board';
+import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
+import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
+import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
+import toggleFocusMode from 'ee_else_ce/boards/toggle_focus';
+import toggleLabels from 'ee_else_ce/boards/toggle_labels';
+import {
+ setPromotionState,
+ setWeigthFetchingState,
+ setEpicFetchingState,
+ getMilestoneTitle,
+ getBoardsModalData,
+} from 'ee_else_ce/boards/ee_functions';
+
import Flash from '~/flash';
import { __ } from '~/locale';
import './models/label';
@@ -9,35 +25,19 @@ import './models/assignee';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import eventHub from '~/boards/eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
-import 'ee_else_ce/boards/models/issue';
-import 'ee_else_ce/boards/models/list';
import '~/boards/models/milestone';
import '~/boards/models/project';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import ModalStore from '~/boards/stores/modal_store';
-import BoardService from 'ee_else_ce/boards/services/board_service';
import modalMixin from '~/boards/mixins/modal_mixins';
import '~/boards/filters/due_date_filters';
-import Board from 'ee_else_ce/boards/components/board';
-import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
-import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
} from '~/lib/utils/common_utils';
-import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
-import toggleFocusMode from 'ee_else_ce/boards/toggle_focus';
-import toggleLabels from 'ee_else_ce/boards/toggle_labels';
-import {
- setPromotionState,
- setWeigthFetchingState,
- setEpicFetchingState,
- getMilestoneTitle,
- getBoardsModalData,
-} from 'ee_else_ce/boards/ee_functions';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
let issueBoardsApp;
@@ -68,6 +68,8 @@ export default () => {
Board,
BoardSidebar,
BoardAddIssuesModal,
+ BoardSettingsSidebar: () =>
+ import('ee_component/boards/components/board_settings_sidebar.vue'),
},
store,
data: {
@@ -97,7 +99,6 @@ export default () => {
bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId,
});
- gl.boardService = new BoardService();
boardsStore.rootPath = this.boardsEndpoint;
eventHub.$on('updateTokens', this.updateTokens);
@@ -116,7 +117,7 @@ export default () => {
this.filterManager.setup();
boardsStore.disabled = this.disabled;
- gl.boardService
+ boardsStore
.all()
.then(res => res.data)
.then(lists => {
@@ -155,7 +156,8 @@ export default () => {
newIssue.setFetchingState('subscriptions', true);
setWeigthFetchingState(newIssue, true);
setEpicFetchingState(newIssue, true);
- BoardService.getIssueInfo(sidebarInfoEndpoint)
+ boardsStore
+ .getIssueInfo(sidebarInfoEndpoint)
.then(res => res.data)
.then(data => {
const {
@@ -166,6 +168,7 @@ export default () => {
humanTotalTimeSpent,
weight,
epic,
+ assignees,
} = convertObjectPropsToCamelCase(data);
newIssue.setFetchingState('subscriptions', false);
@@ -179,6 +182,7 @@ export default () => {
subscribed,
weight,
epic,
+ assignees,
});
})
.catch(() => {
@@ -211,7 +215,8 @@ export default () => {
const { issue } = boardsStore.detail;
if (issue.id === id && issue.toggleSubscriptionEndpoint) {
issue.setFetchingState('subscriptions', true);
- BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
+ boardsStore
+ .toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
.then(() => {
issue.setFetchingState('subscriptions', false);
issue.updateData({
diff --git a/app/assets/javascripts/boards/mixins/is_wip_limits.js b/app/assets/javascripts/boards/mixins/is_wip_limits.js
new file mode 100644
index 00000000000..f172179d3c7
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/is_wip_limits.js
@@ -0,0 +1,7 @@
+export default {
+ computed: {
+ isWipLimitsOn() {
+ return false;
+ },
+ },
+};
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index bb8c8e68297..b232fea0882 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,9 +1,9 @@
/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow */
+import ListIssue from 'ee_else_ce/boards/models/issue';
import { __ } from '~/locale';
import ListLabel from './label';
import ListAssignee from './assignee';
-import ListIssue from 'ee_else_ce/boards/models/issue';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import flash from '~/flash';
import boardsStore from '../stores/boards_store';
@@ -52,6 +52,9 @@ class List {
this.loadingMore = false;
this.issues = obj.issues || [];
this.issuesSize = obj.issuesSize ? obj.issuesSize : 0;
+ this.maxIssueCount = Object.hasOwnProperty.call(obj, 'max_issue_count')
+ ? obj.max_issue_count
+ : 0;
this.defaultAvatar = defaultAvatar;
if (obj.label) {
@@ -90,7 +93,7 @@ class List {
entityType = 'milestone_id';
}
- return gl.boardService
+ return boardsStore
.createList(entity.id, entityType)
.then(res => res.data)
.then(data => {
@@ -108,14 +111,14 @@ class List {
boardsStore.state.lists.splice(index, 1);
boardsStore.updateNewListDropdown(this.id);
- gl.boardService.destroyList(this.id).catch(() => {
+ boardsStore.destroyList(this.id).catch(() => {
// TODO: handle request error
});
}
update() {
const collapsed = !this.isExpanded;
- return gl.boardService.updateList(this.id, this.position, collapsed).catch(() => {
+ return boardsStore.updateList(this.id, this.position, collapsed).catch(() => {
// TODO: handle request error
});
}
@@ -144,7 +147,7 @@ class List {
this.loading = true;
}
- return gl.boardService
+ return boardsStore
.getIssuesForList(this.id, data)
.then(res => res.data)
.then(data => {
@@ -165,7 +168,7 @@ class List {
this.addIssue(issue, null, 0);
this.issuesSize += 1;
- return gl.boardService
+ return boardsStore
.newIssue(this.id, issue)
.then(res => res.data)
.then(data => this.onNewIssueResponse(issue, data));
@@ -273,7 +276,7 @@ class List {
this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issue);
- gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => {
+ boardsStore.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => {
// TODO: handle request error
});
}
@@ -284,7 +287,7 @@ class List {
});
this.issues.splice(newIndex, 0, ...issues);
- gl.boardService
+ boardsStore
.moveMultipleIssues({
ids: issues.map(issue => issue.id),
fromListId: null,
@@ -296,15 +299,13 @@ class List {
}
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
- gl.boardService
- .moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
- .catch(() => {
- // TODO: handle request error
- });
+ boardsStore.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId).catch(() => {
+ // TODO: handle request error
+ });
}
updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId) {
- gl.boardService
+ boardsStore
.moveMultipleIssues({
ids: issues.map(issue => issue.id),
fromListId: listFrom.id,
@@ -356,7 +357,7 @@ class List {
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
- gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
+ boardsStore.moveIssue(issue.id, null, null, null, moveBeforeId);
}
}
}
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
deleted file mode 100644
index 03369febb4a..00000000000
--- a/app/assets/javascripts/boards/services/board_service.js
+++ /dev/null
@@ -1,98 +0,0 @@
-/* eslint-disable class-methods-use-this */
-/**
- * This file is intended to be deleted.
- * The existing functions will removed one by one in favor of using the board store directly.
- * see https://gitlab.com/gitlab-org/gitlab-foss/issues/61621
- */
-
-import boardsStore from '~/boards/stores/boards_store';
-
-export default class BoardService {
- generateBoardsPath(id) {
- return boardsStore.generateBoardsPath(id);
- }
-
- generateIssuesPath(id) {
- return boardsStore.generateIssuesPath(id);
- }
-
- static generateIssuePath(boardId, id) {
- return boardsStore.generateIssuePath(boardId, id);
- }
-
- all() {
- return boardsStore.all();
- }
-
- generateDefaultLists() {
- return boardsStore.generateDefaultLists();
- }
-
- createList(entityId, entityType) {
- return boardsStore.createList(entityId, entityType);
- }
-
- updateList(id, position, collapsed) {
- return boardsStore.updateList(id, position, collapsed);
- }
-
- destroyList(id) {
- return boardsStore.destroyList(id);
- }
-
- getIssuesForList(id, filter = {}) {
- return boardsStore.getIssuesForList(id, filter);
- }
-
- moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
- return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId);
- }
-
- moveMultipleIssues({
- ids,
- fromListId = null,
- toListId = null,
- moveBeforeId = null,
- moveAfterId = null,
- }) {
- return boardsStore.moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId });
- }
-
- newIssue(id, issue) {
- return boardsStore.newIssue(id, issue);
- }
-
- getBacklog(data) {
- return boardsStore.getBacklog(data);
- }
-
- bulkUpdate(issueIds, extraData = {}) {
- return boardsStore.bulkUpdate(issueIds, extraData);
- }
-
- static getIssueInfo(endpoint) {
- return boardsStore.getIssueInfo(endpoint);
- }
-
- static toggleIssueSubscription(endpoint) {
- return boardsStore.toggleIssueSubscription(endpoint);
- }
-
- allBoards() {
- return boardsStore.allBoards();
- }
-
- recentBoards() {
- return boardsStore.recentBoards();
- }
-
- createBoard(board) {
- return boardsStore.createBoard(board);
- }
-
- deleteBoard({ id }) {
- return boardsStore.deleteBoard({ id });
- }
-}
-
-window.BoardService = BoardService;
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 24f44dc5629..731aea996fb 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -1,3 +1,4 @@
export default () => ({
isShowingLabels: true,
+ activeListId: 0,
});
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 75909dd9d20..d990d2677a8 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,7 +1,7 @@
import Visibility from 'visibilityjs';
import Vue from 'vue';
-import AccessorUtilities from '~/lib/utils/accessor';
import { GlToast } from '@gitlab/ui';
+import AccessorUtilities from '~/lib/utils/accessor';
import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
@@ -12,6 +12,7 @@ import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX, CROSSPLANE } from '
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
+import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
import setupToggleButtons from '../toggle_buttons';
import initProjectSelectDropdown from '~/project_select';
@@ -144,6 +145,8 @@ export default class Clusters {
() => this.handlePollError(),
);
}
+
+ this.initRemoveClusterActions();
}
initApplications(type) {
@@ -205,6 +208,25 @@ export default class Clusters {
});
}
+ initRemoveClusterActions() {
+ const el = document.querySelector('#js-cluster-remove-actions');
+ if (el && el.dataset) {
+ const { clusterName, clusterPath } = el.dataset;
+
+ this.removeClusterAction = new Vue({
+ el,
+ render(createElement) {
+ return createElement(RemoveClusterConfirmation, {
+ props: {
+ clusterName,
+ clusterPath,
+ },
+ });
+ },
+ });
+ }
+ }
+
handleClusterEnvironmentsSuccess(data) {
this.store.toggleFetchEnvironments(false);
this.store.updateEnvironments(data.data);
diff --git a/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue b/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue
index 966918ae636..6b99bb09504 100644
--- a/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue
+++ b/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue
@@ -1,6 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { s__ } from '../../locale';
export default {
@@ -8,7 +7,7 @@ export default {
components: {
GlDropdown,
GlDropdownItem,
- Icon,
+ GlIcon,
},
props: {
stacks: {
@@ -86,8 +85,9 @@ export default {
href="https://crossplane.io/docs/master/stacks-guide.html"
target="_blank"
rel="noopener noreferrer"
- >{{ __('Crossplane') }}</a
- >
+ >{{ __('Crossplane') }}
+ <gl-icon name="external-link" class="vertical-align-middle" />
+ </a>
</p>
</div>
</template>
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
index 25347b11b6c..66c8297cb75 100644
--- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -1,7 +1,7 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { APPLICATION_STATUS } from '~/clusters/constants';
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
new file mode 100644
index 00000000000..c31ba7ef14a
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -0,0 +1,168 @@
+<script>
+import _ from 'underscore';
+import SplitButton from '~/vue_shared/components/split_button.vue';
+import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+
+const splitButtonActionItems = [
+ {
+ title: s__('ClusterIntegration|Remove integration and resources'),
+ description: s__(
+ 'ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal',
+ ),
+ eventName: 'remove-cluster-and-cleanup',
+ },
+ {
+ title: s__('ClusterIntegration|Remove integration'),
+ description: s__(
+ 'ClusterIntegration|Removes cluster from project but keeps associated resources',
+ ),
+ eventName: 'remove-cluster',
+ },
+];
+
+export default {
+ splitButtonActionItems,
+ components: {
+ SplitButton,
+ GlModal,
+ GlButton,
+ GlFormInput,
+ },
+ props: {
+ clusterPath: {
+ type: String,
+ required: true,
+ },
+ clusterName: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ enteredClusterName: '',
+ confirmCleanup: false,
+ };
+ },
+ computed: {
+ csrfToken() {
+ return csrf.token;
+ },
+ modalTitle() {
+ return this.confirmCleanup
+ ? s__('ClusterIntegration|Remove integration and resources?')
+ : s__('ClusterIntegration|Remove integration?');
+ },
+ warningMessage() {
+ return this.confirmCleanup
+ ? s__(
+ 'ClusterIntegration|You are about to remove your cluster integration and all GitLab-created resources associated with this cluster.',
+ )
+ : s__('ClusterIntegration|You are about to remove your cluster integration.');
+ },
+ warningToBeRemoved() {
+ return s__(`ClusterIntegration|
+ This will permanently delete the following resources:
+ <ul>
+ <li>All installed applications and related resources</li>
+ <li>The <code>gitlab-managed-apps</code> namespace</li>
+ <li>Any project namespaces</li>
+ <li><code>clusterroles</code></li>
+ <li><code>clusterrolebindings</code></li>
+ </ul>
+ `);
+ },
+ confirmationTextLabel() {
+ return sprintf(
+ this.confirmCleanup
+ ? s__(
+ 'ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:',
+ )
+ : s__('ClusterIntegration|To remove your integration, type %{clusterName} to confirm:'),
+ {
+ clusterName: `<code>${_.escape(this.clusterName)}</code>`,
+ },
+ false,
+ );
+ },
+ canSubmit() {
+ return this.enteredClusterName === this.clusterName;
+ },
+ },
+ methods: {
+ handleClickRemoveCluster(cleanup = false) {
+ this.confirmCleanup = cleanup;
+ this.$refs.modal.show();
+ },
+ handleCancel() {
+ this.$refs.modal.hide();
+ this.enteredClusterName = '';
+ },
+ handleSubmit(cleanup = false) {
+ this.$refs.cleanup.name = cleanup === true ? 'cleanup' : 'no_cleanup';
+ this.$refs.form.submit();
+ this.enteredClusterName = '';
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <split-button
+ :action-items="$options.splitButtonActionItems"
+ menu-class="dropdown-menu-large"
+ variant="danger"
+ @remove-cluster="handleClickRemoveCluster(false)"
+ @remove-cluster-and-cleanup="handleClickRemoveCluster(true)"
+ />
+ <gl-modal
+ ref="modal"
+ size="lg"
+ modal-id="delete-cluster-modal"
+ :title="modalTitle"
+ kind="danger"
+ >
+ <template>
+ <p>{{ warningMessage }}</p>
+ <div v-if="confirmCleanup" v-html="warningToBeRemoved"></div>
+ <strong v-html="confirmationTextLabel"></strong>
+ <form ref="form" :action="clusterPath" method="post" class="append-bottom-20">
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <input ref="cleanup" type="hidden" name="cleanup" value="true" />
+ <gl-form-input
+ v-model="enteredClusterName"
+ autofocus
+ type="text"
+ name="confirm_cluster_name_input"
+ autocomplete="off"
+ />
+ </form>
+ <span v-if="confirmCleanup">{{
+ s__(
+ 'ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration.',
+ )
+ }}</span>
+ </template>
+ <template slot="modal-footer">
+ <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button>
+ <template v-if="confirmCleanup">
+ <gl-button :disabled="!canSubmit" variant="warning" @click="handleSubmit">{{
+ s__('ClusterIntegration|Remove integration')
+ }}</gl-button>
+ <gl-button :disabled="!canSubmit" variant="danger" @click="handleSubmit(true)">{{
+ s__('ClusterIntegration|Remove integration and resources')
+ }}</gl-button>
+ </template>
+ <template v-else>
+ <gl-button :disabled="!canSubmit" variant="danger" @click="handleSubmit">{{
+ s__('ClusterIntegration|Remove integration')
+ }}</gl-button>
+ </template>
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
index 125bcaacc1c..e33431d2ea1 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlModal } from '@gitlab/ui';
-import { sprintf, s__ } from '~/locale';
import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
+import { sprintf, s__ } from '~/locale';
import {
HELM,
INGRESS,
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 60c2059a876..a28e17f7a56 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, no-else-return, consistent-return, one-var, no-return-assign */
+/* eslint-disable func-names, no-else-return, consistent-return, one-var, no-return-assign */
import $ from 'jquery';
@@ -51,7 +51,7 @@ export default class ImageFile {
}
// eslint-disable-next-line class-methods-use-this
initDraggable($el, padding, callback) {
- var dragging = false;
+ let dragging = false;
const $body = $('body');
const $offsetEl = $el.parent();
const dragStart = function() {
@@ -88,14 +88,12 @@ export default class ImageFile {
}
static prepareFrames(view) {
- var maxHeight, maxWidth;
- maxWidth = 0;
- maxHeight = 0;
+ let maxWidth = 0;
+ let maxHeight = 0;
$('.frame', view)
.each((index, frame) => {
- var height, width;
- width = $(frame).width();
- height = $(frame).height();
+ const width = $(frame).width();
+ const height = $(frame).height();
maxWidth = width > maxWidth ? width : maxWidth;
return (maxHeight = height > maxHeight ? height : maxHeight);
})
@@ -110,8 +108,7 @@ export default class ImageFile {
'two-up': function() {
return $('.two-up.view .wrap', this.file).each((index, wrap) => {
$('img', wrap).each(function() {
- var currentWidth;
- currentWidth = $(this).width();
+ const currentWidth = $(this).width();
if (currentWidth > availWidth / 2) {
return $(this).width(availWidth / 2);
}
@@ -124,16 +121,14 @@ export default class ImageFile {
});
},
swipe() {
- var maxHeight, maxWidth;
- maxWidth = 0;
- maxHeight = 0;
+ let maxWidth = 0;
+ let maxHeight = 0;
return $('.swipe.view', this.file).each((index, view) => {
- var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding;
const ref = ImageFile.prepareFrames(view);
[maxWidth, maxHeight] = ref;
- $swipeFrame = $('.swipe-frame', view);
- $swipeWrap = $('.swipe-wrap', view);
- $swipeBar = $('.swipe-bar', view);
+ const $swipeFrame = $('.swipe-frame', view);
+ const $swipeWrap = $('.swipe-wrap', view);
+ const $swipeBar = $('.swipe-bar', view);
$swipeFrame.css({
width: maxWidth + 16,
@@ -148,7 +143,7 @@ export default class ImageFile {
left: 1,
});
- wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
+ const wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
this.initDraggable($swipeBar, wrapPadding, (e, left) => {
if (left > 0 && left < $swipeFrame.width() - wrapPadding * 2) {
@@ -159,19 +154,17 @@ export default class ImageFile {
});
},
'onion-skin': function() {
- var dragTrackWidth, maxHeight, maxWidth;
+ let maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
- dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
+ const dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
return $('.onion-skin.view', this.file).each((index, view) => {
- var $frame, $track, $dragger, $frameAdded, framePadding;
-
const ref = ImageFile.prepareFrames(view);
[maxWidth, maxHeight] = ref;
- $frame = $('.onion-skin-frame', view);
- $frameAdded = $('.frame.added', view);
- $track = $('.drag-track', view);
- $dragger = $('.dragger', $track);
+ const $frame = $('.onion-skin-frame', view);
+ const $frameAdded = $('.frame.added', view);
+ const $track = $('.drag-track', view);
+ const $dragger = $('.dragger', $track);
$frame.css({
width: maxWidth + 16,
@@ -186,10 +179,10 @@ export default class ImageFile {
});
$frameAdded.css('opacity', 1);
- framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
+ const framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
this.initDraggable($dragger, framePadding, (e, left) => {
- var opacity = left / dragTrackWidth;
+ const opacity = left / dragTrackWidth;
if (opacity >= 0 && opacity <= 1) {
$dragger.css('left', left);
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 7a6ad3dc771..dd300b8a307 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -4,6 +4,7 @@ import 'core-js/es/array/find';
import 'core-js/es/array/find-index';
import 'core-js/es/array/from';
import 'core-js/es/array/includes';
+import 'core-js/es/number/is-integer';
import 'core-js/es/object/assign';
import 'core-js/es/object/values';
import 'core-js/es/object/entries';
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index 4fa18b19556..f2853564f94 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -1,6 +1,6 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import { __, sprintf } from '../../locale';
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { __ } from '../../locale';
import createFlash from '../../flash';
import Api from '../../api';
import state from '../state';
@@ -9,6 +9,7 @@ import Dropdown from './dropdown.vue';
export default {
components: {
GlLink,
+ GlSprintf,
Dropdown,
},
props: {
@@ -38,15 +39,6 @@ export default {
selectedProject() {
return state.selectedProject;
},
- noForkText() {
- return sprintf(
- __(
- "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private.",
- ),
- { link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' },
- false,
- );
- },
},
mounted() {
this.fetchProjects();
@@ -123,8 +115,20 @@ export default {
}}
</template>
<template v-else>
- {{ __('No forks available to you.') }}<br />
- <span v-html="noForkText"></span>
+ {{ __('No forks are available to you.') }}<br />
+ <gl-sprintf
+ :message="
+ __(
+ `To protect this issue's confidentiality, %{forkLink} and set the fork's visibility to private.`,
+ )
+ "
+ >
+ <template #forkLink>
+ <a :href="newForkPath" target="_blank" class="help-link">{{
+ __('fork this project')
+ }}</a>
+ </template>
+ </gl-sprintf>
</template>
<gl-link
:href="helpPagePath"
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 1000c310e35..262d501bfba 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,39 +1,54 @@
import $ from 'jquery';
import { rstrip } from './lib/utils/common_utils';
-function openConfirmDangerModal($form, text) {
- const $input = $('.js-confirm-danger-input');
+function openConfirmDangerModal($form, $modal, text) {
+ const $input = $('.js-confirm-danger-input', $modal);
$input.val('');
- $('.js-confirm-text').text(text || '');
- $('#modal-confirm-danger').modal('show');
+ $('.js-confirm-text', $modal).text(text || '');
+ $modal.modal('show');
- const confirmTextMatch = $('.js-confirm-danger-match').text();
- const $submit = $('.js-confirm-danger-submit');
+ const confirmTextMatch = $('.js-confirm-danger-match', $modal).text();
+ const $submit = $('.js-confirm-danger-submit', $modal);
$submit.disable();
$input.focus();
- $('.js-confirm-danger-input')
- .off('input')
- .on('input', function handleInput() {
- const confirmText = rstrip($(this).val());
- if (confirmText === confirmTextMatch) {
- $submit.enable();
- } else {
- $submit.disable();
- }
- });
- $('.js-confirm-danger-submit')
+ $input.off('input').on('input', function handleInput() {
+ const confirmText = rstrip($(this).val());
+ if (confirmText === confirmTextMatch) {
+ $submit.enable();
+ } else {
+ $submit.disable();
+ }
+ });
+ $('.js-confirm-danger-submit', $modal)
.off('click')
.on('click', () => $form.submit());
}
+function getModal($btn) {
+ const $modal = $btn.prev('.modal');
+
+ if ($modal.length) {
+ return $modal;
+ }
+
+ return $('#modal-confirm-danger');
+}
+
export default function initConfirmDangerModal() {
$(document).on('click', '.js-confirm-danger', e => {
- e.preventDefault();
const $btn = $(e.target);
- const $form = $btn.closest('form');
- const text = $btn.data('confirmDangerMessage');
- openConfirmDangerModal($form, text);
+ const checkFieldName = $btn.data('checkFieldName');
+ const checkFieldCompareValue = $btn.data('checkCompareValue');
+ const checkFieldVal = parseInt($(`[name="${checkFieldName}"]`).val(), 10);
+
+ if (!checkFieldName || checkFieldVal < checkFieldCompareValue) {
+ e.preventDefault();
+ const $form = $btn.closest('form');
+ const $modal = getModal($btn);
+ const text = $btn.data('confirmDangerMessage');
+ openConfirmDangerModal($form, $modal, text);
+ }
});
}
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
index 7dd6b051cb4..fb7000ee9ed 100644
--- a/app/assets/javascripts/contributors/components/contributors.vue
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -1,9 +1,9 @@
<script>
-import { __ } from '~/locale';
import _ from 'underscore';
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { __ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility';
import { xAxisLabelFormatter, dateFormatter } from '../utils';
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
index e6893c14cda..2f7fcfcb755 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
@@ -1,8 +1,9 @@
<script>
+import $ from 'jquery';
+import { GlIcon } from '@gitlab/ui';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import { GlIcon } from '@gitlab/ui';
const toArray = value => [].concat(value);
const itemsProp = (items, prop) => items.map(item => item[prop]);
@@ -106,6 +107,7 @@ export default {
data() {
return {
searchQuery: '',
+ focusOnSearch: false,
};
},
computed: {
@@ -141,6 +143,18 @@ export default {
return itemsProp(this.selectedItems, this.valueProperty).join(', ');
},
},
+ mounted() {
+ $(this.$refs.dropdown)
+ .on('shown.bs.dropdown', () => {
+ this.focusOnSearch = true;
+ })
+ .on('hidden.bs.dropdown', () => {
+ this.focusOnSearch = false;
+ });
+ },
+ beforeDestroy() {
+ $(this.$refs.dropdown).off();
+ },
methods: {
getItemsOrEmptyList() {
return this.items || [];
@@ -170,7 +184,7 @@ export default {
<template>
<div>
- <div class="js-gcp-machine-type-dropdown dropdown">
+ <div ref="dropdown" class="dropdown">
<dropdown-hidden-input :name="fieldName" :value="selectedItemsValues" />
<dropdown-button
:class="{ 'border-danger': hasErrors }"
@@ -179,7 +193,11 @@ export default {
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
- <dropdown-search-input v-model="searchQuery" :placeholder-text="searchFieldPlaceholder" />
+ <dropdown-search-input
+ v-model="searchQuery"
+ :focused="focusOnSearch"
+ :placeholder-text="searchFieldPlaceholder"
+ />
<div class="dropdown-content">
<ul>
<li v-if="!results.length">
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index 57d5f4f541b..d04d0ff2a6d 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -1,8 +1,8 @@
<script>
import { createNamespacedHelpers, mapState, mapActions } from 'vuex';
-import { sprintf, s__ } from '~/locale';
import _ from 'underscore';
import { GlFormInput, GlFormCheckbox } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
import ClusterFormDropdown from './cluster_form_dropdown.vue';
import { KUBERNETES_VERSIONS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
@@ -22,10 +22,7 @@ const {
mapState: mapSecurityGroupsState,
mapActions: mapSecurityGroupsActions,
} = createNamespacedHelpers('securityGroups');
-const {
- mapState: mapInstanceTypesState,
- mapActions: mapInstanceTypesActions,
-} = createNamespacedHelpers('instanceTypes');
+const { mapState: mapInstanceTypesState } = createNamespacedHelpers('instanceTypes');
export default {
components: {
@@ -265,12 +262,10 @@ export default {
mounted() {
this.fetchRegions();
this.fetchRoles();
- this.fetchInstanceTypes();
},
methods: {
...mapActions([
'createCluster',
- 'signOut',
'setClusterName',
'setEnvironmentScope',
'setKubernetesVersion',
@@ -290,7 +285,6 @@ export default {
...mapRolesActions({ fetchRoles: 'fetchItems' }),
...mapKeyPairsActions({ fetchKeyPairs: 'fetchItems' }),
...mapSecurityGroupsActions({ fetchSecurityGroups: 'fetchItems' }),
- ...mapInstanceTypesActions({ fetchInstanceTypes: 'fetchItems' }),
setRegionAndFetchVpcsAndKeyPairs(region) {
this.setRegion({ region });
this.setVpc({ vpc: null });
@@ -316,11 +310,6 @@ export default {
{{ s__('ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster') }}
</h2>
<div class="mb-3" v-html="kubernetesIntegrationHelpText"></div>
- <div class="mb-3">
- <button class="btn btn-link js-sign-out" @click.prevent="signOut()">
- {{ s__('ClusterIntegration|Select a different AWS role') }}
- </button>
- </div>
<div class="form-group">
<label class="label-bold" for="eks-cluster-name">{{
s__('ClusterIntegration|Kubernetes cluster name')
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
index ab33e9fbc95..1dd4c468ae6 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
@@ -1,8 +1,8 @@
<script>
import { GlFormInput } from '@gitlab/ui';
-import { sprintf, s__, __ } from '~/locale';
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
+import { sprintf, s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
@@ -28,7 +28,7 @@ export default {
},
data() {
return {
- roleArn: '',
+ roleArn: this.$store.state.roleArn,
};
},
computed: {
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/index.js b/app/assets/javascripts/create_cluster/eks_cluster/index.js
index 27f859d8972..fb993a7aa59 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/index.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/index.js
@@ -12,20 +12,14 @@ export default el => {
kubernetesIntegrationHelpPath,
accountAndExternalIdsHelpPath,
createRoleArnHelpPath,
- getRolesPath,
- getRegionsPath,
- getKeyPairsPath,
- getVpcsPath,
- getSubnetsPath,
- getSecurityGroupsPath,
- getInstanceTypesPath,
externalId,
accountId,
+ instanceTypes,
hasCredentials,
createRolePath,
createClusterPath,
- signOutPath,
externalLinkIcon,
+ roleArn,
} = el.dataset;
return new Vue({
@@ -35,18 +29,10 @@ export default el => {
hasCredentials: parseBoolean(hasCredentials),
externalId,
accountId,
+ instanceTypes: JSON.parse(instanceTypes),
createRolePath,
createClusterPath,
- signOutPath,
- },
- apiPaths: {
- getRolesPath,
- getRegionsPath,
- getKeyPairsPath,
- getVpcsPath,
- getSubnetsPath,
- getSecurityGroupsPath,
- getInstanceTypesPath,
+ roleArn,
},
}),
components: {
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
index 21b87d525cf..601ff6f9adc 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
@@ -1,58 +1,98 @@
-import axios from '~/lib/utils/axios_utils';
-
-export default apiPaths => ({
- fetchRoles() {
- return axios
- .get(apiPaths.getRolesPath)
- .then(({ data: { roles } }) =>
- roles.map(({ role_name: name, arn: value }) => ({ name, value })),
- );
- },
- fetchKeyPairs({ region }) {
- return axios
- .get(apiPaths.getKeyPairsPath, { params: { region } })
- .then(({ data: { key_pairs: keyPairs } }) =>
- keyPairs.map(({ key_name }) => ({ name: key_name, value: key_name })),
- );
- },
- fetchRegions() {
- return axios.get(apiPaths.getRegionsPath).then(({ data: { regions } }) =>
- regions.map(({ region_name }) => ({
- name: region_name,
- value: region_name,
+import AWS from 'aws-sdk/global';
+import EC2 from 'aws-sdk/clients/ec2';
+import IAM from 'aws-sdk/clients/iam';
+
+const lookupVpcName = ({ Tags: tags, VpcId: id }) => {
+ const nameTag = tags.find(({ Key: key }) => key === 'Name');
+
+ return nameTag ? nameTag.Value : id;
+};
+
+export const DEFAULT_REGION = 'us-east-2';
+
+export const setAWSConfig = ({ awsCredentials }) => {
+ AWS.config = {
+ ...awsCredentials,
+ region: DEFAULT_REGION,
+ };
+};
+
+export const fetchRoles = () => {
+ const iam = new IAM();
+
+ return iam
+ .listRoles()
+ .promise()
+ .then(({ Roles: roles }) => roles.map(({ RoleName: name, Arn: value }) => ({ name, value })));
+};
+
+export const fetchRegions = () => {
+ const ec2 = new EC2();
+
+ return ec2
+ .describeRegions()
+ .promise()
+ .then(({ Regions: regions }) =>
+ regions.map(({ RegionName: name }) => ({
+ name,
+ value: name,
})),
);
- },
- fetchVpcs({ region }) {
- return axios.get(apiPaths.getVpcsPath, { params: { region } }).then(({ data: { vpcs } }) =>
- vpcs.map(({ vpc_id }) => ({
- value: vpc_id,
- name: vpc_id,
+};
+
+export const fetchKeyPairs = ({ region }) => {
+ const ec2 = new EC2({ region });
+
+ return ec2
+ .describeKeyPairs()
+ .promise()
+ .then(({ KeyPairs: keyPairs }) => keyPairs.map(({ KeyName: name }) => ({ name, value: name })));
+};
+
+export const fetchVpcs = ({ region }) => {
+ const ec2 = new EC2({ region });
+
+ return ec2
+ .describeVpcs()
+ .promise()
+ .then(({ Vpcs: vpcs }) =>
+ vpcs.map(vpc => ({
+ value: vpc.VpcId,
+ name: lookupVpcName(vpc),
})),
);
- },
- fetchSubnets({ vpc, region }) {
- return axios
- .get(apiPaths.getSubnetsPath, { params: { vpc_id: vpc, region } })
- .then(({ data: { subnets } }) =>
- subnets.map(({ subnet_id }) => ({ name: subnet_id, value: subnet_id })),
- );
- },
- fetchSecurityGroups({ vpc, region }) {
- return axios
- .get(apiPaths.getSecurityGroupsPath, { params: { vpc_id: vpc, region } })
- .then(({ data: { security_groups: securityGroups } }) =>
- securityGroups.map(({ group_name: name, group_id: value }) => ({ name, value })),
- );
- },
- fetchInstanceTypes() {
- return axios
- .get(apiPaths.getInstanceTypesPath)
- .then(({ data: { instance_types: instanceTypes } }) =>
- instanceTypes.map(({ instance_type_name }) => ({
- name: instance_type_name,
- value: instance_type_name,
- })),
- );
- },
-});
+};
+
+export const fetchSubnets = ({ vpc, region }) => {
+ const ec2 = new EC2({ region });
+
+ return ec2
+ .describeSubnets({
+ Filters: [
+ {
+ Name: 'vpc-id',
+ Values: [vpc],
+ },
+ ],
+ })
+ .promise()
+ .then(({ Subnets: subnets }) => subnets.map(({ SubnetId: id }) => ({ value: id, name: id })));
+};
+
+export const fetchSecurityGroups = ({ region, vpc }) => {
+ const ec2 = new EC2({ region });
+
+ return ec2
+ .describeSecurityGroups({
+ Filters: [
+ {
+ Name: 'vpc-id',
+ Values: [vpc],
+ },
+ ],
+ })
+ .promise()
+ .then(({ SecurityGroups: securityGroups }) =>
+ securityGroups.map(({ GroupName: name, GroupId: value }) => ({ name, value })),
+ );
+};
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
index 72f15263a8f..e96e6d6e4f8 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
@@ -1,6 +1,8 @@
import * as types from './mutation_types';
+import { setAWSConfig } from '../services/aws_services_facade';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const getErrorMessage = data => {
const errorKey = Object.keys(data)[0];
@@ -28,7 +30,7 @@ export const createRole = ({ dispatch, state: { createRolePath } }, payload) =>
role_arn: payload.roleArn,
role_external_id: payload.externalId,
})
- .then(() => dispatch('createRoleSuccess'))
+ .then(({ data }) => dispatch('createRoleSuccess', convertObjectPropsToCamelCase(data)))
.catch(error => dispatch('createRoleError', { error }));
};
@@ -36,7 +38,8 @@ export const requestCreateRole = ({ commit }) => {
commit(types.REQUEST_CREATE_ROLE);
};
-export const createRoleSuccess = ({ commit }) => {
+export const createRoleSuccess = ({ commit }, awsCredentials) => {
+ setAWSConfig({ awsCredentials });
commit(types.CREATE_ROLE_SUCCESS);
};
@@ -117,9 +120,3 @@ export const setInstanceType = ({ commit }, payload) => {
export const setNodeCount = ({ commit }, payload) => {
commit(types.SET_NODE_COUNT, payload);
};
-
-export const signOut = ({ commit, state: { signOutPath } }) =>
- axios
- .delete(signOutPath)
- .then(() => commit(types.SIGN_OUT))
- .catch(({ response: { data } }) => createFlash(getErrorMessage(data)));
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/cluster_dropdown/index.js b/app/assets/javascripts/create_cluster/eks_cluster/store/cluster_dropdown/index.js
index 07a5821c47d..0b19589215c 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/cluster_dropdown/index.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/cluster_dropdown/index.js
@@ -3,11 +3,11 @@ import actions from './actions';
import mutations from './mutations';
import state from './state';
-const createStore = fetchFn => ({
+const createStore = ({ fetchFn, initialState }) => ({
actions: actions(fetchFn),
getters,
mutations,
- state: state(),
+ state: Object.assign(state(), initialState || {}),
});
export default createStore;
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
index 5982fc8a2fd..09fd560240d 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
@@ -6,12 +6,17 @@ import state from './state';
import clusterDropdownStore from './cluster_dropdown';
-import awsServicesFactory from '../services/aws_services_facade';
+import {
+ fetchRoles,
+ fetchRegions,
+ fetchKeyPairs,
+ fetchVpcs,
+ fetchSubnets,
+ fetchSecurityGroups,
+} from '../services/aws_services_facade';
-const createStore = ({ initialState, apiPaths }) => {
- const awsServices = awsServicesFactory(apiPaths);
-
- return new Vuex.Store({
+const createStore = ({ initialState }) =>
+ new Vuex.Store({
actions,
getters,
mutations,
@@ -19,34 +24,33 @@ const createStore = ({ initialState, apiPaths }) => {
modules: {
roles: {
namespaced: true,
- ...clusterDropdownStore(awsServices.fetchRoles),
+ ...clusterDropdownStore({ fetchFn: fetchRoles }),
},
regions: {
namespaced: true,
- ...clusterDropdownStore(awsServices.fetchRegions),
+ ...clusterDropdownStore({ fetchFn: fetchRegions }),
},
keyPairs: {
namespaced: true,
- ...clusterDropdownStore(awsServices.fetchKeyPairs),
+ ...clusterDropdownStore({ fetchFn: fetchKeyPairs }),
},
vpcs: {
namespaced: true,
- ...clusterDropdownStore(awsServices.fetchVpcs),
+ ...clusterDropdownStore({ fetchFn: fetchVpcs }),
},
subnets: {
namespaced: true,
- ...clusterDropdownStore(awsServices.fetchSubnets),
+ ...clusterDropdownStore({ fetchFn: fetchSubnets }),
},
securityGroups: {
namespaced: true,
- ...clusterDropdownStore(awsServices.fetchSecurityGroups),
+ ...clusterDropdownStore({ fetchFn: fetchSecurityGroups }),
},
instanceTypes: {
namespaced: true,
- ...clusterDropdownStore(awsServices.fetchInstanceTypes),
+ ...clusterDropdownStore({ initialState: { items: initialState.instanceTypes } }),
},
},
});
-};
export default createStore;
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
index f9204cc2207..9dee6abae5f 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
@@ -13,7 +13,6 @@ export const SET_GITLAB_MANAGED_CLUSTER = 'SET_GITLAB_MANAGED_CLUSTER';
export const REQUEST_CREATE_ROLE = 'REQUEST_CREATE_ROLE';
export const CREATE_ROLE_SUCCESS = 'CREATE_ROLE_SUCCESS';
export const CREATE_ROLE_ERROR = 'CREATE_ROLE_ERROR';
-export const SIGN_OUT = 'SIGN_OUT';
export const REQUEST_CREATE_CLUSTER = 'REQUEST_CREATE_CLUSTER';
export const CREATE_CLUSTER_SUCCESS = 'CREATE_CLUSTER_SUCCESS';
export const CREATE_CLUSTER_ERROR = 'CREATE_CLUSTER_ERROR';
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
index aa04c8f7079..c331d27d255 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
@@ -60,7 +60,4 @@ export default {
state.isCreatingCluster = false;
state.createClusterError = error;
},
- [types.SIGN_OUT](state) {
- state.hasCredentials = false;
- },
};
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
index 2e3a05a9187..20434dcce98 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
@@ -12,6 +12,8 @@ export default () => ({
accountId: '',
externalId: '',
+ roleArn: '',
+
clusterName: '',
environmentScope: '*',
kubernetesVersion,
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js
index 5a3407693e5..43fd0cac3be 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js
@@ -1,8 +1,8 @@
import _ from 'underscore';
+import { GlLoadingIcon } from '@gitlab/ui';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
import store from '../store';
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue
index 83811ab489a..a9d9f0224e3 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue
@@ -1,6 +1,6 @@
<script>
-import { sprintf, s__ } from '~/locale';
import { mapState, mapGetters, mapActions } from 'vuex';
+import { sprintf, s__ } from '~/locale';
import gkeDropdownMixin from './gke_dropdown_mixin';
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
index a2eb79af4f9..6815d3629e3 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import _ from 'underscore';
-import { s__, sprintf } from '~/locale';
import { mapState, mapGetters, mapActions } from 'vuex';
+import { s__, sprintf } from '~/locale';
import gkeDropdownMixin from './gke_dropdown_mixin';
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue
index fd5d5f86401..b60a5be2e63 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue
@@ -1,6 +1,6 @@
<script>
-import { sprintf, s__ } from '~/locale';
import { mapState, mapActions } from 'vuex';
+import { sprintf, s__ } from '~/locale';
import gkeDropdownMixin from './gke_dropdown_mixin';
diff --git a/app/assets/javascripts/create_cluster/init_create_cluster.js b/app/assets/javascripts/create_cluster/init_create_cluster.js
index 7c984582fd8..2b09771d772 100644
--- a/app/assets/javascripts/create_cluster/init_create_cluster.js
+++ b/app/assets/javascripts/create_cluster/init_create_cluster.js
@@ -6,7 +6,7 @@ const newClusterViews = [':clusters:new', ':clusters:create_gcp', ':clusters:cre
const isProjectLevelCluster = page => page.startsWith('project:clusters');
-export default (document, gon) => {
+export default document => {
const { page } = document.body.dataset;
const isNewClusterView = newClusterViews.some(view => page.endsWith(view));
@@ -19,17 +19,15 @@ export default (document, gon) => {
initGkeDropdowns();
- if (gon.features.createEksClusters) {
- import(/* webpackChunkName: 'eks_cluster' */ '~/create_cluster/eks_cluster')
- .then(({ default: initCreateEKSCluster }) => {
- const el = document.querySelector('.js-create-eks-cluster-form-container');
+ import(/* webpackChunkName: 'eks_cluster' */ '~/create_cluster/eks_cluster')
+ .then(({ default: initCreateEKSCluster }) => {
+ const el = document.querySelector('.js-create-eks-cluster-form-container');
- if (el) {
- initCreateEKSCluster(el);
- }
- })
- .catch(() => {});
- }
+ if (el) {
+ initCreateEKSCluster(el);
+ }
+ })
+ .catch(() => {});
if (isProjectLevelCluster(page)) {
initGkeNamespace();
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index dce9c1a5410..d9805e5e76a 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -311,6 +311,7 @@ export default class CreateMergeRequestDropdown {
}
onChangeInput(event) {
+ this.disable();
let target;
let value;
diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue
index e44588efbfc..ae8c430dcd6 100644
--- a/app/assets/javascripts/cycle_analytics/components/banner.vue
+++ b/app/assets/javascripts/cycle_analytics/components/banner.vue
@@ -1,6 +1,6 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
+import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 922c907bb36..048f3a2485c 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import Flash from '~/flash';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
@@ -6,7 +7,6 @@ import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
@@ -133,7 +133,7 @@ export default {
:keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
- class="qa-project-deploy-keys"
+ data-qa-selector="project_deploy_keys"
/>
</template>
</div>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 6ffb8c4e1c0..4d36a492c1c 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -159,7 +159,7 @@ export default {
<div role="rowheader" class="table-mobile-header">{{ __('Created') }}</div>
<div class="table-mobile-content text-secondary key-created-at">
<span v-tooltip :title="tooltipTitle(deployKey.created_at)">
- <icon name="calendar" /> <span>{{ timeFormated(deployKey.created_at) }}</span>
+ <icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span>
</span>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 19b85710710..8ea443814e9 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -1,11 +1,12 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Mousetrap from 'mousetrap';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import createFlash from '~/flash';
-import { GlLoadingIcon } from '@gitlab/ui';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
-import Mousetrap from 'mousetrap';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
@@ -36,11 +37,20 @@ export default {
GlLoadingIcon,
PanelResizer,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
endpoint: {
type: String,
required: true,
},
+ endpointMetadata: {
+ type: String,
+ required: true,
+ },
+ endpointBatch: {
+ type: String,
+ required: true,
+ },
projectPath: {
type: String,
required: true,
@@ -92,6 +102,7 @@ export default {
computed: {
...mapState({
isLoading: state => state.diffs.isLoading,
+ isBatchLoading: state => state.diffs.isBatchLoading,
diffFiles: state => state.diffs.diffFiles,
diffViewType: state => state.diffs.diffViewType,
mergeRequestDiffs: state => state.diffs.mergeRequestDiffs,
@@ -133,6 +144,9 @@ export default {
isLimitedContainer() {
return !this.showTreeList && !this.isParallelView && !this.isFluidLayout;
},
+ shouldSetDiscussions() {
+ return this.isNotesFetched && !this.assignedDiscussions && !this.isLoading;
+ },
},
watch: {
diffViewType() {
@@ -149,13 +163,21 @@ export default {
},
isLoading: 'adjustView',
showTreeList: 'adjustView',
+ shouldSetDiscussions(newVal) {
+ if (newVal) {
+ this.setDiscussions();
+ }
+ },
},
mounted() {
this.setBaseConfig({
endpoint: this.endpoint,
+ endpointMetadata: this.endpointMetadata,
+ endpointBatch: this.endpointBatch,
projectPath: this.projectPath,
dismissEndpoint: this.dismissEndpoint,
showSuggestPopover: this.showSuggestPopover,
+ useSingleDiffStyle: this.glFeatures.singleMrDiffView,
});
if (this.shouldShow) {
@@ -185,6 +207,8 @@ export default {
...mapActions('diffs', [
'setBaseConfig',
'fetchDiffFiles',
+ 'fetchDiffFilesMeta',
+ 'fetchDiffFilesBatch',
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
'setHighlightedRow',
@@ -196,31 +220,56 @@ export default {
this.assignedDiscussions = false;
this.fetchData(false);
},
+ startDiffRendering() {
+ requestIdleCallback(
+ () => {
+ this.startRenderDiffsQueue();
+ },
+ { timeout: 1000 },
+ );
+ },
fetchData(toggleTree = true) {
- this.fetchDiffFiles()
- .then(() => {
- if (toggleTree) {
- this.hideTreeListIfJustOneFile();
- }
+ if (this.glFeatures.diffsBatchLoad) {
+ this.fetchDiffFilesMeta()
+ .then(() => {
+ if (toggleTree) this.hideTreeListIfJustOneFile();
- requestIdleCallback(
- () => {
- this.setDiscussions();
- this.startRenderDiffsQueue();
- },
- { timeout: 1000 },
- );
- })
- .catch(() => {
- createFlash(__('Something went wrong on our end. Please try again!'));
- });
+ this.startDiffRendering();
+ })
+ .catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
+
+ this.fetchDiffFilesBatch()
+ .then(() => this.startDiffRendering())
+ .catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
+ } else {
+ this.fetchDiffFiles()
+ .then(() => {
+ if (toggleTree) {
+ this.hideTreeListIfJustOneFile();
+ }
+
+ requestIdleCallback(
+ () => {
+ this.startRenderDiffsQueue();
+ },
+ { timeout: 1000 },
+ );
+ })
+ .catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
+ }
if (!this.isNotesFetched) {
eventHub.$emit('fetchNotesData');
}
},
setDiscussions() {
- if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) {
+ if (this.shouldSetDiscussions) {
this.assignedDiscussions = true;
requestIdleCallback(
@@ -324,7 +373,8 @@ export default {
}"
>
<commit-widget v-if="commit" :commit="commit" />
- <template v-if="renderDiffFiles">
+ <div v-if="isBatchLoading" class="loading"><gl-loading-icon /></div>
+ <template v-else-if="renderDiffFiles">
<diff-file
v-for="file in diffFiles"
:key="file.newPath"
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 839ab542377..23fbfc2b74b 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -1,7 +1,7 @@
<script>
+import { mapState, mapActions } from 'vuex';
import createFlash from '~/flash';
import { s__ } from '~/locale';
-import { mapState, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { UNFOLD_COUNT } from '../constants';
import * as utils from '../store/utils';
@@ -226,7 +226,7 @@ export default {
<icon :size="12" name="expand-up" aria-hidden="true" />
</a>
<a class="mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()">
- <span>{{ s__('Diffs|Show all lines') }}</span>
+ <span>{{ s__('Diffs|Show unchanged lines') }}</span>
</a>
<a
v-if="canExpandDown"
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 9236f0d5349..0dbff4ffcec 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,9 +1,9 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
+import { GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
-import { GlLoadingIcon } from '@gitlab/ui';
import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 665328eb234..91d374eafc0 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -1,17 +1,17 @@
<script>
import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
-import { polyfillSticky, stickyMonitor } from '~/lib/utils/sticky';
+import { GlButton, GlTooltipDirective, GlTooltip, GlLoadingIcon } from '@gitlab/ui';
+import { polyfillSticky } from '~/lib/utils/sticky';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import { GlButton, GlTooltipDirective, GlTooltip, GlLoadingIcon } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants';
import EditButton from './edit_button.vue';
import DiffStats from './diff_stats.vue';
-import { scrollToElement, contentTop } from '~/lib/utils/common_utils';
+import { scrollToElement } from '~/lib/utils/common_utils';
export default {
components: {
@@ -127,8 +127,6 @@ export default {
},
mounted() {
polyfillSticky(this.$refs.header);
- const fileHeaderHeight = this.$refs.header.clientHeight;
- stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
},
methods: {
...mapActions('diffs', [
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 7ede7a4f430..be19d8520b5 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -1,9 +1,9 @@
<script>
+import { GlTooltipDirective } from '@gitlab/ui';
import { n__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import { truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import { GlTooltipDirective } from '@gitlab/ui';
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
export default {
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index 434d554d148..34aa15856d2 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -72,7 +72,7 @@ export default {
lineCode() {
return (
this.line.line_code ||
- (this.line.left && this.line.line.left.line_code) ||
+ (this.line.left && this.line.left.line_code) ||
(this.line.right && this.line.right.line_code)
);
},
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index da0cdbe467b..f81f50f8490 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -1,7 +1,7 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { s__ } from '~/locale';
import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
+import { s__ } from '~/locale';
import noteForm from '../../notes/components/note_form.vue';
import autosave from '../../notes/mixins/autosave';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index d84e1af11f3..7521f3c950a 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -57,3 +57,4 @@ export const MIN_RENDERING_MS = 2;
export const START_RENDERING_INDEX = 200;
export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines';
export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines';
+export const DIFFS_PER_PAGE = 20;
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index c9580e3d3b4..375ac80021f 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -67,6 +67,8 @@ export default function initDiffsApp(store) {
return {
endpoint: dataset.endpoint,
+ endpointMetadata: dataset.endpointMetadata || '',
+ endpointBatch: dataset.endpointBatch || '',
projectPath: dataset.projectPath,
helpPagePath: dataset.helpPagePath,
currentUser: JSON.parse(dataset.currentUserData) || {},
@@ -100,6 +102,8 @@ export default function initDiffsApp(store) {
return createElement('diffs-app', {
props: {
endpoint: this.endpoint,
+ endpointMetadata: this.endpointMetadata,
+ endpointBatch: this.endpointBatch,
currentUser: this.currentUser,
projectPath: this.projectPath,
helpPagePath: this.helpPagePath,
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 6695d9fe96c..992b45c97ac 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
+import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils';
@@ -13,6 +13,7 @@ import {
convertExpandLines,
idleCallback,
allDiscussionWrappersExpanded,
+ prepareDiffData,
} from './utils';
import * as types from './mutation_types';
import {
@@ -33,16 +34,36 @@ import {
START_RENDERING_INDEX,
INLINE_DIFF_LINES_KEY,
PARALLEL_DIFF_LINES_KEY,
+ DIFFS_PER_PAGE,
} from '../constants';
import { diffViewerModes } from '~/ide/constants';
export const setBaseConfig = ({ commit }, options) => {
- const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options;
- commit(types.SET_BASE_CONFIG, { endpoint, projectPath, dismissEndpoint, showSuggestPopover });
+ const {
+ endpoint,
+ endpointMetadata,
+ endpointBatch,
+ projectPath,
+ dismissEndpoint,
+ showSuggestPopover,
+ useSingleDiffStyle,
+ } = options;
+ commit(types.SET_BASE_CONFIG, {
+ endpoint,
+ endpointMetadata,
+ endpointBatch,
+ projectPath,
+ dismissEndpoint,
+ showSuggestPopover,
+ useSingleDiffStyle,
+ });
};
export const fetchDiffFiles = ({ state, commit }) => {
const worker = new TreeWorker();
+ const urlParams = {
+ w: state.showWhitespace ? '0' : '1',
+ };
commit(types.SET_LOADING, true);
@@ -53,9 +74,10 @@ export const fetchDiffFiles = ({ state, commit }) => {
});
return axios
- .get(mergeUrlParams({ w: state.showWhitespace ? '0' : '1' }, state.endpoint))
+ .get(mergeUrlParams(urlParams, state.endpoint))
.then(res => {
commit(types.SET_LOADING, false);
+
commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
commit(types.SET_DIFF_DATA, res.data);
@@ -67,6 +89,52 @@ export const fetchDiffFiles = ({ state, commit }) => {
.catch(() => worker.terminate());
};
+export const fetchDiffFilesBatch = ({ commit, state }) => {
+ commit(types.SET_BATCH_LOADING, true);
+
+ const getBatch = page =>
+ axios
+ .get(state.endpointBatch, {
+ params: { page, per_page: DIFFS_PER_PAGE, w: state.showWhitespace ? '0' : '1' },
+ })
+ .then(({ data: { pagination, diff_files } }) => {
+ commit(types.SET_DIFF_DATA_BATCH, { diff_files });
+ commit(types.SET_BATCH_LOADING, false);
+ return pagination.next_page;
+ })
+ .then(nextPage => nextPage && getBatch(nextPage));
+
+ return getBatch()
+ .then(handleLocationHash)
+ .catch(() => null);
+};
+
+export const fetchDiffFilesMeta = ({ commit, state }) => {
+ const worker = new TreeWorker();
+
+ commit(types.SET_LOADING, true);
+
+ worker.addEventListener('message', ({ data }) => {
+ commit(types.SET_TREE_DATA, data);
+
+ worker.terminate();
+ });
+
+ return axios
+ .get(state.endpointMetadata)
+ .then(({ data }) => {
+ const strippedData = { ...data };
+ delete strippedData.diff_files;
+ commit(types.SET_LOADING, false);
+ commit(types.SET_MERGE_REQUEST_DIFFS, data.merge_request_diffs || []);
+ commit(types.SET_DIFF_DATA, strippedData);
+
+ prepareDiffData(data);
+ worker.postMessage(data.diff_files);
+ })
+ .catch(() => worker.terminate());
+};
+
export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 6821c8445ea..7366c50752c 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -8,6 +8,7 @@ const defaultViewType = INLINE_DIFF_VIEW_TYPE;
export default () => ({
isLoading: true,
+ isBatchLoading: false,
addedLines: null,
removedLines: null,
endpoint: '',
@@ -30,4 +31,5 @@ export default () => ({
fileFinderVisible: false,
dismissEndpoint: '',
showSuggestPopover: true,
+ useSingleDiffStyle: false,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 9db56331faa..5a90d78b2bc 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -1,6 +1,8 @@
export const SET_BASE_CONFIG = 'SET_BASE_CONFIG';
export const SET_LOADING = 'SET_LOADING';
+export const SET_BATCH_LOADING = 'SET_BATCH_LOADING';
export const SET_DIFF_DATA = 'SET_DIFF_DATA';
+export const SET_DIFF_DATA_BATCH = 'SET_DIFF_DATA_BATCH';
export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';
export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS';
export const TOGGLE_LINE_HAS_FORM = 'TOGGLE_LINE_HAS_FORM';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index a6915a46c00..859f43b3b6d 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -12,22 +12,57 @@ import * as types from './mutation_types';
export default {
[types.SET_BASE_CONFIG](state, options) {
- const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options;
- Object.assign(state, { endpoint, projectPath, dismissEndpoint, showSuggestPopover });
+ const {
+ endpoint,
+ endpointMetadata,
+ endpointBatch,
+ projectPath,
+ dismissEndpoint,
+ showSuggestPopover,
+ useSingleDiffStyle,
+ } = options;
+ Object.assign(state, {
+ endpoint,
+ endpointMetadata,
+ endpointBatch,
+ projectPath,
+ dismissEndpoint,
+ showSuggestPopover,
+ useSingleDiffStyle,
+ });
},
[types.SET_LOADING](state, isLoading) {
Object.assign(state, { isLoading });
},
+ [types.SET_BATCH_LOADING](state, isBatchLoading) {
+ Object.assign(state, { isBatchLoading });
+ },
+
[types.SET_DIFF_DATA](state, data) {
- prepareDiffData(data);
+ if (
+ !(
+ gon &&
+ gon.features &&
+ gon.features.diffsBatchLoad &&
+ window.location.search.indexOf('diff_id') === -1
+ )
+ ) {
+ prepareDiffData(data);
+ }
Object.assign(state, {
...convertObjectPropsToCamelCase(data),
});
},
+ [types.SET_DIFF_DATA_BATCH](state, data) {
+ prepareDiffData(data);
+
+ state.diffFiles.push(...data.diff_files);
+ },
+
[types.RENDER_FILE](state, file) {
Object.assign(file, {
renderIt: true,
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index d46bdea9b50..281a0de1fc2 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -252,10 +252,11 @@ export function prepareDiffData(diffData) {
showingLines += file.parallel_diff_lines.length;
}
+ const name = (file.viewer && file.viewer.name) || diffViewerModes.text;
+
Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
- collapsed:
- file.viewer.name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED,
+ collapsed: name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED,
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [],
@@ -497,7 +498,7 @@ export const allDiscussionWrappersExpanded = diff => {
}
});
} else if (diff.highlighted_diff_lines) {
- diff.parallel_diff_lines.forEach(line => {
+ diff.highlighted_diff_lines.forEach(line => {
if (line.discussions.length) {
discussionsExpandedArray.push(line.discussionsExpanded);
}
diff --git a/app/assets/javascripts/emoji/no_emoji_validator.js b/app/assets/javascripts/emoji/no_emoji_validator.js
index 384d62a133a..edef868619a 100644
--- a/app/assets/javascripts/emoji/no_emoji_validator.js
+++ b/app/assets/javascripts/emoji/no_emoji_validator.js
@@ -1,5 +1,5 @@
-import { __ } from '~/locale';
import emojiRegex from 'emoji-regex';
+import { __ } from '~/locale';
import InputValidator from '../validators/input_validator';
export default class NoEmojiValidator extends InputValidator {
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 426bb63d4f7..cdf62259479 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import containerMixin from 'ee_else_ce/environments/mixins/container_mixin';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import EnvironmentTable from '../components/environments_table.vue';
export default {
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 22bba21526c..d2978422224 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,10 +1,10 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip';
-import { GlLoadingIcon } from '@gitlab/ui';
export default {
directives: {
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index c94039326aa..428dfe5fcf7 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,12 +1,13 @@
<script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
-import { __, sprintf } from '~/locale';
-import Timeago from 'timeago.js';
+import { format } from 'timeago.js';
import _ from 'underscore';
import { GlTooltipDirective } from '@gitlab/ui';
+import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import { __, sprintf } from '~/locale';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
@@ -22,11 +23,9 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
*
* Renders a table row for each environment.
*/
-const timeagoInstance = new Timeago();
export default {
components: {
- UserAvatarLink,
CommitComponent,
Icon,
ActionsComponent,
@@ -35,6 +34,8 @@ export default {
RollbackComponent,
TerminalButtonComponent,
MonitoringButtonComponent,
+ TooltipOnTruncate,
+ UserAvatarLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -42,16 +43,21 @@ export default {
mixins: [environmentItemMixin],
props: {
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
model: {
type: Object,
required: true,
default: () => ({}),
},
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
+ tableData: {
+ type: Object,
+ required: true,
},
},
@@ -121,7 +127,7 @@ export default {
*/
deployedDate() {
if (this.canShowDate) {
- return timeagoInstance.format(this.model.last_deployment.deployed_at);
+ return format(this.model.last_deployment.deployed_at);
}
return '';
},
@@ -446,9 +452,13 @@ export default {
class="gl-responsive-table-row"
role="row"
>
- <div class="table-section section-wrap section-15 text-truncate" role="gridcell">
+ <div
+ class="table-section section-wrap text-truncate"
+ :class="tableData.name.spacing"
+ role="gridcell"
+ >
<div v-if="!model.isFolder" class="table-mobile-header" role="rowheader">
- {{ s__('Environments|Environment') }}
+ {{ tableData.name.title }}
</div>
<span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard">
@@ -488,7 +498,8 @@ export default {
</div>
<div
- class="table-section section-10 deployment-column d-none d-sm-none d-md-block"
+ class="table-section deployment-column d-none d-sm-none d-md-block"
+ :class="tableData.deploy.spacing"
role="gridcell"
>
<span v-if="shouldRenderDeploymentID" class="text-break-word">
@@ -507,18 +518,32 @@ export default {
</span>
</div>
- <div class="table-section section-15 d-none d-sm-none d-md-block" role="gridcell">
- <a
- v-if="shouldRenderBuildName"
- :href="buildPath"
- class="build-link cgray flex-truncate-parent"
- >
- <span class="flex-truncate-child">{{ buildName }}</span>
+ <div
+ class="table-section d-none d-sm-none d-md-block"
+ :class="tableData.build.spacing"
+ role="gridcell"
+ >
+ <a v-if="shouldRenderBuildName" :href="buildPath" class="build-link cgray">
+ <tooltip-on-truncate
+ :title="buildName"
+ truncate-target="child"
+ class="flex-truncate-parent"
+ >
+ <span class="flex-truncate-child">
+ {{ buildName }}
+ </span>
+ </tooltip-on-truncate>
</a>
</div>
- <div v-if="!model.isFolder" class="table-section section-20" role="gridcell">
- <div role="rowheader" class="table-mobile-header">{{ s__('Environments|Commit') }}</div>
+ <div
+ v-if="!model.isFolder"
+ class="table-section"
+ :class="tableData.commit.spacing"
+ role="gridcell"
+ >
+ <div role="rowheader" class="table-mobile-header">{{ tableData.commit.title }}</div>
+
<div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
@@ -534,8 +559,14 @@ export default {
</div>
</div>
- <div v-if="!model.isFolder" class="table-section section-10" role="gridcell">
- <div role="rowheader" class="table-mobile-header">{{ s__('Environments|Updated') }}</div>
+ <div
+ v-if="!model.isFolder"
+ class="table-section"
+ :class="tableData.date.spacing"
+ role="gridcell"
+ >
+ <div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div>
+
<span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content">
{{ deployedDate }}
</span>
@@ -543,7 +574,8 @@ export default {
<div
v-if="!model.isFolder && displayEnvironmentActions"
- class="table-section section-30 table-button-footer"
+ class="table-section table-button-footer"
+ :class="tableData.actions.spacing"
role="gridcell"
>
<div class="btn-group table-action-buttons" role="group">
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 886490847ea..7b4b633dc7f 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -1,9 +1,9 @@
<script>
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
export default {
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 81927d18f8b..50c667e6966 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -31,10 +31,6 @@ export default {
type: Boolean,
required: true,
},
- cssContainerClass: {
- type: String,
- required: true,
- },
newEnvironmentPath: {
type: String,
required: true,
@@ -93,7 +89,7 @@ export default {
};
</script>
<template>
- <div :class="cssContainerClass">
+ <div>
<stop-environment-modal :environment="environmentInStopModal" />
<confirm-rollback-modal :environment="environmentInRollbackModal" />
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 4464f5e5578..453e7610e21 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -5,6 +5,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import _ from 'underscore';
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
+import { s__ } from '~/locale';
import EnvironmentItem from './environment_item.vue';
export default {
@@ -41,6 +42,34 @@ export default {
: env,
);
},
+ tableData() {
+ return {
+ // percent spacing for cols, should add up to 100
+ name: {
+ title: s__('Environments|Environment'),
+ spacing: 'section-15',
+ },
+ deploy: {
+ title: s__('Environments|Deployment'),
+ spacing: 'section-10',
+ },
+ build: {
+ title: s__('Environments|Job'),
+ spacing: 'section-15',
+ },
+ commit: {
+ title: s__('Environments|Commit'),
+ spacing: 'section-20',
+ },
+ date: {
+ title: s__('Environments|Updated'),
+ spacing: 'section-10',
+ },
+ actions: {
+ spacing: 'section-30',
+ },
+ };
+ },
},
methods: {
folderUrl(model) {
@@ -79,20 +108,20 @@ export default {
<template>
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
- <div class="table-section section-15 environments-name" role="columnheader">
- {{ s__('Environments|Environment') }}
+ <div class="table-section" :class="tableData.name.spacing" role="columnheader">
+ {{ tableData.name.title }}
</div>
- <div class="table-section section-10 environments-deploy" role="columnheader">
- {{ s__('Environments|Deployment') }}
+ <div class="table-section" :class="tableData.deploy.spacing" role="columnheader">
+ {{ tableData.deploy.title }}
</div>
- <div class="table-section section-15 environments-build" role="columnheader">
- {{ s__('Environments|Job') }}
+ <div class="table-section" :class="tableData.build.spacing" role="columnheader">
+ {{ tableData.build.title }}
</div>
- <div class="table-section section-20 environments-commit" role="columnheader">
- {{ s__('Environments|Commit') }}
+ <div class="table-section" :class="tableData.commit.spacing" role="columnheader">
+ {{ tableData.commit.title }}
</div>
- <div class="table-section section-10 environments-date" role="columnheader">
- {{ s__('Environments|Updated') }}
+ <div class="table-section" :class="tableData.date.spacing" role="columnheader">
+ {{ tableData.date.title }}
</div>
</div>
<template v-for="(model, i) in sortedEnvironments" :model="model">
@@ -101,6 +130,7 @@ export default {
:key="`environment-item-${i}`"
:model="model"
:can-read-environment="canReadEnvironment"
+ :table-data="tableData"
/>
<div
@@ -115,7 +145,8 @@ export default {
:is-loading="model.isLoadingDeployBoard"
:is-empty="model.isEmptyDeployBoard"
:has-legacy-app-label="model.hasLegacyAppLabel"
- :logs-path="model.logs_path"
+ :project-path="model.project_path"
+ :environment-name="model.name"
/>
</div>
</div>
@@ -132,6 +163,7 @@ export default {
:key="`env-item-${i}-${index}`"
:model="children"
:can-read-environment="canReadEnvironment"
+ :table-data="tableData"
/>
<div :key="`sub-div-${i}`">
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 6fd0561f682..d60c2efd618 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -40,13 +40,13 @@ export default {
<div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
- <div v-if="!isLoading" class="top-area">
- <h4 class="js-folder-name environments-folder-name">
- {{ s__('Environments|Environments') }} /
- <b>{{ folderName }}</b>
- </h4>
+ <h4 class="js-folder-name environments-folder-name">
+ {{ s__('Environments|Environments') }} /
+ <b>{{ folderName }}</b>
+ </h4>
- <tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" />
+ <div class="top-area">
+ <tabs v-if="!isLoading" :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" />
</div>
<container
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index dcdaf8731f8..9a68619d4f7 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -21,7 +21,6 @@ export default () =>
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
deployBoardsHelpPath: environmentsData.deployBoardsHelpPath,
- cssContainerClass: environmentsData.cssClass,
canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
};
@@ -33,7 +32,6 @@ export default () =>
newEnvironmentPath: this.newEnvironmentPath,
helpPagePath: this.helpPagePath,
deployBoardsHelpPath: this.deployBoardsHelpPath,
- cssContainerClass: this.cssContainerClass,
canCreateEnvironment: this.canCreateEnvironment,
canReadEnvironment: this.canReadEnvironment,
...this.canaryCalloutProps,
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5fb420e9da5..81c257acd53 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,5 +1,5 @@
-import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { setDeployBoard } from 'ee_else_ce/environments/stores/helpers';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
/**
* Environments Store.
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 37c9818f869..14b2e59009a 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -1,8 +1,9 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat';
-import { __, sprintf } from '~/locale';
-import { GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { __, sprintf, n__ } from '~/locale';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import Stacktrace from './stacktrace.vue';
@@ -12,7 +13,8 @@ import { trackClickErrorLinkToSentryOptions } from '../utils';
export default {
components: {
- GlButton,
+ LoadingButton,
+ GlFormInput,
GlLink,
GlLoadingIcon,
TooltipOnTruncate,
@@ -32,6 +34,19 @@ export default {
type: String,
required: true,
},
+ projectIssuesPath: {
+ type: String,
+ required: true,
+ },
+ csrfToken: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ issueCreationInProgress: false,
+ };
},
computed: {
...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']),
@@ -41,7 +56,7 @@ export default {
__('Reported %{timeAgo} by %{reportedBy}'),
{
reportedBy: `<strong>${this.error.culprit}</strong>`,
- timeAgo: this.timeFormated(this.stacktraceData.date_received),
+ timeAgo: this.timeFormatted(this.stacktraceData.date_received),
},
false,
);
@@ -58,6 +73,27 @@ export default {
showStacktrace() {
return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length);
},
+ issueTitle() {
+ return this.error.title;
+ },
+ issueDescription() {
+ return sprintf(
+ __(
+ '%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}',
+ ),
+ {
+ description: '# Error Details:\n',
+ errorUrl: `${this.error.external_url}\n`,
+ firstSeen: `\n${this.error.first_seen}\n`,
+ lastSeen: `${this.error.last_seen}\n`,
+ countLabel: n__('- Event', '- Events', this.error.count),
+ count: `${this.error.count}\n`,
+ userCountLabel: n__('- User', '- Users', this.error.user_count),
+ userCount: `${this.error.user_count}\n`,
+ },
+ false,
+ );
+ },
},
mounted() {
this.startPollingDetails(this.issueDetailsPath);
@@ -66,8 +102,12 @@ export default {
methods: {
...mapActions('details', ['startPollingDetails', 'startPollingStacktrace']),
trackClickErrorLinkToSentryOptions,
+ createIssue() {
+ this.issueCreationInProgress = true;
+ this.$refs.sentryIssueForm.submit();
+ },
formatDate(date) {
- return `${this.timeFormated(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
+ return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
},
},
};
@@ -78,13 +118,27 @@ export default {
<div v-if="loading" class="py-3">
<gl-loading-icon :size="3" />
</div>
-
<div v-else-if="showDetails" class="error-details">
<div class="top-area align-items-center justify-content-between py-3">
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span>
- <!-- <gl-button class="my-3 ml-auto" variant="success">
- {{ __('Create Issue') }}
- </gl-button>-->
+ <form ref="sentryIssueForm" :action="projectIssuesPath" method="POST">
+ <gl-form-input class="hidden" name="issue[title]" :value="issueTitle" />
+ <input name="issue[description]" :value="issueDescription" type="hidden" />
+ <gl-form-input
+ :value="error.id"
+ class="hidden"
+ name="issue[sentry_issue_attributes][sentry_issue_identifier]"
+ />
+ <gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" />
+ <loading-button
+ v-if="!error.gitlab_issue"
+ class="btn-success"
+ :label="__('Create issue')"
+ :loading="issueCreationInProgress"
+ data-qa-selector="create_issue_button"
+ @click="createIssue"
+ />
+ </form>
</div>
<div>
<tooltip-on-truncate :title="error.title" truncate-target="child" placement="top">
@@ -92,6 +146,12 @@ export default {
</tooltip-on-truncate>
<h3>{{ __('Error details') }}</h3>
<ul>
+ <li v-if="error.gitlab_issue">
+ <span class="bold">{{ __('GitLab Issue') }}:</span>
+ <gl-link :href="error.gitlab_issue">
+ <span>{{ error.gitlab_issue }}</span>
+ </gl-link>
+ </li>
<li>
<span class="bold">{{ __('Sentry event') }}:</span>
<gl-link
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 88139ce7403..8e2128ac713 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -1,39 +1,57 @@
<script>
-import { mapActions, mapState, mapGetters } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import {
GlEmptyState,
GlButton,
+ GlIcon,
GlLink,
GlLoadingIcon,
GlTable,
- GlSearchBoxByType,
+ GlFormInput,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlTooltipDirective,
+ GlPagination,
} from '@gitlab/ui';
-import { visitUrl } from '~/lib/utils/url_utility';
+import AccessorUtils from '~/lib/utils/accessor';
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
-import TrackEventDirective from '~/vue_shared/directives/track_event';
-import { trackViewInSentryOptions } from '../utils';
+import _ from 'underscore';
export default {
+ FIRST_PAGE: 1,
+ PREV_PAGE: 1,
+ NEXT_PAGE: 2,
fields: [
{ key: 'error', label: __('Open errors'), thClass: 'w-70p' },
{ key: 'events', label: __('Events') },
{ key: 'users', label: __('Users') },
{ key: 'lastSeen', label: __('Last seen'), thClass: 'w-15p' },
],
+ sortFields: {
+ last_seen: __('Last Seen'),
+ first_seen: __('First Seen'),
+ frequency: __('Frequency'),
+ },
components: {
GlEmptyState,
GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlIcon,
GlLink,
GlLoadingIcon,
GlTable,
- GlSearchBoxByType,
+ GlFormInput,
Icon,
+ GlPagination,
TimeAgo,
},
directives: {
- TrackEvent: TrackEventDirective,
+ GlTooltip: GlTooltipDirective,
},
props: {
indexPath: {
@@ -57,112 +75,214 @@ export default {
required: true,
},
},
+ hasLocalStorage: AccessorUtils.isLocalStorageAccessSafe(),
data() {
return {
errorSearchQuery: '',
+ pageValue: this.$options.FIRST_PAGE,
};
},
computed: {
- ...mapState('list', ['errors', 'externalUrl', 'loading']),
- ...mapGetters('list', ['filterErrorsByTitle']),
- filteredErrors() {
- return this.errorSearchQuery ? this.filterErrorsByTitle(this.errorSearchQuery) : this.errors;
+ ...mapState('list', [
+ 'errors',
+ 'loading',
+ 'searchQuery',
+ 'sortField',
+ 'recentSearches',
+ 'pagination',
+ ]),
+ paginationRequired() {
+ return !_.isEmpty(this.pagination);
+ },
+ },
+ watch: {
+ pagination() {
+ if (typeof this.pagination.previous === 'undefined') {
+ this.pageValue = this.$options.FIRST_PAGE;
+ }
},
},
created() {
if (this.errorTrackingEnabled) {
- this.startPolling(this.indexPath);
+ this.setEndpoint(this.indexPath);
+ this.startPolling();
}
},
methods: {
- ...mapActions('list', ['startPolling', 'restartPolling']),
- trackViewInSentryOptions,
- viewDetails(errorId) {
- visitUrl(`error_tracking/${errorId}/details`);
+ ...mapActions('list', [
+ 'startPolling',
+ 'restartPolling',
+ 'setEndpoint',
+ 'searchByQuery',
+ 'sortByField',
+ 'addRecentSearch',
+ 'clearRecentSearches',
+ 'loadRecentSearches',
+ 'setIndexPath',
+ ]),
+ setSearchText(text) {
+ this.errorSearchQuery = text;
+ this.searchByQuery(text);
+ },
+ getDetailsLink(errorId) {
+ return `error_tracking/${errorId}/details`;
+ },
+ goToNextPage() {
+ this.pageValue = this.$options.NEXT_PAGE;
+ this.startPolling(`${this.indexPath}?cursor=${this.pagination.next.cursor}`);
+ },
+ goToPrevPage() {
+ this.startPolling(`${this.indexPath}?cursor=${this.pagination.previous.cursor}`);
+ },
+ goToPage(page) {
+ window.scrollTo(0, 0);
+ return page === this.$options.PREV_PAGE ? this.goToPrevPage() : this.goToNextPage();
+ },
+ isCurrentSortField(field) {
+ return field === this.sortField;
},
},
};
</script>
<template>
- <div>
+ <div class="error-list">
<div v-if="errorTrackingEnabled">
- <div v-if="loading" class="py-3">
- <gl-loading-icon :size="3" />
- </div>
- <div v-else>
- <div class="d-flex flex-row justify-content-around bg-secondary border">
- <gl-search-box-by-type
- v-model="errorSearchQuery"
- class="col-lg-10 m-3 p-0"
- :placeholder="__('Search or filter results...')"
- type="search"
- autofocus
- />
- <gl-button
- v-track-event="trackViewInSentryOptions(externalUrl)"
- class="m-3"
- variant="primary"
- :href="externalUrl"
- target="_blank"
+ <div
+ class="d-flex flex-row justify-content-around align-items-center bg-secondary border mt-2"
+ >
+ <div class="filtered-search-box flex-grow-1 my-3 ml-3 mr-2">
+ <gl-dropdown
+ :text="__('Recent searches')"
+ class="filtered-search-history-dropdown-wrapper d-none d-md-block"
+ toggle-class="filtered-search-history-dropdown-toggle-button"
+ :disabled="loading"
>
- {{ __('View in Sentry') }}
- <icon name="external-link" class="flex-shrink-0" />
- </gl-button>
+ <div v-if="!$options.hasLocalStorage" class="px-3">
+ {{ __('This feature requires local storage to be enabled') }}
+ </div>
+ <template v-else-if="recentSearches.length > 0">
+ <gl-dropdown-item
+ v-for="searchQuery in recentSearches"
+ :key="searchQuery"
+ @click="setSearchText(searchQuery)"
+ >{{ searchQuery }}</gl-dropdown-item
+ >
+ <gl-dropdown-divider />
+ <gl-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches">{{
+ __('Clear recent searches')
+ }}</gl-dropdown-item>
+ </template>
+ <div v-else class="px-3">{{ __("You don't have any recent searches") }}</div>
+ </gl-dropdown>
+ <div class="filtered-search-input-container flex-fill">
+ <gl-form-input
+ v-model="errorSearchQuery"
+ class="pl-2 filtered-search"
+ :disabled="loading"
+ :placeholder="__('Search or filter results…')"
+ autofocus
+ @keyup.enter.native="searchByQuery(errorSearchQuery)"
+ />
+ </div>
+ <div class="gl-search-box-by-type-right-icons">
+ <gl-button
+ v-if="errorSearchQuery.length > 0"
+ v-gl-tooltip.hover
+ :title="__('Clear')"
+ class="clear-search text-secondary"
+ name="clear"
+ @click="errorSearchQuery = ''"
+ >
+ <gl-icon name="close" :size="12" />
+ </gl-button>
+ </div>
</div>
- <gl-table
- class="mt-3"
- :items="filteredErrors"
- :fields="$options.fields"
- :show-empty="true"
- fixed
- stacked="sm"
+ <gl-dropdown
+ :text="$options.sortFields[sortField]"
+ left
+ :disabled="loading"
+ class="mr-3"
+ menu-class="sort-dropdown"
>
- <template slot="HEAD_events" slot-scope="data">
- <div class="text-md-right">{{ data.label }}</div>
- </template>
- <template slot="HEAD_users" slot-scope="data">
- <div class="text-md-right">{{ data.label }}</div>
- </template>
- <template slot="error" slot-scope="errors">
- <div class="d-flex flex-column">
- <gl-link
- class="d-flex text-dark"
- target="_blank"
- @click="viewDetails(errors.item.id)"
- >
- <strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
- </gl-link>
- <span class="text-secondary text-truncate">
- {{ errors.item.culprit }}
- </span>
- </div>
- </template>
+ <gl-dropdown-item
+ v-for="(label, field) in $options.sortFields"
+ :key="field"
+ @click="sortByField(field)"
+ >
+ <span class="d-flex">
+ <icon
+ class="flex-shrink-0 append-right-4"
+ :class="{ invisible: !isCurrentSortField(field) }"
+ name="mobile-issue-close"
+ />
+ {{ label }}
+ </span>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
- <template slot="events" slot-scope="errors">
- <div class="text-md-right">{{ errors.item.count }}</div>
- </template>
+ <div v-if="loading" class="py-3">
+ <gl-loading-icon size="md" />
+ </div>
- <template slot="users" slot-scope="errors">
- <div class="text-md-right">{{ errors.item.userCount }}</div>
- </template>
+ <gl-table
+ v-else
+ class="mt-3"
+ :items="errors"
+ :fields="$options.fields"
+ :show-empty="true"
+ fixed
+ stacked="sm"
+ >
+ <template slot="HEAD_events" slot-scope="data">
+ <div class="text-md-right">{{ data.label }}</div>
+ </template>
+ <template slot="HEAD_users" slot-scope="data">
+ <div class="text-md-right">{{ data.label }}</div>
+ </template>
+ <template slot="error" slot-scope="errors">
+ <div class="d-flex flex-column">
+ <gl-link class="d-flex text-dark" :href="getDetailsLink(errors.item.id)">
+ <strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
+ </gl-link>
+ <span class="text-secondary text-truncate">
+ {{ errors.item.culprit }}
+ </span>
+ </div>
+ </template>
+ <template slot="events" slot-scope="errors">
+ <div class="text-md-right">{{ errors.item.count }}</div>
+ </template>
- <template slot="lastSeen" slot-scope="errors">
- <div class="d-flex align-items-center">
- <time-ago :time="errors.item.lastSeen" class="text-secondary" />
- </div>
- </template>
- <template slot="empty">
- <div ref="empty">
- {{ __('No errors to display.') }}
- <gl-link class="js-try-again" @click="restartPolling">
- {{ __('Check again') }}
- </gl-link>
- </div>
- </template>
- </gl-table>
- </div>
+ <template slot="users" slot-scope="errors">
+ <div class="text-md-right">{{ errors.item.userCount }}</div>
+ </template>
+
+ <template slot="lastSeen" slot-scope="errors">
+ <div class="d-flex align-items-center">
+ <time-ago :time="errors.item.lastSeen" class="text-secondary" />
+ </div>
+ </template>
+ <template slot="empty">
+ <div ref="empty">
+ {{ __('No errors to display.') }}
+ <gl-link class="js-try-again" @click="restartPolling">
+ {{ __('Check again') }}
+ </gl-link>
+ </div>
+ </template>
+ </gl-table>
+ <gl-pagination
+ v-show="!loading"
+ v-if="paginationRequired"
+ :prev-page="$options.PREV_PAGE"
+ :next-page="$options.NEXT_PAGE"
+ :value="pageValue"
+ align="center"
+ @input="goToPage"
+ />
</div>
<div v-else-if="userCanEnableErrorTracking">
<gl-empty-state
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace.vue b/app/assets/javascripts/error_tracking/components/stacktrace.vue
index 6b71967624f..f58d54f2933 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace.vue
@@ -27,6 +27,8 @@ export default {
:lines="entry.context"
:file-path="entry.filename"
:error-line="entry.lineNo"
+ :error-fn="entry.function"
+ :error-column="entry.colNo"
:expanded="isFirstEntry(index)"
/>
</div>
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index ad542c579a9..62fd379aa4c 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,5 +1,6 @@
<script>
import { GlTooltip } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
@@ -22,9 +23,20 @@ export default {
type: String,
required: true,
},
+ errorFn: {
+ type: String,
+ required: false,
+ default: '',
+ },
errorLine: {
type: Number,
- required: true,
+ required: false,
+ default: 0,
+ },
+ errorColumn: {
+ type: Number,
+ required: false,
+ default: 0,
},
expanded: {
type: Boolean,
@@ -38,12 +50,23 @@ export default {
};
},
computed: {
- linesLength() {
- return this.lines.length;
+ hasCode() {
+ return Boolean(this.lines.length);
},
collapseIcon() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
},
+ noCodeFn() {
+ return this.errorFn ? sprintf(__('in %{errorFn} '), { errorFn: this.errorFn }) : '';
+ },
+ noCodeLine() {
+ return this.errorLine
+ ? sprintf(__('at line %{errorLine}%{errorColumn}'), {
+ errorLine: this.errorLine,
+ errorColumn: this.errorColumn ? `:${this.errorColumn}` : '',
+ })
+ : '';
+ },
},
methods: {
isHighlighted(lineNum) {
@@ -66,27 +89,31 @@ export default {
<template>
<div class="file-holder">
<div ref="header" class="file-title file-title-flex-parent">
- <div class="file-header-content ">
- <div class="d-inline-block cursor-pointer" @click="toggle()">
+ <div class="file-header-content d-flex align-content-center">
+ <div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()">
<icon :name="collapseIcon" :size="16" aria-hidden="true" class="append-right-5" />
</div>
- <div class="d-inline-block append-right-4">
- <file-icon
- :file-name="filePath"
- :size="18"
- aria-hidden="true"
- css-classes="append-right-5"
- />
- <strong v-gl-tooltip :title="filePath" class="file-title-name" data-container="body">
- {{ filePath }}
- </strong>
- </div>
-
+ <file-icon
+ :file-name="filePath"
+ :size="18"
+ aria-hidden="true"
+ css-classes="append-right-5"
+ />
+ <strong
+ v-gl-tooltip
+ :title="filePath"
+ class="file-title-name d-inline-block overflow-hidden text-truncate"
+ :class="{ 'limited-width': !hasCode }"
+ data-container="body"
+ >
+ {{ filePath }}
+ </strong>
<clipboard-button
:title="__('Copy file path')"
:text="filePath"
- css-class="btn-default btn-transparent btn-clipboard"
+ css-class="btn-default btn-transparent btn-clipboard position-static"
/>
+ <span v-if="!hasCode" class="text-tertiary">{{ noCodeFn }}{{ noCodeLine }}</span>
</div>
</div>
diff --git a/app/assets/javascripts/error_tracking/details.js b/app/assets/javascripts/error_tracking/details.js
index b9b51a6539f..872cb8868a2 100644
--- a/app/assets/javascripts/error_tracking/details.js
+++ b/app/assets/javascripts/error_tracking/details.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import store from './store';
import ErrorDetails from './components/error_details.vue';
+import csrf from '~/lib/utils/csrf';
export default () => {
// eslint-disable-next-line no-new
@@ -12,12 +13,14 @@ export default () => {
store,
render(createElement) {
const domEl = document.querySelector(this.$options.el);
- const { issueDetailsPath, issueStackTracePath } = domEl.dataset;
+ const { issueDetailsPath, issueStackTracePath, projectIssuesPath } = domEl.dataset;
return createElement('error-details', {
props: {
issueDetailsPath,
issueStackTracePath,
+ projectIssuesPath,
+ csrfToken: csrf.token,
},
});
},
diff --git a/app/assets/javascripts/error_tracking/services/index.js b/app/assets/javascripts/error_tracking/services/index.js
index 68988296cc2..3b3f8311d67 100644
--- a/app/assets/javascripts/error_tracking/services/index.js
+++ b/app/assets/javascripts/error_tracking/services/index.js
@@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
export default {
- getSentryData({ endpoint }) {
- return axios.get(endpoint);
+ getSentryData({ endpoint, params }) {
+ return axios.get(endpoint, { params });
},
};
diff --git a/app/assets/javascripts/error_tracking/store/details/getters.js b/app/assets/javascripts/error_tracking/store/details/getters.js
index 7d13439d721..a36c84dc28c 100644
--- a/app/assets/javascripts/error_tracking/store/details/getters.js
+++ b/app/assets/javascripts/error_tracking/store/details/getters.js
@@ -1,3 +1,6 @@
-export const stacktrace = state => state.stacktraceData.stack_trace_entries.reverse();
+export const stacktrace = state =>
+ state.stacktraceData.stack_trace_entries
+ ? state.stacktraceData.stack_trace_entries.reverse()
+ : [];
export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/index.js b/app/assets/javascripts/error_tracking/store/index.js
index 941c752e96a..ad05eecef6c 100644
--- a/app/assets/javascripts/error_tracking/store/index.js
+++ b/app/assets/javascripts/error_tracking/store/index.js
@@ -4,7 +4,6 @@ import Vuex from 'vuex';
import * as listActions from './list/actions';
import listMutations from './list/mutations';
import listState from './list/state';
-import * as listGetters from './list/getters';
import * as detailsActions from './details/actions';
import detailsMutations from './details/mutations';
@@ -21,7 +20,6 @@ export const createStore = () =>
state: listState(),
actions: listActions,
mutations: listMutations,
- getters: listGetters,
},
details: {
namespaced: true,
diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index 18c6e5e9695..c9e882c4ed2 100644
--- a/app/assets/javascripts/error_tracking/store/list/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/actions.js
@@ -6,17 +6,25 @@ import { __, sprintf } from '~/locale';
let eTagPoll;
-export function startPolling({ commit, dispatch }, endpoint) {
+export function startPolling({ state, commit, dispatch }) {
+ commit(types.SET_LOADING, true);
+
eTagPoll = new Poll({
resource: Service,
method: 'getSentryData',
- data: { endpoint },
+ data: {
+ endpoint: state.endpoint,
+ params: {
+ search_term: state.searchQuery,
+ sort: state.sortField,
+ },
+ },
successCallback: ({ data }) => {
if (!data) {
return;
}
+ commit(types.SET_PAGINATION, data.pagination);
commit(types.SET_ERRORS, data.errors);
- commit(types.SET_EXTERNAL_URL, data.external_url);
commit(types.SET_LOADING, false);
dispatch('stopPolling');
},
@@ -43,10 +51,43 @@ export const stopPolling = () => {
export function restartPolling({ commit }) {
commit(types.SET_ERRORS, []);
- commit(types.SET_EXTERNAL_URL, '');
commit(types.SET_LOADING, true);
if (eTagPoll) eTagPoll.restart();
}
+export function setIndexPath({ commit }, path) {
+ commit(types.SET_INDEX_PATH, path);
+}
+
+export function loadRecentSearches({ commit }) {
+ commit(types.LOAD_RECENT_SEARCHES);
+}
+
+export function addRecentSearch({ commit }, searchQuery) {
+ commit(types.ADD_RECENT_SEARCH, searchQuery);
+}
+
+export function clearRecentSearches({ commit }) {
+ commit(types.CLEAR_RECENT_SEARCHES);
+}
+
+export const searchByQuery = ({ commit, dispatch }, query) => {
+ const searchQuery = query.trim();
+ commit(types.SET_SEARCH_QUERY, searchQuery);
+ commit(types.ADD_RECENT_SEARCH, searchQuery);
+ dispatch('stopPolling');
+ dispatch('startPolling');
+};
+
+export const sortByField = ({ commit, dispatch }, field) => {
+ commit(types.SET_SORT_FIELD, field);
+ dispatch('stopPolling');
+ dispatch('startPolling');
+};
+
+export const setEndpoint = ({ commit }, endpoint) => {
+ commit(types.SET_ENDPOINT, endpoint);
+};
+
export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/list/getters.js b/app/assets/javascripts/error_tracking/store/list/getters.js
deleted file mode 100644
index 1a2ec62f79f..00000000000
--- a/app/assets/javascripts/error_tracking/store/list/getters.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const filterErrorsByTitle = state => errorQuery =>
- state.errors.filter(error => error.title.match(new RegExp(`${errorQuery}`, 'i')));
-
-export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/list/mutation_types.js b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
index f9d77a6b08e..301984a1ee0 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutation_types.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
@@ -1,3 +1,10 @@
export const SET_ERRORS = 'SET_ERRORS';
-export const SET_EXTERNAL_URL = 'SET_EXTERNAL_URL';
+export const SET_INDEX_PATH = 'SET_INDEX_PATH';
export const SET_LOADING = 'SET_LOADING';
+export const ADD_RECENT_SEARCH = 'ADD_RECENT_SEARCH';
+export const CLEAR_RECENT_SEARCHES = 'CLEAR_RECENT_SEARCHES';
+export const LOAD_RECENT_SEARCHES = 'LOAD_RECENT_SEARCHES';
+export const SET_PAGINATION = 'SET_PAGINATION';
+export const SET_ENDPOINT = 'SET_ENDPOINT';
+export const SET_SORT_FIELD = 'SET_SORT_FIELD';
+export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
diff --git a/app/assets/javascripts/error_tracking/store/list/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js
index e4bd81db9c9..5648013bb89 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutations.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutations.js
@@ -1,14 +1,59 @@
import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import AccessorUtils from '~/lib/utils/accessor';
export default {
[types.SET_ERRORS](state, data) {
state.errors = convertObjectPropsToCamelCase(data, { deep: true });
},
- [types.SET_EXTERNAL_URL](state, url) {
- state.externalUrl = url;
- },
[types.SET_LOADING](state, loading) {
state.loading = loading;
},
+ [types.SET_INDEX_PATH](state, path) {
+ state.indexPath = path;
+ },
+ [types.ADD_RECENT_SEARCH](state, searchTerm) {
+ if (searchTerm.length === 0) {
+ return;
+ }
+ // remove any existing item, then add it to the start of the list
+ const recentSearches = state.recentSearches.filter(s => s !== searchTerm);
+ recentSearches.unshift(searchTerm);
+ // only keep the last 5
+ state.recentSearches = recentSearches.slice(0, 5);
+
+ if (AccessorUtils.isLocalStorageAccessSafe()) {
+ localStorage.setItem(
+ `recent-searches${state.indexPath}`,
+ JSON.stringify(state.recentSearches),
+ );
+ }
+ },
+ [types.CLEAR_RECENT_SEARCHES](state) {
+ state.recentSearches = [];
+ if (AccessorUtils.isLocalStorageAccessSafe()) {
+ localStorage.removeItem(`recent-searches${state.indexPath}`);
+ }
+ },
+ [types.LOAD_RECENT_SEARCHES](state) {
+ const recentSearches = localStorage.getItem(`recent-searches${state.indexPath}`) || [];
+ try {
+ state.recentSearches = JSON.parse(recentSearches);
+ } catch (e) {
+ state.recentSearches = [];
+ throw e;
+ }
+ },
+ [types.SET_PAGINATION](state, pagination) {
+ state.pagination = pagination;
+ },
+ [types.SET_SORT_FIELD](state, field) {
+ state.sortField = field;
+ },
+ [types.SET_SEARCH_QUERY](state, query) {
+ state.searchQuery = query;
+ },
+ [types.SET_ENDPOINT](state, endpoint) {
+ state.endpoint = endpoint;
+ },
};
diff --git a/app/assets/javascripts/error_tracking/store/list/state.js b/app/assets/javascripts/error_tracking/store/list/state.js
index d371350ef0e..93dc1040fde 100644
--- a/app/assets/javascripts/error_tracking/store/list/state.js
+++ b/app/assets/javascripts/error_tracking/store/list/state.js
@@ -1,5 +1,10 @@
export default () => ({
errors: [],
- externalUrl: '',
loading: true,
+ endpoint: null,
+ sortField: 'last_seen',
+ searchQuery: null,
+ indexPath: '',
+ recentSearches: [],
+ pagination: {},
});
diff --git a/app/assets/javascripts/error_tracking/utils.js b/app/assets/javascripts/error_tracking/utils.js
index b832b1371b1..3c382ccd1aa 100644
--- a/app/assets/javascripts/error_tracking/utils.js
+++ b/app/assets/javascripts/error_tracking/utils.js
@@ -1,15 +1,4 @@
-/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
-
-/**
- * Tracks snowplow event when user clicks View in Sentry btn
- * @param {String} externalUrl that will be send as a property for the event
- */
-export const trackViewInSentryOptions = url => ({
- category: 'Error Tracking',
- action: 'click_view_in_sentry',
- label: 'External Url',
- property: url,
-});
+/* eslint-disable @gitlab/i18n/no-non-i18n-strings, import/prefer-default-export */
/**
* Tracks snowplow event when User clicks on error link to Sentry
diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
index d86116aa315..9f77fe8cd59 100644
--- a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
@@ -32,12 +32,16 @@ export default {
placeholder="https://mysentryserver.com"
@input="updateApiHost"
/>
+ <p class="form-text text-muted">
+ {{
+ s__(
+ "ErrorTracking|If you self-host Sentry, enter the full URL of your Sentry instance. If you're using Sentry's hosted solution, enter https://sentry.io",
+ )
+ }}
+ </p>
<!-- eslint-enable @gitlab/vue-i18n/no-bare-attribute-strings -->
</div>
</div>
- <p class="form-text text-muted">
- {{ s__('ErrorTracking|Find your hostname in your Sentry account settings page') }}
- </p>
</div>
<div class="form-group" :class="{ 'gl-show-field-errors': connectError }">
<label class="label-bold" for="error-tracking-token">
diff --git a/app/assets/javascripts/filtered_search/.eslintrc.yml b/app/assets/javascripts/filtered_search/.eslintrc.yml
new file mode 100644
index 00000000000..9faca7152f6
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/.eslintrc.yml
@@ -0,0 +1,3 @@
+rules:
+ # https://gitlab.com/gitlab-org/gitlab/issues/28716
+ import/no-cycle: off
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 5c2d32f4e85..a4edc5fd52d 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,7 +1,7 @@
import _ from 'underscore';
+import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import FilteredSearchContainer from './container';
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index ea58dbd3fa9..1343ccd6468 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -1,4 +1,5 @@
import _ from 'underscore';
+import { USER_TOKEN_TYPES } from 'ee_else_ce/filtered_search/constants';
import FilteredSearchContainer from '~/filtered_search/container';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
import AjaxCache from '~/lib/utils/ajax_cache';
@@ -6,7 +7,6 @@ import DropdownUtils from '~/filtered_search/dropdown_utils';
import Flash from '~/flash';
import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale';
-import { USER_TOKEN_TYPES } from 'ee_else_ce/filtered_search/constants';
export default class VisualTokenValue {
constructor(tokenValue, tokenType) {
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 968e255e1fc..8cf939254c1 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -1,7 +1,7 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import AccessorUtilities from '~/lib/utils/accessor';
import { GlLoadingIcon } from '@gitlab/ui';
+import AccessorUtilities from '~/lib/utils/accessor';
import eventHub from '../event_hub';
import store from '../store/';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js
index 92ac3a2c94d..78ccef7f253 100644
--- a/app/assets/javascripts/frequent_items/store/mutations.js
+++ b/app/assets/javascripts/frequent_items/store/mutations.js
@@ -48,7 +48,7 @@ export default {
});
},
[types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, results) {
- const rawItems = results.data;
+ const rawItems = results.data ? results.data : results; // Api.groups returns array, Api.projects returns object
Object.assign(state, {
items: rawItems.map(rawItem => ({
id: rawItem.id,
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index db3ad0bb4c9..e25c9d90f60 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -3,11 +3,44 @@ import 'at.js';
import _ from 'underscore';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
+import { spriteIcon } from './lib/utils/common_utils';
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
+export function membersBeforeSave(members) {
+ return _.map(members, member => {
+ const GROUP_TYPE = 'Group';
+
+ let title = '';
+ if (member.username == null) {
+ return member;
+ }
+ title = member.name;
+ if (member.count && !member.mentionsDisabled) {
+ title += ` (${member.count})`;
+ }
+
+ const autoCompleteAvatar = member.avatar_url || member.username.charAt(0).toUpperCase();
+
+ const rectAvatarClass = member.type === GROUP_TYPE ? 'rect-avatar' : '';
+ const imgAvatar = `<img src="${member.avatar_url}" alt="${member.username}" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`;
+ const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`;
+ const avatarIcon = member.mentionsDisabled
+ ? spriteIcon('notifications-off', 's16 vertical-align-middle prepend-left-5')
+ : '';
+
+ return {
+ username: member.username,
+ avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
+ title: sanitize(title),
+ search: sanitize(`${member.username} ${member.name}`),
+ icon: avatarIcon,
+ };
+ });
+}
+
export const defaultAutocompleteConfig = {
emojis: true,
members: true,
@@ -167,12 +200,13 @@ class GfmAutoComplete {
alias: 'users',
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
- const { avatarTag, username, title } = value;
+ const { avatarTag, username, title, icon } = value;
if (username != null) {
tmpl = GfmAutoComplete.Members.templateFunction({
avatarTag,
username,
title,
+ icon,
});
}
return tmpl;
@@ -185,33 +219,7 @@ class GfmAutoComplete {
data: GfmAutoComplete.defaultLoadingData,
callbacks: {
...this.getDefaultCallbacks(),
- beforeSave(members) {
- return $.map(members, m => {
- let title = '';
- if (m.username == null) {
- return m;
- }
- title = m.name;
- if (m.count) {
- title += ` (${m.count})`;
- }
-
- const GROUP_TYPE = 'Group';
-
- const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
-
- const rectAvatarClass = m.type === GROUP_TYPE ? 'rect-avatar' : '';
- const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`;
- const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`;
-
- return {
- username: m.username,
- avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
- title: sanitize(title),
- search: sanitize(`${m.username} ${m.name}`),
- };
- });
- },
+ beforeSave: membersBeforeSave,
},
});
}
@@ -624,8 +632,8 @@ GfmAutoComplete.Emoji = {
};
// Team Members
GfmAutoComplete.Members = {
- templateFunction({ avatarTag, username, title }) {
- return `<li>${avatarTag} ${username} <small>${_.escape(title)}</small></li>`;
+ templateFunction({ avatarTag, username, title, icon }) {
+ return `<li>${avatarTag} ${username} <small>${_.escape(title)}</small> ${icon}</li>`;
},
};
GfmAutoComplete.Labels = {
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 045f77af7ea..65d05887453 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, vars-on-top, no-shadow, no-cond-assign, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, no-param-reassign, no-loop-func */
+/* eslint-disable func-names, no-underscore-dangle, one-var, no-cond-assign, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, no-param-reassign, no-loop-func */
import $ from 'jquery';
import _ from 'underscore';
@@ -8,984 +8,887 @@ import { visitUrl } from './lib/utils/url_utility';
import { isObject } from './lib/utils/type_utility';
import renderItem from './gl_dropdown/render';
-var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
-
-GitLabDropdownInput = (function() {
- function GitLabDropdownInput(input, options) {
- var $inputContainer, $clearButton;
- var _this = this;
- this.input = input;
- this.options = options;
- this.fieldName = this.options.fieldName || 'field-name';
- $inputContainer = this.input.parent();
- $clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on(
- 'click',
- (function(_this) {
- // Clear click
- return function(e) {
- e.preventDefault();
- e.stopPropagation();
- return _this.input
- .val('')
- .trigger('input')
- .focus();
- };
- })(this),
- );
+const BLUR_KEYCODES = [27, 40];
- this.input
- .on('keydown', e => {
- var keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', e => {
- var val = e.currentTarget.value || _this.options.inputFieldName;
- val = val
- .split(' ')
- .join('-') // replaces space with dash
- .replace(/[^a-zA-Z0-9 -]/g, '')
- .toLowerCase() // replace non alphanumeric
- .replace(/(-)\1+/g, '-'); // replace repeated dashes
- _this.cb(_this.options.fieldName, val, {}, true);
- _this.input
- .closest('.dropdown')
- .find('.dropdown-toggle-text')
- .text(val);
- });
- }
+const HAS_VALUE_CLASS = 'has-value';
- GitLabDropdownInput.prototype.onInput = function(cb) {
- this.cb = cb;
- };
-
- return GitLabDropdownInput;
-})();
-
-GitLabDropdownFilter = (function() {
- var BLUR_KEYCODES, HAS_VALUE_CLASS;
-
- BLUR_KEYCODES = [27, 40];
-
- HAS_VALUE_CLASS = 'has-value';
-
- function GitLabDropdownFilter(input, options) {
- var $clearButton, $inputContainer, ref, timeout;
- this.input = input;
- this.options = options;
- this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
- $inputContainer = this.input.parent();
- $clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on(
- 'click',
- (function(_this) {
- // Clear click
- return function(e) {
- e.preventDefault();
- e.stopPropagation();
- return _this.input
- .val('')
- .trigger('input')
- .focus();
- };
- })(this),
- );
- // Key events
- timeout = '';
- this.input
- .on('keydown', e => {
- var keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', () => {
- if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.addClass(HAS_VALUE_CLASS);
- } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.removeClass(HAS_VALUE_CLASS);
- }
- // Only filter asynchronously only if option remote is set
- if (this.options.remote) {
- clearTimeout(timeout);
- return (timeout = setTimeout(() => {
- $inputContainer.parent().addClass('is-loading');
-
- return this.options.query(this.input.val(), data => {
- $inputContainer.parent().removeClass('is-loading');
- return this.options.callback(data);
- });
- }, 250));
- } else {
- return this.filter(this.input.val());
- }
- });
- }
+const LOADING_CLASS = 'is-loading';
- GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
- return BLUR_KEYCODES.indexOf(keyCode) !== -1;
- };
+const PAGE_TWO_CLASS = 'is-page-two';
- GitLabDropdownFilter.prototype.filter = function(search_text) {
- var data, elements, group, key, results, tmp;
- if (this.options.onFilter) {
- this.options.onFilter(search_text);
- }
- data = this.options.data();
- if (data != null && !this.options.filterByText) {
- results = data;
- if (search_text !== '') {
- // When data is an array of objects therefore [object Array] e.g.
- // [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ]
- if (_.isArray(data)) {
- results = fuzzaldrinPlus.filter(data, search_text, {
- key: this.options.keys,
+const ACTIVE_CLASS = 'is-active';
+
+const INDETERMINATE_CLASS = 'is-indeterminate';
+
+let currentIndex = -1;
+
+const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
+
+const SELECTABLE_CLASSES = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES}, .option-hidden)`;
+
+const CURSOR_SELECT_SCROLL_PADDING = 5;
+
+const FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-filter)';
+
+const NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
+
+function GitLabDropdownInput(input, options) {
+ const _this = this;
+ this.input = input;
+ this.options = options;
+ this.fieldName = this.options.fieldName || 'field-name';
+ const $inputContainer = this.input.parent();
+ const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', e => {
+ // Clear click
+ e.preventDefault();
+ e.stopPropagation();
+ return this.input
+ .val('')
+ .trigger('input')
+ .focus();
+ });
+
+ this.input
+ .on('keydown', e => {
+ const keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', e => {
+ let val = e.currentTarget.value || _this.options.inputFieldName;
+ val = val
+ .split(' ')
+ .join('-') // replaces space with dash
+ .replace(/[^a-zA-Z0-9 -]/g, '')
+ .toLowerCase() // replace non alphanumeric
+ .replace(/(-)\1+/g, '-'); // replace repeated dashes
+ _this.cb(_this.options.fieldName, val, {}, true);
+ _this.input
+ .closest('.dropdown')
+ .find('.dropdown-toggle-text')
+ .text(val);
+ });
+}
+
+GitLabDropdownInput.prototype.onInput = function(cb) {
+ this.cb = cb;
+};
+
+function GitLabDropdownFilter(input, options) {
+ let ref, timeout;
+ this.input = input;
+ this.options = options;
+ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
+ const $inputContainer = this.input.parent();
+ const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', e => {
+ // Clear click
+ e.preventDefault();
+ e.stopPropagation();
+ return this.input
+ .val('')
+ .trigger('input')
+ .focus();
+ });
+ // Key events
+ timeout = '';
+ this.input
+ .on('keydown', e => {
+ const keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', () => {
+ if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.addClass(HAS_VALUE_CLASS);
+ } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.removeClass(HAS_VALUE_CLASS);
+ }
+ // Only filter asynchronously only if option remote is set
+ if (this.options.remote) {
+ clearTimeout(timeout);
+ return (timeout = setTimeout(() => {
+ $inputContainer.parent().addClass('is-loading');
+
+ return this.options.query(this.input.val(), data => {
+ $inputContainer.parent().removeClass('is-loading');
+ return this.options.callback(data);
});
- } else {
- // If data is grouped therefore an [object Object]. e.g.
- // {
- // groupName1: [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ],
- // groupName2: [
- // { prop: 'abc' },
- // { prop: 'def' }
- // ]
- // }
- if (isObject(data)) {
- results = {};
- for (key in data) {
- group = data[key];
- tmp = fuzzaldrinPlus.filter(group, search_text, {
- key: this.options.keys,
- });
- if (tmp.length) {
- results[key] = tmp.map(item => item);
- }
+ }, 250));
+ } else {
+ return this.filter(this.input.val());
+ }
+ });
+}
+
+GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
+ return BLUR_KEYCODES.indexOf(keyCode) !== -1;
+};
+
+GitLabDropdownFilter.prototype.filter = function(search_text) {
+ let elements, group, key, results, tmp;
+ if (this.options.onFilter) {
+ this.options.onFilter(search_text);
+ }
+ const data = this.options.data();
+ if (data != null && !this.options.filterByText) {
+ results = data;
+ if (search_text !== '') {
+ // When data is an array of objects therefore [object Array] e.g.
+ // [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ]
+ if (_.isArray(data)) {
+ results = fuzzaldrinPlus.filter(data, search_text, {
+ key: this.options.keys,
+ });
+ } else {
+ // If data is grouped therefore an [object Object]. e.g.
+ // {
+ // groupName1: [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ],
+ // groupName2: [
+ // { prop: 'abc' },
+ // { prop: 'def' }
+ // ]
+ // }
+ if (isObject(data)) {
+ results = {};
+ for (key in data) {
+ group = data[key];
+ tmp = fuzzaldrinPlus.filter(group, search_text, {
+ key: this.options.keys,
+ });
+ if (tmp.length) {
+ results[key] = tmp.map(item => item);
}
}
}
}
- return this.options.callback(results);
- } else {
- elements = this.options.elements();
- if (search_text) {
- elements.each(function() {
- var $el, matches;
- $el = $(this);
- matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
- if (!$el.is('.dropdown-header')) {
- if (matches.length) {
- return $el.show().removeClass('option-hidden');
- } else {
- return $el.hide().addClass('option-hidden');
- }
+ }
+ return this.options.callback(results);
+ } else {
+ elements = this.options.elements();
+ if (search_text) {
+ elements.each(function() {
+ const $el = $(this);
+ const matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
+ if (!$el.is('.dropdown-header')) {
+ if (matches.length) {
+ return $el.show().removeClass('option-hidden');
+ } else {
+ return $el.hide().addClass('option-hidden');
}
- });
- } else {
- elements.show().removeClass('option-hidden');
- }
-
- elements
- .parent()
- .find('.dropdown-menu-empty-item')
- .toggleClass('hidden', elements.is(':visible'));
+ }
+ });
+ } else {
+ elements.show().removeClass('option-hidden');
}
- };
-
- return GitLabDropdownFilter;
-})();
-GitLabDropdownRemote = (function() {
- function GitLabDropdownRemote(dataEndpoint, options) {
- this.dataEndpoint = dataEndpoint;
- this.options = options;
+ elements
+ .parent()
+ .find('.dropdown-menu-empty-item')
+ .toggleClass('hidden', elements.is(':visible'));
}
+};
- GitLabDropdownRemote.prototype.execute = function() {
- if (typeof this.dataEndpoint === 'string') {
- return this.fetchData();
- } else if (typeof this.dataEndpoint === 'function') {
- if (this.options.beforeSend) {
- this.options.beforeSend();
- }
- return this.dataEndpoint(
- '',
- (function(_this) {
- // Fetch the data by calling the data funcfion
- return function(data) {
- if (_this.options.success) {
- _this.options.success(data);
- }
- if (_this.options.beforeSend) {
- return _this.options.beforeSend();
- }
- };
- })(this),
- );
- }
- };
+function GitLabDropdownRemote(dataEndpoint, options) {
+ this.dataEndpoint = dataEndpoint;
+ this.options = options;
+}
- GitLabDropdownRemote.prototype.fetchData = function() {
+GitLabDropdownRemote.prototype.execute = function() {
+ if (typeof this.dataEndpoint === 'string') {
+ return this.fetchData();
+ } else if (typeof this.dataEndpoint === 'function') {
if (this.options.beforeSend) {
this.options.beforeSend();
}
-
- // Fetch the data through ajax if the data is a string
- return axios.get(this.dataEndpoint).then(({ data }) => {
+ return this.dataEndpoint('', data => {
+ // Fetch the data by calling the data function
if (this.options.success) {
- return this.options.success(data);
+ this.options.success(data);
+ }
+ if (this.options.beforeSend) {
+ return this.options.beforeSend();
}
});
- };
-
- return GitLabDropdownRemote;
-})();
-
-GitLabDropdown = (function() {
- var ACTIVE_CLASS,
- FILTER_INPUT,
- NO_FILTER_INPUT,
- INDETERMINATE_CLASS,
- LOADING_CLASS,
- PAGE_TWO_CLASS,
- NON_SELECTABLE_CLASSES,
- SELECTABLE_CLASSES,
- CURSOR_SELECT_SCROLL_PADDING,
- currentIndex;
-
- LOADING_CLASS = 'is-loading';
-
- PAGE_TWO_CLASS = 'is-page-two';
-
- ACTIVE_CLASS = 'is-active';
-
- INDETERMINATE_CLASS = 'is-indeterminate';
+ }
+};
- currentIndex = -1;
+GitLabDropdownRemote.prototype.fetchData = function() {
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
- NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
-
- SELECTABLE_CLASSES = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES}, .option-hidden)`;
-
- CURSOR_SELECT_SCROLL_PADDING = 5;
-
- FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-filter)';
-
- NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
-
- function GitLabDropdown(el1, options) {
- var searchFields, selector, self;
- this.el = el1;
- this.options = options;
- this.updateLabel = this.updateLabel.bind(this);
- this.hidden = this.hidden.bind(this);
- this.opened = this.opened.bind(this);
- this.shouldPropagate = this.shouldPropagate.bind(this);
- self = this;
- selector = $(this.el).data('target');
- this.dropdown = selector != null ? $(selector) : $(this.el).parent();
- // Set Defaults
- this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
- this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
- this.highlight = Boolean(this.options.highlight);
- this.icon = Boolean(this.options.icon);
- this.filterInputBlur =
- this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
- // If no input is passed create a default one
- self = this;
- // If selector was passed
- if (_.isString(this.filterInput)) {
- this.filterInput = this.getElement(this.filterInput);
+ // Fetch the data through ajax if the data is a string
+ return axios.get(this.dataEndpoint).then(({ data }) => {
+ if (this.options.success) {
+ return this.options.success(data);
}
- searchFields = this.options.search ? this.options.search.fields : [];
- if (this.options.data) {
- // If we provided data
- // data could be an array of objects or a group of arrays
- if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
- this.fullData = this.options.data;
- currentIndex = -1;
- this.parseData(this.options.data);
- this.focusTextInput();
- } else {
- this.remote = new GitLabDropdownRemote(this.options.data, {
- dataType: this.options.dataType,
- beforeSend: this.toggleLoading.bind(this),
- success: (function(_this) {
- return function(data) {
- _this.fullData = data;
- _this.parseData(_this.fullData);
- _this.focusTextInput();
-
- // Update dropdown position since remote data may have changed dropdown size
- _this.dropdown.find('.dropdown-menu-toggle').dropdown('update');
-
- if (
- _this.options.filterable &&
- _this.filter &&
- _this.filter.input &&
- _this.filter.input.val() &&
- _this.filter.input.val().trim() !== ''
- ) {
- return _this.filter.input.trigger('input');
- }
- };
- // Remote data
- })(this),
- instance: this,
- });
- }
- }
- if (this.noFilterInput.length) {
- this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options);
- this.plainInput.onInput(this.addInput.bind(this));
- }
- // Init filterable
- if (this.options.filterable) {
- this.filter = new GitLabDropdownFilter(this.filterInput, {
- elIsInput: $(this.el).is('input'),
- filterInputBlur: this.filterInputBlur,
- filterByText: this.options.filterByText,
- onFilter: this.options.onFilter,
- remote: this.options.filterRemote,
- query: this.options.data,
- keys: searchFields,
+ });
+};
+
+function GitLabDropdown(el1, options) {
+ let selector, self;
+ this.el = el1;
+ this.options = options;
+ this.updateLabel = this.updateLabel.bind(this);
+ this.hidden = this.hidden.bind(this);
+ this.opened = this.opened.bind(this);
+ this.shouldPropagate = this.shouldPropagate.bind(this);
+ self = this;
+ selector = $(this.el).data('target');
+ this.dropdown = selector != null ? $(selector) : $(this.el).parent();
+ // Set Defaults
+ this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
+ this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
+ this.highlight = Boolean(this.options.highlight);
+ this.icon = Boolean(this.options.icon);
+ this.filterInputBlur = this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
+ // If no input is passed create a default one
+ self = this;
+ // If selector was passed
+ if (_.isString(this.filterInput)) {
+ this.filterInput = this.getElement(this.filterInput);
+ }
+ const searchFields = this.options.search ? this.options.search.fields : [];
+ if (this.options.data) {
+ // If we provided data
+ // data could be an array of objects or a group of arrays
+ if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
+ this.fullData = this.options.data;
+ currentIndex = -1;
+ this.parseData(this.options.data);
+ this.focusTextInput();
+ } else {
+ this.remote = new GitLabDropdownRemote(this.options.data, {
+ dataType: this.options.dataType,
+ beforeSend: this.toggleLoading.bind(this),
+ success: data => {
+ this.fullData = data;
+ this.parseData(this.fullData);
+ this.focusTextInput();
+
+ // Update dropdown position since remote data may have changed dropdown size
+ this.dropdown.find('.dropdown-menu-toggle').dropdown('update');
+
+ if (
+ this.options.filterable &&
+ this.filter &&
+ this.filter.input &&
+ this.filter.input.val() &&
+ this.filter.input.val().trim() !== ''
+ ) {
+ return this.filter.input.trigger('input');
+ }
+ },
instance: this,
- elements: (function(_this) {
- return function() {
- selector = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
- if (_this.dropdown.find('.dropdown-toggle-page').length) {
- selector = `.dropdown-page-one ${selector}`;
- }
- return $(selector, this.instance.dropdown);
- };
- })(this),
- data: (function(_this) {
- return function() {
- return _this.fullData;
- };
- })(this),
- callback: (function(_this) {
- return function(data) {
- _this.parseData(data);
- if (_this.filterInput.val() !== '') {
- selector = SELECTABLE_CLASSES;
- if (_this.dropdown.find('.dropdown-toggle-page').length) {
- selector = `.dropdown-page-one ${selector}`;
- }
- if ($(_this.el).is('input')) {
- currentIndex = -1;
- } else {
- $(selector, _this.dropdown)
- .first()
- .find('a')
- .addClass('is-focused');
- currentIndex = 0;
- }
- }
- };
- })(this),
});
}
- // Event listeners
- this.dropdown.on('shown.bs.dropdown', this.opened);
- this.dropdown.on('hidden.bs.dropdown', this.hidden);
- $(this.el).on('update.label', this.updateLabel);
- this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate);
- this.dropdown.on(
- 'keyup',
- (function(_this) {
- return function(e) {
- // Escape key
- if (e.which === 27) {
- return $('.dropdown-menu-close', _this.dropdown).trigger('click');
+ }
+ if (this.noFilterInput.length) {
+ this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options);
+ this.plainInput.onInput(this.addInput.bind(this));
+ }
+ // Init filterable
+ if (this.options.filterable) {
+ this.filter = new GitLabDropdownFilter(this.filterInput, {
+ elIsInput: $(this.el).is('input'),
+ filterInputBlur: this.filterInputBlur,
+ filterByText: this.options.filterByText,
+ onFilter: this.options.onFilter,
+ remote: this.options.filterRemote,
+ query: this.options.data,
+ keys: searchFields,
+ instance: this,
+ elements: () => {
+ selector = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = `.dropdown-page-one ${selector}`;
+ }
+ return $(selector, this.dropdown);
+ },
+ data: () => this.fullData,
+ callback: data => {
+ this.parseData(data);
+ if (this.filterInput.val() !== '') {
+ selector = SELECTABLE_CLASSES;
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = `.dropdown-page-one ${selector}`;
}
- };
- })(this),
- );
- this.dropdown.on(
- 'blur',
- 'a',
- (function(_this) {
- return function(e) {
- var $dropdownMenu, $relatedTarget;
- if (e.relatedTarget != null) {
- $relatedTarget = $(e.relatedTarget);
- $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
- if ($dropdownMenu.length === 0) {
- return _this.dropdown.removeClass('show');
- }
+ if ($(this.el).is('input')) {
+ currentIndex = -1;
+ } else {
+ $(selector, this.dropdown)
+ .first()
+ .find('a')
+ .addClass('is-focused');
+ currentIndex = 0;
}
- };
- })(this),
- );
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on(
- 'click',
- (function(_this) {
- return function(e) {
- e.preventDefault();
- e.stopPropagation();
- return _this.togglePage();
- };
- })(this),
- );
+ }
+ },
+ });
+ }
+ // Event listeners
+ this.dropdown.on('shown.bs.dropdown', this.opened);
+ this.dropdown.on('hidden.bs.dropdown', this.hidden);
+ $(this.el).on('update.label', this.updateLabel);
+ this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate);
+ this.dropdown.on('keyup', e => {
+ // Escape key
+ if (e.which === 27) {
+ return $('.dropdown-menu-close', this.dropdown).trigger('click');
}
- if (this.options.selectable) {
- selector = '.dropdown-content a';
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- selector = '.dropdown-page-one .dropdown-content a';
+ });
+ this.dropdown.on('blur', 'a', e => {
+ let $dropdownMenu, $relatedTarget;
+ if (e.relatedTarget != null) {
+ $relatedTarget = $(e.relatedTarget);
+ $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
+ if ($dropdownMenu.length === 0) {
+ return this.dropdown.removeClass('show');
+ }
+ }
+ });
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on('click', e => {
+ e.preventDefault();
+ e.stopPropagation();
+ return this.togglePage();
+ });
+ }
+ if (this.options.selectable) {
+ selector = '.dropdown-content a';
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = '.dropdown-page-one .dropdown-content a';
+ }
+ this.dropdown.on('click', selector, e => {
+ const $el = $(e.currentTarget);
+ const selected = self.rowClicked($el);
+ const selectedObj = selected ? selected[0] : null;
+ const isMarking = selected ? selected[1] : null;
+ if (this.options.clicked) {
+ this.options.clicked.call(this, {
+ selectedObj,
+ $el,
+ e,
+ isMarking,
+ });
}
- this.dropdown.on('click', selector, e => {
- var $el, selected, selectedObj, isMarking;
- $el = $(e.currentTarget);
- selected = self.rowClicked($el);
- selectedObj = selected ? selected[0] : null;
- isMarking = selected ? selected[1] : null;
- if (this.options.clicked) {
- this.options.clicked.call(this, {
- selectedObj,
- $el,
- e,
- isMarking,
- });
- }
- // Update label right after all modifications in dropdown has been done
- if (this.options.toggleLabel) {
- this.updateLabel(selectedObj, $el, this);
- }
+ // Update label right after all modifications in dropdown has been done
+ if (this.options.toggleLabel) {
+ this.updateLabel(selectedObj, $el, this);
+ }
- $el.trigger('blur');
- });
- }
+ $el.trigger('blur');
+ });
}
+}
- // Finds an element inside wrapper element
- GitLabDropdown.prototype.getElement = function(selector) {
- return this.dropdown.find(selector);
- };
+// Finds an element inside wrapper element
+GitLabDropdown.prototype.getElement = function(selector) {
+ return this.dropdown.find(selector);
+};
- GitLabDropdown.prototype.toggleLoading = function() {
- return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
- };
+GitLabDropdown.prototype.toggleLoading = function() {
+ return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
+};
- GitLabDropdown.prototype.togglePage = function() {
- var menu;
- menu = $('.dropdown-menu', this.dropdown);
- if (menu.hasClass(PAGE_TWO_CLASS)) {
- if (this.remote) {
- this.remote.execute();
- }
+GitLabDropdown.prototype.togglePage = function() {
+ const menu = $('.dropdown-menu', this.dropdown);
+ if (menu.hasClass(PAGE_TWO_CLASS)) {
+ if (this.remote) {
+ this.remote.execute();
}
- menu.toggleClass(PAGE_TWO_CLASS);
- // Focus first visible input on active page
- return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
- };
-
- GitLabDropdown.prototype.parseData = function(data) {
- var full_html, groupData, html, name;
- this.renderedData = data;
- if (this.options.filterable && data.length === 0) {
- // render no matching results
- html = [this.noResults()];
- } else {
- // Handle array groups
- if (isObject(data)) {
- html = [];
- for (name in data) {
- groupData = data[name];
- html.push(
- this.renderItem(
- {
- content: name,
- type: 'header',
- },
- name,
- ),
- );
- this.renderData(groupData, name).map(item => html.push(item));
- }
- } else {
- // Render each row
- html = this.renderData(data);
+ }
+ menu.toggleClass(PAGE_TWO_CLASS);
+ // Focus first visible input on active page
+ return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
+};
+
+GitLabDropdown.prototype.parseData = function(data) {
+ let groupData, html, name;
+ this.renderedData = data;
+ if (this.options.filterable && data.length === 0) {
+ // render no matching results
+ html = [this.noResults()];
+ } else {
+ // Handle array groups
+ if (isObject(data)) {
+ html = [];
+ for (name in data) {
+ groupData = data[name];
+ html.push(
+ this.renderItem(
+ {
+ content: name,
+ type: 'header',
+ },
+ name,
+ ),
+ );
+ this.renderData(groupData, name).map(item => html.push(item));
}
+ } else {
+ // Render each row
+ html = this.renderData(data);
}
- // Render the full menu
- full_html = this.renderMenu(html);
- return this.appendMenu(full_html);
- };
-
- GitLabDropdown.prototype.renderData = function(data, group) {
- return data.map((obj, index) => this.renderItem(obj, group || false, index));
- };
-
- GitLabDropdown.prototype.shouldPropagate = function(e) {
- var $target;
- if (this.options.multiSelect || this.options.shouldPropagate === false) {
- $target = $(e.target);
- if (
- $target &&
- !$target.hasClass('dropdown-menu-close') &&
- !$target.hasClass('dropdown-menu-close-icon') &&
- !$target.data('isLink')
- ) {
- e.stopPropagation();
-
- // This prevents automatic scrolling to the top
- if ($target.closest('a').length) {
- return false;
- }
- }
+ }
+ // Render the full menu
+ const full_html = this.renderMenu(html);
+ return this.appendMenu(full_html);
+};
+
+GitLabDropdown.prototype.renderData = function(data, group) {
+ return data.map((obj, index) => this.renderItem(obj, group || false, index));
+};
- return true;
+GitLabDropdown.prototype.shouldPropagate = function(e) {
+ let $target;
+ if (this.options.multiSelect || this.options.shouldPropagate === false) {
+ $target = $(e.target);
+ if (
+ $target &&
+ !$target.hasClass('dropdown-menu-close') &&
+ !$target.hasClass('dropdown-menu-close-icon') &&
+ !$target.data('isLink')
+ ) {
+ e.stopPropagation();
+
+ // This prevents automatic scrolling to the top
+ if ($target.closest('a').length) {
+ return false;
+ }
}
- };
-
- GitLabDropdown.prototype.filteredFullData = function() {
- return this.fullData.filter(
- r =>
- typeof r === 'object' &&
- !Object.prototype.hasOwnProperty.call(r, 'beforeDivider') &&
- !Object.prototype.hasOwnProperty.call(r, 'header'),
- );
- };
- GitLabDropdown.prototype.opened = function(e) {
- var contentHtml;
- this.resetRows();
- this.addArrowKeyEvent();
+ return true;
+ }
+};
- const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle');
- const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update');
- const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open');
- const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
+GitLabDropdown.prototype.filteredFullData = function() {
+ return this.fullData.filter(
+ r =>
+ typeof r === 'object' &&
+ !Object.prototype.hasOwnProperty.call(r, 'beforeDivider') &&
+ !Object.prototype.hasOwnProperty.call(r, 'header'),
+ );
+};
- // Makes indeterminate items effective
- if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) {
- this.parseData(this.fullData);
- }
+GitLabDropdown.prototype.opened = function(e) {
+ this.resetRows();
+ this.addArrowKeyEvent();
- // Process the data to make sure rendered data
- // matches the correct layout
- const inputValue = this.filterInput.val();
- if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
- this.options.processData.call(
- this.options,
- inputValue,
- this.filteredFullData(),
- this.parseData.bind(this),
- );
- }
+ const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle');
+ const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update');
+ const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open');
+ const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
- contentHtml = $('.dropdown-content', this.dropdown).html();
- if (this.remote && contentHtml === '') {
- this.remote.execute();
+ // Makes indeterminate items effective
+ if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) {
+ this.parseData(this.fullData);
+ }
+
+ // Process the data to make sure rendered data
+ // matches the correct layout
+ const inputValue = this.filterInput.val();
+ if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
+ this.options.processData.call(
+ this.options,
+ inputValue,
+ this.filteredFullData(),
+ this.parseData.bind(this),
+ );
+ }
+
+ const contentHtml = $('.dropdown-content', this.dropdown).html();
+ if (this.remote && contentHtml === '') {
+ this.remote.execute();
+ } else {
+ this.focusTextInput();
+ }
+
+ if (this.options.showMenuAbove) {
+ this.positionMenuAbove();
+ }
+
+ if (this.options.opened) {
+ if (this.options.preserveContext) {
+ this.options.opened(e);
} else {
- this.focusTextInput();
+ this.options.opened.call(this, e);
}
+ }
- if (this.options.showMenuAbove) {
- this.positionMenuAbove();
- }
+ return this.dropdown.trigger('shown.gl.dropdown');
+};
- if (this.options.opened) {
- if (this.options.preserveContext) {
- this.options.opened(e);
- } else {
- this.options.opened.call(this, e);
- }
- }
+GitLabDropdown.prototype.positionMenuAbove = function() {
+ const $menu = this.dropdown.find('.dropdown-menu');
- return this.dropdown.trigger('shown.gl.dropdown');
- };
+ $menu.addClass('dropdown-open-top');
+ $menu.css('top', 'initial');
+ $menu.css('bottom', '100%');
+};
- GitLabDropdown.prototype.positionMenuAbove = function() {
- var $menu = this.dropdown.find('.dropdown-menu');
+GitLabDropdown.prototype.hidden = function(e) {
+ this.resetRows();
+ this.removeArrowKeyEvent();
+ const $input = this.dropdown.find('.dropdown-input-field');
+ if (this.options.filterable) {
+ $input.blur();
+ }
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
+ }
+ if (this.options.hidden) {
+ this.options.hidden.call(this, e);
+ }
+ return this.dropdown.trigger('hidden.gl.dropdown');
+};
- $menu.addClass('dropdown-open-top');
- $menu.css('top', 'initial');
- $menu.css('bottom', '100%');
- };
+// Render the full menu
+GitLabDropdown.prototype.renderMenu = function(html) {
+ if (this.options.renderMenu) {
+ return this.options.renderMenu(html);
+ } else {
+ return $('<ul>').append(html);
+ }
+};
- GitLabDropdown.prototype.hidden = function(e) {
- var $input;
- this.resetRows();
- this.removeArrowKeyEvent();
- $input = this.dropdown.find('.dropdown-input-field');
- if (this.options.filterable) {
- $input.blur();
- }
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
- }
- if (this.options.hidden) {
- this.options.hidden.call(this, e);
- }
- return this.dropdown.trigger('hidden.gl.dropdown');
- };
+// Append the menu into the dropdown
+GitLabDropdown.prototype.appendMenu = function(html) {
+ return this.clearMenu().append(html);
+};
- // Render the full menu
- GitLabDropdown.prototype.renderMenu = function(html) {
- if (this.options.renderMenu) {
- return this.options.renderMenu(html);
+GitLabDropdown.prototype.clearMenu = function() {
+ let selector;
+ selector = '.dropdown-content';
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ if (this.options.containerSelector) {
+ selector = this.options.containerSelector;
} else {
- return $('<ul>').append(html);
+ selector = '.dropdown-page-one .dropdown-content';
}
- };
+ }
- // Append the menu into the dropdown
- GitLabDropdown.prototype.appendMenu = function(html) {
- return this.clearMenu().append(html);
- };
+ return $(selector, this.dropdown).empty();
+};
- GitLabDropdown.prototype.clearMenu = function() {
- var selector;
- selector = '.dropdown-content';
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- if (this.options.containerSelector) {
- selector = this.options.containerSelector;
- } else {
- selector = '.dropdown-page-one .dropdown-content';
- }
- }
+GitLabDropdown.prototype.renderItem = function(data, group, index) {
+ let parent;
- return $(selector, this.dropdown).empty();
- };
+ if (this.dropdown && this.dropdown[0]) {
+ parent = this.dropdown[0].parentNode;
+ }
- GitLabDropdown.prototype.renderItem = function(data, group, index) {
- let parent;
+ return renderItem({
+ instance: this,
+ options: Object.assign({}, this.options, {
+ icon: this.icon,
+ highlight: this.highlight,
+ highlightText: text => this.highlightTextMatches(text, this.filterInput.val()),
+ highlightTemplate: this.highlightTemplate.bind(this),
+ parent,
+ }),
+ data,
+ group,
+ index,
+ });
+};
- if (this.dropdown && this.dropdown[0]) {
- parent = this.dropdown[0].parentNode;
- }
+GitLabDropdown.prototype.highlightTemplate = function(text, template) {
+ return `"<b>${_.escape(text)}</b>" ${template}`;
+};
- return renderItem({
- instance: this,
- options: Object.assign({}, this.options, {
- icon: this.icon,
- highlight: this.highlight,
- highlightText: text => this.highlightTextMatches(text, this.filterInput.val()),
- highlightTemplate: this.highlightTemplate.bind(this),
- parent,
- }),
- data,
- group,
- index,
- });
- };
-
- GitLabDropdown.prototype.highlightTemplate = function(text, template) {
- return `"<b>${_.escape(text)}</b>" ${template}`;
- };
-
- GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
- const occurrences = fuzzaldrinPlus.match(text, term);
- const { indexOf } = [];
-
- return text
- .split('')
- .map((character, i) => {
- if (indexOf.call(occurrences, i) !== -1) {
- return `<b>${character}</b>`;
- } else {
- return character;
- }
- })
- .join('');
- };
-
- GitLabDropdown.prototype.noResults = function() {
- return '<li class="dropdown-menu-empty-item"><a>No matching results</a></li>';
- };
-
- GitLabDropdown.prototype.rowClicked = function(el) {
- var field, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
-
- const { fieldName } = this.options;
- isInput = $(this.el).is('input');
- if (this.renderedData) {
- groupName = el.data('group');
- if (groupName) {
- selectedIndex = el.data('index');
- selectedObject = this.renderedData[groupName][selectedIndex];
- } else {
- selectedIndex = el.closest('li').index();
- this.selectedIndex = selectedIndex;
- selectedObject = this.renderedData[selectedIndex];
- }
- }
+GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
+ const occurrences = fuzzaldrinPlus.match(text, term);
+ const { indexOf } = [];
- if (this.options.vue) {
- if (el.hasClass(ACTIVE_CLASS)) {
- el.removeClass(ACTIVE_CLASS);
+ return text
+ .split('')
+ .map((character, i) => {
+ if (indexOf.call(occurrences, i) !== -1) {
+ return `<b>${character}</b>`;
} else {
- el.addClass(ACTIVE_CLASS);
+ return character;
}
+ })
+ .join('');
+};
- return [selectedObject];
- }
-
- field = [];
- value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
- if (isInput) {
- field = $(this.el);
- } else if (value != null) {
- field = this.dropdown
- .parent()
- .find(`input[name='${fieldName}'][value='${value.toString().replace(/'/g, "\\'")}']`);
- }
+GitLabDropdown.prototype.noResults = function() {
+ return '<li class="dropdown-menu-empty-item"><a>No matching results</a></li>';
+};
- if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
- return [selectedObject];
+GitLabDropdown.prototype.rowClicked = function(el) {
+ let field, groupName, selectedIndex, selectedObject, isMarking;
+ const { fieldName } = this.options;
+ const isInput = $(this.el).is('input');
+ if (this.renderedData) {
+ groupName = el.data('group');
+ if (groupName) {
+ selectedIndex = el.data('index');
+ selectedObject = this.renderedData[groupName][selectedIndex];
+ } else {
+ selectedIndex = el.closest('li').index();
+ this.selectedIndex = selectedIndex;
+ selectedObject = this.renderedData[selectedIndex];
}
+ }
- if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
- isMarking = false;
+ if (this.options.vue) {
+ if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS);
- if (field && field.length) {
- this.clearField(field, isInput);
- }
- } else if (el.hasClass(INDETERMINATE_CLASS)) {
- isMarking = true;
- el.addClass(ACTIVE_CLASS);
- el.removeClass(INDETERMINATE_CLASS);
- if (field && field.length && value == null) {
- this.clearField(field, isInput);
- }
- if ((!field || !field.length) && fieldName) {
- this.addInput(fieldName, value, selectedObject);
- }
} else {
- isMarking = true;
- if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
- this.dropdown.find(`.${ACTIVE_CLASS}`).removeClass(ACTIVE_CLASS);
- if (!isInput) {
- this.dropdown
- .parent()
- .find(`input[name='${fieldName}']`)
- .remove();
- }
- }
- if (field && field.length && value == null) {
- this.clearField(field, isInput);
- }
- // Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
- if (value != null) {
- if ((!field || !field.length) && fieldName) {
- this.addInput(fieldName, value, selectedObject);
- } else if (field && field.length) {
- field.val(value).trigger('change');
- }
- }
}
- return [selectedObject, isMarking];
- };
+ return [selectedObject];
+ }
+
+ field = [];
+ const value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
+ if (isInput) {
+ field = $(this.el);
+ } else if (value != null) {
+ field = this.dropdown
+ .parent()
+ .find(`input[name='${fieldName}'][value='${value.toString().replace(/'/g, "\\'")}']`);
+ }
- GitLabDropdown.prototype.focusTextInput = function() {
- if (this.options.filterable) {
- const initialScrollTop = $(window).scrollTop();
+ if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
+ return [selectedObject];
+ }
- if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) {
- this.filterInput.focus();
+ if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
+ isMarking = false;
+ el.removeClass(ACTIVE_CLASS);
+ if (field && field.length) {
+ this.clearField(field, isInput);
+ }
+ } else if (el.hasClass(INDETERMINATE_CLASS)) {
+ isMarking = true;
+ el.addClass(ACTIVE_CLASS);
+ el.removeClass(INDETERMINATE_CLASS);
+ if (field && field.length && value == null) {
+ this.clearField(field, isInput);
+ }
+ if ((!field || !field.length) && fieldName) {
+ this.addInput(fieldName, value, selectedObject);
+ }
+ } else {
+ isMarking = true;
+ if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
+ this.dropdown.find(`.${ACTIVE_CLASS}`).removeClass(ACTIVE_CLASS);
+ if (!isInput) {
+ this.dropdown
+ .parent()
+ .find(`input[name='${fieldName}']`)
+ .remove();
}
-
- if ($(window).scrollTop() < initialScrollTop) {
- $(window).scrollTop(initialScrollTop);
+ }
+ if (field && field.length && value == null) {
+ this.clearField(field, isInput);
+ }
+ // Toggle active class for the tick mark
+ el.addClass(ACTIVE_CLASS);
+ if (value != null) {
+ if ((!field || !field.length) && fieldName) {
+ this.addInput(fieldName, value, selectedObject);
+ } else if (field && field.length) {
+ field.val(value).trigger('change');
}
}
- };
+ }
- GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject, single) {
- var $input;
- // Create hidden input for form
- if (single) {
- $(`input[name="${fieldName}"]`).remove();
- }
+ return [selectedObject, isMarking];
+};
- $input = $('<input>')
- .attr('type', 'hidden')
- .attr('name', fieldName)
- .val(value);
- if (this.options.inputId != null) {
- $input.attr('id', this.options.inputId);
- }
+GitLabDropdown.prototype.focusTextInput = function() {
+ if (this.options.filterable) {
+ const initialScrollTop = $(window).scrollTop();
- if (this.options.multiSelect) {
- Object.keys(selectedObject).forEach(attribute => {
- $input.attr(`data-${attribute}`, selectedObject[attribute]);
- });
+ if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) {
+ this.filterInput.focus();
}
- if (this.options.inputMeta) {
- $input.attr('data-meta', selectedObject[this.options.inputMeta]);
+ if ($(window).scrollTop() < initialScrollTop) {
+ $(window).scrollTop(initialScrollTop);
}
+ }
+};
+
+GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject, single) {
+ // Create hidden input for form
+ if (single) {
+ $(`input[name="${fieldName}"]`).remove();
+ }
+
+ const $input = $('<input>')
+ .attr('type', 'hidden')
+ .attr('name', fieldName)
+ .val(value);
+ if (this.options.inputId != null) {
+ $input.attr('id', this.options.inputId);
+ }
- this.dropdown.before($input).trigger('change');
- };
+ if (this.options.multiSelect) {
+ Object.keys(selectedObject).forEach(attribute => {
+ $input.attr(`data-${attribute}`, selectedObject[attribute]);
+ });
+ }
+
+ if (this.options.inputMeta) {
+ $input.attr('data-meta', selectedObject[this.options.inputMeta]);
+ }
- GitLabDropdown.prototype.selectRowAtIndex = function(index) {
- var $el, selector;
- // If we pass an option index
- if (typeof index !== 'undefined') {
- selector = `${SELECTABLE_CLASSES}:eq(${index}) a`;
+ this.dropdown.before($input).trigger('change');
+};
+
+GitLabDropdown.prototype.selectRowAtIndex = function(index) {
+ let selector;
+ // If we pass an option index
+ if (typeof index !== 'undefined') {
+ selector = `${SELECTABLE_CLASSES}:eq(${index}) a`;
+ } else {
+ selector = '.dropdown-content .is-focused';
+ }
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = `.dropdown-page-one ${selector}`;
+ }
+ // simulate a click on the first link
+ const $el = $(selector, this.dropdown);
+ if ($el.length) {
+ const href = $el.attr('href');
+ if (href && href !== '#') {
+ visitUrl(href);
} else {
- selector = '.dropdown-content .is-focused';
+ $el.trigger('click');
}
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- selector = `.dropdown-page-one ${selector}`;
- }
- // simulate a click on the first link
- $el = $(selector, this.dropdown);
- if ($el.length) {
- var href = $el.attr('href');
- if (href && href !== '#') {
- visitUrl(href);
- } else {
- $el.trigger('click');
- }
- }
- };
+ }
+};
- GitLabDropdown.prototype.addArrowKeyEvent = function() {
- var ARROW_KEY_CODES, selector;
- ARROW_KEY_CODES = [38, 40];
- selector = SELECTABLE_CLASSES;
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- selector = `.dropdown-page-one ${selector}`;
+GitLabDropdown.prototype.addArrowKeyEvent = function() {
+ let selector;
+ const ARROW_KEY_CODES = [38, 40];
+ selector = SELECTABLE_CLASSES;
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = `.dropdown-page-one ${selector}`;
+ }
+ return $('body').on('keydown', e => {
+ let $listItems, PREV_INDEX;
+ const currentKeyCode = e.which;
+ if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ PREV_INDEX = currentIndex;
+ $listItems = $(selector, this.dropdown);
+ // if @options.filterable
+ // $input.blur()
+ if (currentKeyCode === 40) {
+ // Move down
+ if (currentIndex < $listItems.length - 1) {
+ currentIndex += 1;
+ }
+ } else if (currentKeyCode === 38) {
+ // Move up
+ if (currentIndex > 0) {
+ currentIndex -= 1;
+ }
+ }
+ if (currentIndex !== PREV_INDEX) {
+ this.highlightRowAtIndex($listItems, currentIndex);
+ }
+ return false;
}
- return $('body').on(
- 'keydown',
- (function(_this) {
- return function(e) {
- var $listItems, PREV_INDEX, currentKeyCode;
- currentKeyCode = e.which;
- if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
- e.preventDefault();
- e.stopImmediatePropagation();
- PREV_INDEX = currentIndex;
- $listItems = $(selector, _this.dropdown);
- // if @options.filterable
- // $input.blur()
- if (currentKeyCode === 40) {
- // Move down
- if (currentIndex < $listItems.length - 1) {
- currentIndex += 1;
- }
- } else if (currentKeyCode === 38) {
- // Move up
- if (currentIndex > 0) {
- currentIndex -= 1;
- }
- }
- if (currentIndex !== PREV_INDEX) {
- _this.highlightRowAtIndex($listItems, currentIndex);
- }
- return false;
- }
- if (currentKeyCode === 13 && currentIndex !== -1) {
- e.preventDefault();
- _this.selectRowAtIndex();
- }
- };
- })(this),
- );
- };
-
- GitLabDropdown.prototype.removeArrowKeyEvent = function() {
- return $('body').off('keydown');
- };
-
- GitLabDropdown.prototype.resetRows = function resetRows() {
- currentIndex = -1;
- $('.is-focused', this.dropdown).removeClass('is-focused');
- };
-
- GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
- var $dropdownContent,
- $listItem,
- dropdownContentBottom,
- dropdownContentHeight,
- dropdownContentTop,
- dropdownScrollTop,
- listItemBottom,
- listItemHeight,
- listItemTop;
-
- if (!$listItems) {
- $listItems = $(SELECTABLE_CLASSES, this.dropdown);
+ if (currentKeyCode === 13 && currentIndex !== -1) {
+ e.preventDefault();
+ this.selectRowAtIndex();
}
+ });
+};
- // Remove the class for the previously focused row
- $('.is-focused', this.dropdown).removeClass('is-focused');
- // Update the class for the row at the specific index
- $listItem = $listItems.eq(index);
- $listItem.find('a:first-child').addClass('is-focused');
- // Dropdown content scroll area
- $dropdownContent = $listItem.closest('.dropdown-content');
- dropdownScrollTop = $dropdownContent.scrollTop();
- dropdownContentHeight = $dropdownContent.outerHeight();
- dropdownContentTop = $dropdownContent.prop('offsetTop');
- dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
- // Get the offset bottom of the list item
- listItemHeight = $listItem.outerHeight();
- listItemTop = $listItem.prop('offsetTop');
- listItemBottom = listItemTop + listItemHeight;
- if (!index) {
- // Scroll the dropdown content to the top
- $dropdownContent.scrollTop(0);
- } else if (index === $listItems.length - 1) {
- // Scroll the dropdown content to the bottom
- $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
- } else if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
- // Scroll the dropdown content down
- $dropdownContent.scrollTop(
- listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING,
- );
- } else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
- // Scroll the dropdown content up
- return $dropdownContent.scrollTop(
- listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING,
- );
- }
- };
+GitLabDropdown.prototype.removeArrowKeyEvent = function() {
+ return $('body').off('keydown');
+};
- GitLabDropdown.prototype.updateLabel = function(selected, el, instance) {
- if (selected == null) {
- selected = null;
- }
- if (el == null) {
- el = null;
- }
- if (instance == null) {
- instance = null;
- }
+GitLabDropdown.prototype.resetRows = function resetRows() {
+ currentIndex = -1;
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+};
- let toggleText = this.options.toggleLabel(selected, el, instance);
- if (this.options.updateLabel) {
- // Option to override the dropdown label text
- toggleText = this.options.updateLabel;
- }
+GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
+ if (!$listItems) {
+ $listItems = $(SELECTABLE_CLASSES, this.dropdown);
+ }
+
+ // Remove the class for the previously focused row
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ // Update the class for the row at the specific index
+ const $listItem = $listItems.eq(index);
+ $listItem.find('a:first-child').addClass('is-focused');
+ // Dropdown content scroll area
+ const $dropdownContent = $listItem.closest('.dropdown-content');
+ const dropdownScrollTop = $dropdownContent.scrollTop();
+ const dropdownContentHeight = $dropdownContent.outerHeight();
+ const dropdownContentTop = $dropdownContent.prop('offsetTop');
+ const dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
+ // Get the offset bottom of the list item
+ const listItemHeight = $listItem.outerHeight();
+ const listItemTop = $listItem.prop('offsetTop');
+ const listItemBottom = listItemTop + listItemHeight;
+ if (!index) {
+ // Scroll the dropdown content to the top
+ $dropdownContent.scrollTop(0);
+ } else if (index === $listItems.length - 1) {
+ // Scroll the dropdown content to the bottom
+ $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+ } else if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
+ // Scroll the dropdown content down
+ $dropdownContent.scrollTop(
+ listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING,
+ );
+ } else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
+ // Scroll the dropdown content up
+ return $dropdownContent.scrollTop(
+ listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING,
+ );
+ }
+};
+
+GitLabDropdown.prototype.updateLabel = function(selected, el, instance) {
+ if (selected == null) {
+ selected = null;
+ }
+ if (el == null) {
+ el = null;
+ }
+ if (instance == null) {
+ instance = null;
+ }
- return $(this.el)
- .find('.dropdown-toggle-text')
- .text(toggleText);
- };
+ let toggleText = this.options.toggleLabel(selected, el, instance);
+ if (this.options.updateLabel) {
+ // Option to override the dropdown label text
+ toggleText = this.options.updateLabel;
+ }
- GitLabDropdown.prototype.clearField = function(field, isInput) {
- return isInput ? field.val('') : field.remove();
- };
+ return $(this.el)
+ .find('.dropdown-toggle-text')
+ .text(toggleText);
+};
- return GitLabDropdown;
-})();
+GitLabDropdown.prototype.clearField = function(field, isInput) {
+ return isInput ? field.val('') : field.remove();
+};
$.fn.glDropdown = function(opts) {
return this.each(function() {
diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
index bd504d95ee2..6258ee7f153 100644
--- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
+++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox, GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
import { mapState, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 8d2dac47ff2..ce6591e85cf 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -2,13 +2,13 @@
/* global Flash */
import $ from 'jquery';
+import { GlLoadingIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { GlLoadingIcon } from '@gitlab/ui';
import eventHub from '../event_hub';
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
import groupsComponent from './groups.vue';
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index 734a9a89c72..675552e6c2b 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -1,5 +1,6 @@
<script>
import icon from '~/vue_shared/components/icon.vue';
+import { GlBadge } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
ITEM_TYPE,
@@ -8,13 +9,16 @@ import {
PROJECT_VISIBILITY_TYPE,
} from '../constants';
import itemStatsValue from './item_stats_value.vue';
+import isProjectPendingRemoval from 'ee_else_ce/groups/mixins/is_project_pending_removal';
export default {
components: {
icon,
timeAgoTooltip,
itemStatsValue,
+ GlBadge,
},
+ mixins: [isProjectPendingRemoval],
props: {
item: {
type: Object,
@@ -70,6 +74,9 @@ export default {
css-class="project-stars"
icon-name="star"
/>
+ <div v-if="isProjectPendingRemoval">
+ <gl-badge variant="warning">{{ __('pending removal') }}</gl-badge>
+ </div>
<div v-if="isProject" class="last-updated">
<time-ago-tooltip :time="item.updatedAt" tooltip-placement="bottom" />
</div>
diff --git a/app/assets/javascripts/groups/mixins/is_project_pending_removal.js b/app/assets/javascripts/groups/mixins/is_project_pending_removal.js
new file mode 100644
index 00000000000..e44e5780199
--- /dev/null
+++ b/app/assets/javascripts/groups/mixins/is_project_pending_removal.js
@@ -0,0 +1,7 @@
+export default {
+ computed: {
+ isProjectPendingRemoval() {
+ return false;
+ },
+ },
+};
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
index 16f95d5a0cc..214ac5e3db5 100644
--- a/app/assets/javascripts/groups/store/groups_store.js
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -93,6 +93,7 @@ export default class GroupsStore {
memberCount: rawGroupItem.number_users_with_delimiter,
starCount: rawGroupItem.star_count,
updatedAt: rawGroupItem.updated_at,
+ pendingRemoval: rawGroupItem.marked_for_deletion_at,
};
}
diff --git a/app/assets/javascripts/helpers/monitor_helper.js b/app/assets/javascripts/helpers/monitor_helper.js
index d172aa8a444..87b4b14f6bf 100644
--- a/app/assets/javascripts/helpers/monitor_helper.js
+++ b/app/assets/javascripts/helpers/monitor_helper.js
@@ -7,6 +7,7 @@
export const makeDataSeries = (queryResults, defaultConfig) =>
queryResults
.map(result => {
+ // NaN values may disrupt avg., max. & min. calculations in the legend, filter them out
const data = result.values.filter(([, value]) => !Number.isNaN(value));
if (!data.length) {
return null;
diff --git a/app/assets/javascripts/ide/.eslintrc.yml b/app/assets/javascripts/ide/.eslintrc.yml
index 92b96d717be..2af5c1798f5 100644
--- a/app/assets/javascripts/ide/.eslintrc.yml
+++ b/app/assets/javascripts/ide/.eslintrc.yml
@@ -1,3 +1,5 @@
rules:
+ # https://gitlab.com/gitlab-org/gitlab/issues/28717
+ import/no-cycle: off
# https://gitlab.com/gitlab-org/gitlab/issues/33024
promise/no-nesting: off
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
index db8365a08e0..31f1dec43ad 100644
--- a/app/assets/javascripts/ide/components/branches/search_list.vue
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
-import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
import Item from './item.vue';
export default {
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
index 343e0cca672..35e5f9bcf69 100644
--- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -1,8 +1,8 @@
<script>
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
-import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import { GlLoadingIcon } from '@gitlab/ui';
+import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 326589fa50f..6eaf08e8033 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -22,7 +22,7 @@ export default {
mixins: [timeAgoMixin],
data() {
return {
- lastCommitFormatedAge: null,
+ lastCommitFormattedAge: null,
};
},
computed: {
@@ -62,7 +62,7 @@ export default {
},
commitAgeUpdate() {
if (this.lastCommit) {
- this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
+ this.lastCommitFormattedAge = this.timeFormatted(this.lastCommit.committed_date);
}
},
getCommitPath(shortSha) {
@@ -118,7 +118,7 @@ export default {
:title="tooltipTitle(lastCommit.committed_date)"
data-placement="top"
data-container="body"
- >{{ lastCommitFormatedAge }}</time
+ >{{ lastCommitFormattedAge }}</time
>
</div>
<ide-status-list class="ml-auto" />
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 95782b2c88a..3a0dd60f0e0 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
import { GlSkeletonLoading } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import NavDropdown from './nav_dropdown.vue';
import FileRowExtra from './file_row_extra.vue';
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index 5daf2d1422c..5a8face062b 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -1,9 +1,9 @@
<script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
+import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index d2ed1fe3e55..ecafb4e81c4 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,8 +1,8 @@
<script>
import $ from 'jquery';
+import { mapActions, mapState, mapGetters } from 'vuex';
import flash from '~/flash';
import { __, sprintf, s__ } from '~/locale';
-import { mapActions, mapState, mapGetters } from 'vuex';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { modalTypes } from '../../constants';
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
index bc80e1dba25..ff23485f0f0 100644
--- a/app/assets/javascripts/ide/components/preview/navigator.vue
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -1,7 +1,7 @@
<script>
import { listen } from 'codesandbox-api';
-import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 7615cfc966e..8370833233a 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -1,6 +1,6 @@
<script>
-import { __, sprintf } from '~/locale';
import { mapActions } from 'vuex';
+import { __, sprintf } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js
index e86dac20104..bee867fa47c 100644
--- a/app/assets/javascripts/ide/lib/files.js
+++ b/app/assets/javascripts/ide/lib/files.js
@@ -1,5 +1,6 @@
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
-import { decorateData, sortTree, escapeFileUrl } from '../stores/utils';
+import { escapeFileUrl } from '~/lib/utils/url_utility';
+import { decorateData, sortTree } from '../stores/utils';
export const splitParent = path => {
const idx = path.lastIndexOf('/');
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index f6ad2f9c7d1..b130e6e8b81 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,6 +1,5 @@
import axios from '~/lib/utils/axios_utils';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { escapeFileUrl } from '../stores/utils';
+import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import Api from '~/api';
export default {
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 4e18ec58feb..dd69e2d6f1f 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import Vue from 'vue';
+import _ from 'underscore';
import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
-import _ from 'underscore';
import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
@@ -17,10 +17,18 @@ export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DAT
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => {
- commit(types.DISCARD_FILE_CHANGES, file.path);
+ if (file.tempFile || file.prevPath) dispatch('closeFile', file);
if (file.tempFile) {
- dispatch('closeFile', file.path);
+ dispatch('deleteEntry', file.path);
+ } else if (file.prevPath) {
+ dispatch('renameEntry', {
+ path: file.path,
+ name: file.prevName,
+ parentPath: file.prevParentPath,
+ });
+ } else {
+ commit(types.DISCARD_FILE_CHANGES, file.path);
}
});
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 9af0b50d1a5..8864224c19e 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -1,10 +1,10 @@
-import { joinPaths } from '~/lib/utils/url_utility';
+import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
-import { escapeFileUrl, addFinalNewlineIfNeeded, setPageTitleForFile } from '../utils';
+import { addFinalNewlineIfNeeded, setPageTitleForFile } from '../utils';
import { viewerTypes, stageKeys } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index a176fd0aca8..bb8374b4e78 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -115,5 +115,30 @@ export const isOnDefaultBranch = (_state, getters) =>
export const canPushToBranch = (_state, getters) =>
getters.currentBranch && getters.currentBranch.can_push;
+export const isFileDeletedAndReadded = (state, getters) => path => {
+ const stagedFile = getters.getStagedFile(path);
+ const file = state.entries[path];
+ return Boolean(stagedFile && stagedFile.deleted && file.tempFile);
+};
+
+// checks if any diff exists in the staged or unstaged changes for this path
+export const getDiffInfo = (state, getters) => path => {
+ const stagedFile = getters.getStagedFile(path);
+ const file = state.entries[path];
+ const renamed = file.prevPath ? file.path !== file.prevPath : false;
+ const deletedAndReadded = getters.isFileDeletedAndReadded(path);
+ const deleted = deletedAndReadded ? false : file.deleted;
+ const tempFile = deletedAndReadded ? false : file.tempFile;
+ const changed = file.content !== (deletedAndReadded ? stagedFile.raw : file.raw);
+
+ return {
+ exists: changed || renamed || deleted || tempFile,
+ changed,
+ renamed,
+ deleted,
+ tempFile,
+ };
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index be7ee80656f..47a2e6b5202 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -1,4 +1,5 @@
import { commitActionTypes, FILE_VIEW_MODE_EDITOR } from '../constants';
+import { escapeFileUrl } from '~/lib/utils/url_utility';
export const dataStructure = () => ({
id: '',
@@ -217,8 +218,6 @@ export const mergeTrees = (fromTree, toTree) => {
return toTree;
};
-export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
-
export const replaceFileUrl = (url, oldPath, newPath) => {
// Add `/-/` so that we don't accidentally replace project path
const result = url.replace(`/-/${escapeFileUrl(oldPath)}`, `/-/${escapeFileUrl(newPath)}`);
diff --git a/app/assets/javascripts/image_diff/.eslintrc.yml b/app/assets/javascripts/image_diff/.eslintrc.yml
new file mode 100644
index 00000000000..bf9e184381e
--- /dev/null
+++ b/app/assets/javascripts/image_diff/.eslintrc.yml
@@ -0,0 +1,3 @@
+rules:
+ # https://gitlab.com/gitlab-org/gitlab/issues/28719
+ import/no-cycle: off
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 7576d36f27d..1d0807dc15d 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -6,6 +6,36 @@ import UsersSelect from './users_select';
import ZenMode from './zen_mode';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
+import { queryToObject, objectToQuery } from './lib/utils/url_utility';
+
+function organizeQuery(obj, isFallbackKey = false) {
+ const sourceBranch = 'merge_request[source_branch]';
+ const targetBranch = 'merge_request[target_branch]';
+
+ if (isFallbackKey) {
+ return {
+ [sourceBranch]: obj[sourceBranch],
+ };
+ }
+
+ return {
+ [sourceBranch]: obj[sourceBranch],
+ [targetBranch]: obj[targetBranch],
+ };
+}
+
+function format(searchTerm, isFallbackKey = false) {
+ const queryObject = queryToObject(searchTerm);
+ const organizeQueryObject = organizeQuery(queryObject, isFallbackKey);
+ const formattedQuery = objectToQuery(organizeQueryObject);
+
+ return formattedQuery;
+}
+
+function getFallbackKey() {
+ const searchTerm = format(document.location.search, true);
+ return ['autosave', document.location.pathname, searchTerm].join('/');
+}
export default class IssuableForm {
constructor(form) {
@@ -57,16 +87,20 @@ export default class IssuableForm {
}
initAutosave() {
- this.autosave = new Autosave(this.titleField, [
- document.location.pathname,
- document.location.search,
- 'title',
- ]);
- return new Autosave(this.descriptionField, [
- document.location.pathname,
- document.location.search,
- 'description',
- ]);
+ const searchTerm = format(document.location.search);
+ const fallbackKey = getFallbackKey();
+
+ this.autosave = new Autosave(
+ this.titleField,
+ [document.location.pathname, searchTerm, 'title'],
+ `${fallbackKey}=title`,
+ );
+
+ return new Autosave(
+ this.descriptionField,
+ [document.location.pathname, searchTerm, 'description'],
+ `${fallbackKey}=description`,
+ );
}
handleSubmit() {
diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issuable_suggestions/components/item.vue
index 7629e04684c..66a4cc44d51 100644
--- a/app/assets/javascripts/issuable_suggestions/components/item.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/item.vue
@@ -91,7 +91,7 @@ export default {
/>
<gl-tooltip :target="() => $refs.state" placement="bottom">
<span class="d-block">
- <span class="bold"> {{ stateTitle }} </span> {{ timeFormated(closedOrCreatedDate) }}
+ <span class="bold"> {{ stateTitle }} </span> {{ timeFormatted(closedOrCreatedDate) }}
</span>
<span class="text-tertiary">{{ tooltipTitle(closedOrCreatedDate) }}</span>
</gl-tooltip>
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 06477477aad..415fa46835b 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -1,8 +1,8 @@
<script>
-import { __, sprintf } from '~/locale';
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import { GlLink, GlButton } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
@@ -168,13 +168,13 @@ export default {
/>
<detail-row
v-if="job.finished_at"
- :value="timeFormated(job.finished_at)"
+ :value="timeFormatted(job.finished_at)"
class="js-job-finished"
title="Finished"
/>
<detail-row
v-if="job.erased_at"
- :value="timeFormated(job.erased_at)"
+ :value="timeFormatted(job.erased_at)"
class="js-job-erased"
title="Erased"
/>
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 922f64d93fe..5edb8ff555b 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
const HIDDEN_VALUE = '••••••';
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js
index 179d0bc4e0f..0b28c52a78f 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/jobs/store/utils.js
@@ -114,7 +114,7 @@ export const logLinesParser = (lines = [], accumulator = []) =>
acc.push(parseHeaderLine(line, lineNumber));
} else if (isCollapsibleSection(acc, last, line)) {
// if the object belongs to a nested section, we append it to the new `lines` array of the
- // previously formated header
+ // previously formatted header
last.lines.push(parseLine(line, lineNumber));
} else if (line.section_duration) {
// if the line has section_duration, we look for the correct header to add it
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index a04fe609015..4eec5bffc66 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -33,11 +33,9 @@ window.addEventListener('beforeunload', () => {
// Ignore AJAX errors caused by requests
// being cancelled due to browser navigation
-const { gon } = window;
-const featureFlagEnabled = gon && gon.features && gon.features.suppressAjaxNavigationErrors;
axios.interceptors.response.use(
response => response,
- err => suppressAjaxErrorsDuringNavigation(err, isUserNavigating, featureFlagEnabled),
+ err => suppressAjaxErrorsDuringNavigation(err, isUserNavigating),
);
export default axios;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 177ae4f9838..e4001e94478 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -5,7 +5,7 @@
import $ from 'jquery';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
-import { convertToCamelCase } from './text_utility';
+import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import breakpointInstance from '../../breakpoints';
@@ -490,6 +490,8 @@ export const historyPushState = newUrl => {
*/
export const parseBoolean = value => (value && value.toString()) === 'true';
+export const BACKOFF_TIMEOUT = 'BACKOFF_TIMEOUT';
+
/**
* @callback backOffCallback
* @param {Function} next
@@ -541,7 +543,7 @@ export const backOff = (fn, timeout = 60000) => {
timeElapsed += nextInterval;
nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
} else {
- reject(new Error('BACKOFF_TIMEOUT'));
+ reject(new Error(BACKOFF_TIMEOUT));
}
};
@@ -697,6 +699,22 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
}, initial);
};
+/**
+ * Converts all the object keys to snake case
+ *
+ * @param {Object} obj Object to transform
+ * @returns {Object}
+ */
+// Follow up to add additional options param:
+// https://gitlab.com/gitlab-org/gitlab/issues/39173
+export const convertObjectPropsToSnakeCase = (obj = {}) =>
+ obj
+ ? Object.entries(obj).reduce(
+ (acc, [key, value]) => ({ ...acc, [convertToSnakeCase(key)]: value }),
+ {},
+ )
+ : {};
+
export const imagePath = imgUrl =>
`${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 28143859e4c..996692bacb3 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import _ from 'underscore';
-import timeago from 'timeago.js';
+import * as timeago from 'timeago.js';
import dateFormat from 'dateformat';
import { languageCode, s__, __, n__ } from '../../locale';
@@ -92,90 +92,80 @@ export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => {
*/
const timeagoLanguageCode = languageCode().replace(/-/g, '_');
-let timeagoInstance;
-
/**
- * Sets a timeago Instance
+ * Registers timeago locales
*/
-export const getTimeago = () => {
- if (!timeagoInstance) {
- const memoizedLocaleRemaining = () => {
- const cache = [];
-
- const timeAgoLocaleRemaining = [
- () => [s__('Timeago|just now'), s__('Timeago|right now')],
- () => [s__('Timeago|just now'), s__('Timeago|%s seconds remaining')],
- () => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
- () => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
- () => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
- () => [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
- () => [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
- () => [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')],
- () => [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')],
- () => [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')],
- () => [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')],
- () => [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')],
- () => [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
- () => [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
- ];
-
- return (number, index) => {
- if (cache[index]) {
- return cache[index];
- }
- cache[index] = timeAgoLocaleRemaining[index] && timeAgoLocaleRemaining[index]();
- return cache[index];
- };
- };
-
- const memoizedLocale = () => {
- const cache = [];
-
- const timeAgoLocale = [
- () => [s__('Timeago|just now'), s__('Timeago|right now')],
- () => [s__('Timeago|just now'), s__('Timeago|in %s seconds')],
- () => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
- () => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
- () => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
- () => [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
- () => [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
- () => [s__('Timeago|%s days ago'), s__('Timeago|in %s days')],
- () => [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')],
- () => [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')],
- () => [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')],
- () => [s__('Timeago|%s months ago'), s__('Timeago|in %s months')],
- () => [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
- () => [s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
- ];
-
- return (number, index) => {
- if (cache[index]) {
- return cache[index];
- }
- cache[index] = timeAgoLocale[index] && timeAgoLocale[index]();
- return cache[index];
- };
- };
-
- timeago.register(timeagoLanguageCode, memoizedLocale());
- timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
-
- timeagoInstance = timeago();
- }
+const memoizedLocaleRemaining = () => {
+ const cache = [];
+
+ const timeAgoLocaleRemaining = [
+ () => [s__('Timeago|just now'), s__('Timeago|right now')],
+ () => [s__('Timeago|just now'), s__('Timeago|%s seconds remaining')],
+ () => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
+ () => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
+ () => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
+ () => [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
+ () => [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
+ () => [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')],
+ () => [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')],
+ () => [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')],
+ () => [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')],
+ () => [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')],
+ () => [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
+ () => [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
+ ];
+
+ return (number, index) => {
+ if (cache[index]) {
+ return cache[index];
+ }
+ cache[index] = timeAgoLocaleRemaining[index] && timeAgoLocaleRemaining[index]();
+ return cache[index];
+ };
+};
+
+const memoizedLocale = () => {
+ const cache = [];
+
+ const timeAgoLocale = [
+ () => [s__('Timeago|just now'), s__('Timeago|right now')],
+ () => [s__('Timeago|just now'), s__('Timeago|in %s seconds')],
+ () => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
+ () => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
+ () => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
+ () => [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
+ () => [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
+ () => [s__('Timeago|%s days ago'), s__('Timeago|in %s days')],
+ () => [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')],
+ () => [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')],
+ () => [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')],
+ () => [s__('Timeago|%s months ago'), s__('Timeago|in %s months')],
+ () => [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
+ () => [s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
+ ];
- return timeagoInstance;
+ return (number, index) => {
+ if (cache[index]) {
+ return cache[index];
+ }
+ cache[index] = timeAgoLocale[index] && timeAgoLocale[index]();
+ return cache[index];
+ };
};
+timeago.register(timeagoLanguageCode, memoizedLocale());
+timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
+
+export const getTimeago = () => timeago;
+
/**
* For the given elements, sets a tooltip with a formatted date.
* @param {JQuery} $timeagoEls
* @param {Boolean} setTimeago
*/
export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
- getTimeago();
-
$timeagoEls.each((i, el) => {
- $(el).text(timeagoInstance.format($(el).attr('datetime'), timeagoLanguageCode));
+ $(el).text(timeago.format($(el).attr('datetime'), timeagoLanguageCode));
});
if (!setTimeago) {
@@ -207,9 +197,7 @@ export const timeFor = (time, expiredLabel) => {
if (new Date(time) < new Date()) {
return expiredLabel || s__('Timeago|Past due');
}
- return getTimeago()
- .format(time, `${timeagoLanguageCode}-remaining`)
- .trim();
+ return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim();
};
export const getDayDifference = (a, b) => {
@@ -459,7 +447,7 @@ export const parsePikadayDate = dateString => {
/**
* Used `onSelect` method in pickaday
* @param {Date} date UTC format
- * @return {String} Date formated in yyyy-mm-dd
+ * @return {String} Date formatted in yyyy-mm-dd
*/
export const pikadayToString = date => {
const day = pad(date.getDate());
@@ -525,8 +513,8 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
if (fullNameFormat && isNonZero) {
// Remove traling 's' if unit value is singular
- const formatedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
- return `${memo} ${unitValue} ${formatedUnitName}`;
+ const formattedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
+ return `${memo} ${unitValue} ${formattedUnitName}`;
}
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
@@ -602,3 +590,19 @@ export const getDatesInRange = (d1, d2, formatter = x => x) => {
* @return {Number} number of milliseconds
*/
export const secondsToMilliseconds = seconds => seconds * 1000;
+
+/**
+ * Converts the supplied number of seconds to days.
+ *
+ * @param {Number} seconds
+ * @return {Number} number of days
+ */
+export const secondsToDays = seconds => Math.round(seconds / 86400);
+
+/**
+ * Returns the date after the date provided
+ *
+ * @param {Date} date the initial date
+ * @return {Date} the date following the date provided
+ */
+export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() + 1));
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index 5e5d10883a3..1c7d59054dc 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -21,6 +21,7 @@ const httpStatusCodes = {
NOT_FOUND: 404,
GONE: 410,
UNPROCESSABLE_ENTITY: 422,
+ SERVICE_UNAVAILABLE: 503,
};
export const successCodes = [
diff --git a/app/assets/javascripts/lib/utils/logoutput_behaviours.js b/app/assets/javascripts/lib/utils/logoutput_behaviours.js
deleted file mode 100644
index 41b57025cc9..00000000000
--- a/app/assets/javascripts/lib/utils/logoutput_behaviours.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import $ from 'jquery';
-import {
- canScroll,
- isScrolledToBottom,
- isScrolledToTop,
- isScrolledToMiddle,
- toggleDisableButton,
-} from './scroll_utils';
-
-export default class LogOutputBehaviours {
- constructor() {
- // Scroll buttons
- this.$scrollTopBtn = $('.js-scroll-up');
- this.$scrollBottomBtn = $('.js-scroll-down');
-
- this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this));
- this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this));
- }
-
- toggleScroll() {
- if (canScroll()) {
- if (isScrolledToMiddle()) {
- // User is in the middle of the log
-
- toggleDisableButton(this.$scrollTopBtn, false);
- toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (isScrolledToTop()) {
- // User is at Top of Log
-
- toggleDisableButton(this.$scrollTopBtn, true);
- toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (isScrolledToBottom()) {
- // User is at the bottom of the build log.
-
- toggleDisableButton(this.$scrollTopBtn, false);
- toggleDisableButton(this.$scrollBottomBtn, true);
- }
- } else {
- toggleDisableButton(this.$scrollTopBtn, true);
- toggleDisableButton(this.$scrollBottomBtn, true);
- }
- }
-
- toggleScrollAnimation(toggle) {
- this.$scrollBottomBtn.toggleClass('animate', toggle);
- }
-}
diff --git a/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js b/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js
index 4c61da9b862..fb4d9b7de9c 100644
--- a/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js
+++ b/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js
@@ -2,8 +2,8 @@
* An Axios error interceptor that suppresses AJAX errors caused
* by the request being cancelled when the user navigates to a new page
*/
-export default (err, isUserNavigating, featureFlagEnabled) => {
- if (featureFlagEnabled && isUserNavigating && err.code === 'ECONNABORTED') {
+export default (err, isUserNavigating) => {
+ if (isUserNavigating && err.code === 'ECONNABORTED') {
// If the user is navigating away from the current page,
// prevent .then() and .catch() handlers from being
// called by returning a Promise that never resolves
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 2e0270ee42f..cccf9ad311c 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, no-param-reassign, one-var, operator-assignment, no-else-return, consistent-return */
+/* eslint-disable func-names, no-param-reassign, operator-assignment, no-else-return, consistent-return */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
@@ -13,8 +13,7 @@ function addBlockTags(blockTag, selected) {
}
function lineBefore(text, textarea) {
- var split;
- split = text
+ const split = text
.substring(0, textarea.selectionStart)
.trim()
.split('\n');
@@ -80,7 +79,7 @@ function moveCursor({
editorSelectionStart,
editorSelectionEnd,
}) {
- var pos;
+ let pos;
if (textArea && !textArea.setSelectionRange) {
return;
}
@@ -132,18 +131,13 @@ export function insertMarkdownText({
select,
editor,
}) {
- var textToInsert,
- selectedSplit,
- startChar,
- removedLastNewLine,
- removedFirstNewLine,
- currentLineEmpty,
- lastNewLine,
- editorSelectionStart,
- editorSelectionEnd;
- removedLastNewLine = false;
- removedFirstNewLine = false;
- currentLineEmpty = false;
+ let removedLastNewLine = false;
+ let removedFirstNewLine = false;
+ let currentLineEmpty = false;
+ let editorSelectionStart;
+ let editorSelectionEnd;
+ let lastNewLine;
+ let textToInsert;
if (editor) {
const selectionRange = editor.getSelectionRange();
@@ -186,7 +180,7 @@ export function insertMarkdownText({
}
}
- selectedSplit = selected.split('\n');
+ const selectedSplit = selected.split('\n');
if (editor && !wrap) {
lastNewLine = editor.getValue().split('\n')[editorSelectionStart.row];
@@ -207,8 +201,7 @@ export function insertMarkdownText({
(textArea && textArea.selectionStart === 0) ||
(editor && editorSelectionStart.column === 0 && editorSelectionStart.row === 0);
- startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : '';
-
+ const startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : '';
const textPlaceholder = '{text}';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
@@ -263,11 +256,10 @@ export function insertMarkdownText({
}
function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagContent }) {
- var $textArea, selected, text;
- $textArea = $(textArea);
+ const $textArea = $(textArea);
textArea = $textArea.get(0);
- text = $textArea.val();
- selected = selectedText(text, textArea) || tagContent;
+ const text = $textArea.val();
+ const selected = selectedText(text, textArea) || tagContent;
$textArea.focus();
return insertMarkdownText({
textArea,
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 0c194d67bce..6bbf118d7d1 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -72,7 +72,7 @@ export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3
* @param {String} sha
* @returns {String}
*/
-export const truncateSha = sha => sha.substr(0, 8);
+export const truncateSha = sha => sha.substring(0, 8);
const ELLIPSIS_CHAR = '…';
export const truncatePathMiddleToLength = (text, maxWidth) => {
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 4be0d05a9b7..d48678c21f6 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,4 +1,6 @@
-import { join as joinPaths } from 'path';
+const PATH_SEPARATOR = '/';
+const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
+const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
// Returns a decoded url parameter value
// - Treats '+' as '%20'
@@ -6,6 +8,37 @@ function decodeUrlParameter(val) {
return decodeURIComponent(val.replace(/\+/g, '%20'));
}
+function cleanLeadingSeparator(path) {
+ return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
+}
+
+function cleanEndingSeparator(path) {
+ return path.replace(PATH_SEPARATOR_ENDING_REGEX, '');
+}
+
+/**
+ * Safely joins the given paths which might both start and end with a `/`
+ *
+ * Example:
+ * - `joinPaths('abc/', '/def') === 'abc/def'`
+ * - `joinPaths(null, 'abc/def', 'zoo) === 'abc/def/zoo'`
+ *
+ * @param {...String} paths
+ * @returns {String}
+ */
+export function joinPaths(...paths) {
+ return paths.reduce((acc, path) => {
+ if (!path) {
+ return acc;
+ }
+ if (!acc) {
+ return path;
+ }
+
+ return [cleanEndingSeparator(acc), PATH_SEPARATOR, cleanLeadingSeparator(path)].join('');
+ }, '');
+}
+
// Returns an array containing the value(s) of the
// of the key passed as an argument
export function getParameterValues(sParam, url = window.location) {
@@ -181,4 +214,71 @@ export function getWebSocketUrl(path) {
return `${getWebSocketProtocol()}//${joinPaths(window.location.host, path)}`;
}
-export { joinPaths };
+/**
+ * Convert search query into an object
+ *
+ * @param {String} query from "document.location.search"
+ * @returns {Object}
+ *
+ * ex: "?one=1&two=2" into {one: 1, two: 2}
+ */
+export function queryToObject(query) {
+ const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query;
+ return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => {
+ const p = curr.split('=');
+ accumulator[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
+ return accumulator;
+ }, {});
+}
+
+/**
+ * Convert search query object back into a search query
+ *
+ * @param {Object} obj that needs to be converted
+ * @returns {String}
+ *
+ * ex: {one: 1, two: 2} into "one=1&two=2"
+ *
+ */
+export function objectToQuery(obj) {
+ return Object.keys(obj)
+ .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`)
+ .join('&');
+}
+
+/**
+ * Sets query params for a given URL
+ * It adds new query params, updates existing params with a new value and removes params with value null/undefined
+ *
+ * @param {Object} params The query params to be set/updated
+ * @param {String} url The url to be operated on
+ * @param {Boolean} clearParams Indicates whether existing query params should be removed or not
+ * @returns {String} A copy of the original with the updated query params
+ */
+export const setUrlParams = (params, url = window.location.href, clearParams = false) => {
+ const urlObj = new URL(url);
+ const queryString = urlObj.search;
+ const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString);
+
+ Object.keys(params).forEach(key => {
+ if (params[key] === null || params[key] === undefined) {
+ searchParams.delete(key);
+ } else if (Array.isArray(params[key])) {
+ params[key].forEach((val, idx) => {
+ if (idx === 0) {
+ searchParams.set(key, val);
+ } else {
+ searchParams.append(key, val);
+ }
+ });
+ } else {
+ searchParams.set(key, params[key]);
+ }
+ });
+
+ urlObj.search = searchParams.toString();
+
+ return urlObj.toString();
+};
+
+export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 465c9a362ba..674415c9d01 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -222,6 +222,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
+ // eslint-disable-next-line no-jquery/no-ajax-events
$(document).ajaxError((e, xhrObj) => {
const ref = xhrObj.status;
diff --git a/app/assets/javascripts/monitoring/components/charts/anomaly.vue b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
index 8eeac737a11..1df7ca37a98 100644
--- a/app/assets/javascripts/monitoring/components/charts/anomaly.vue
+++ b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
@@ -29,7 +29,7 @@ const AREA_COLOR_RGBA = `rgba(${hexToRgb(AREA_COLOR).join(',')},${AREA_OPACITY})
* time series chart, the boundary band shows the normal
* range of values the metric should take.
*
- * This component accepts 3 queries, which contain the
+ * This component accepts 3 metrics, which contain the
* "metric", "upper" limit and "lower" limit.
*
* The upper and lower series are "stacked areas" visually
@@ -62,10 +62,11 @@ export default {
},
computed: {
series() {
- return this.graphData.queries.map(query => {
- const values = query.result[0] ? query.result[0].values : [];
+ return this.graphData.metrics.map(metric => {
+ const values = metric.result && metric.result[0] ? metric.result[0].values : [];
return {
- label: query.label,
+ label: metric.label,
+ // NaN values may disrupt avg., max. & min. calculations in the legend, filter them out
data: values.filter(([, value]) => !Number.isNaN(value)),
};
});
@@ -82,7 +83,7 @@ export default {
return min < 0 ? -min : 0;
},
metricData() {
- const originalMetricQuery = this.graphData.queries[0];
+ const originalMetricQuery = this.graphData.metrics[0];
const metricQuery = { ...originalMetricQuery };
metricQuery.result[0].values = metricQuery.result[0].values.map(([x, y]) => [
@@ -92,7 +93,7 @@ export default {
return {
...this.graphData,
type: 'line-chart',
- queries: [metricQuery],
+ metrics: [metricQuery],
};
},
metricSeriesConfig() {
diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue
index ee6aaeb7dde..eb407ad1d7f 100644
--- a/app/assets/javascripts/monitoring/components/charts/column.vue
+++ b/app/assets/javascripts/monitoring/components/charts/column.vue
@@ -32,8 +32,8 @@ export default {
},
computed: {
chartData() {
- const queryData = this.graphData.queries.reduce((acc, query) => {
- const series = makeDataSeries(query.result, {
+ const queryData = this.graphData.metrics.reduce((acc, query) => {
+ const series = makeDataSeries(query.result || [], {
name: this.formatLegendLabel(query),
});
@@ -45,13 +45,13 @@ export default {
};
},
xAxisTitle() {
- return this.graphData.queries[0].result[0].x_label !== undefined
- ? this.graphData.queries[0].result[0].x_label
+ return this.graphData.metrics[0].result[0].x_label !== undefined
+ ? this.graphData.metrics[0].result[0].x_label
: '';
},
yAxisTitle() {
- return this.graphData.queries[0].result[0].y_label !== undefined
- ? this.graphData.queries[0].result[0].y_label
+ return this.graphData.metrics[0].result[0].y_label !== undefined
+ ? this.graphData.metrics[0].result[0].y_label
: '';
},
xAxisType() {
diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
index b8158247e49..6ab5aaeba1d 100644
--- a/app/assets/javascripts/monitoring/components/charts/heatmap.vue
+++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
@@ -24,7 +24,7 @@ export default {
},
computed: {
chartData() {
- return this.queries.result.reduce(
+ return this.metrics.result.reduce(
(acc, result, i) => [...acc, ...result.values.map((value, j) => [i, j, value[1]])],
[],
);
@@ -36,7 +36,7 @@ export default {
return this.graphData.y_label || '';
},
xAxisLabels() {
- return this.queries.result.map(res => Object.values(res.metric)[0]);
+ return this.metrics.result.map(res => Object.values(res.metric)[0]);
},
yAxisLabels() {
return this.result.values.map(val => {
@@ -46,10 +46,10 @@ export default {
});
},
result() {
- return this.queries.result[0];
+ return this.metrics.result[0];
},
- queries() {
- return this.graphData.queries[0];
+ metrics() {
+ return this.graphData.metrics[0];
},
},
};
diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
index 076682820e6..e75ddb05808 100644
--- a/app/assets/javascripts/monitoring/components/charts/single_stat.vue
+++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
@@ -17,7 +17,7 @@ export default {
},
computed: {
queryInfo() {
- return this.graphData.queries[0];
+ return this.graphData.metrics[0];
},
engineeringNotation() {
return `${roundOffFloat(this.queryInfo.result[0].value[1], 1)}${this.queryInfo.unit}`;
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 6a88c8a5ee3..0d442f14aea 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -1,9 +1,9 @@
<script>
-import { s__, __ } from '~/locale';
import _ from 'underscore';
import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
+import { s__, __ } from '~/locale';
import { roundOffFloat } from '~/lib/utils/common_utils';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
@@ -105,7 +105,7 @@ export default {
// Transforms & supplements query data to render appropriate labels & styles
// Input: [{ queryAttributes1 }, { queryAttributes2 }]
// Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
- return this.graphData.queries.reduce((acc, query) => {
+ return this.graphData.metrics.reduce((acc, query) => {
const { appearance } = query;
const lineType =
appearance && appearance.line && appearance.line.type
@@ -121,7 +121,7 @@ export default {
? appearance.area.opacity
: undefined,
};
- const series = makeDataSeries(query.result, {
+ const series = makeDataSeries(query.result || [], {
name: this.formatLegendLabel(query),
lineStyle: {
type: lineType,
@@ -253,23 +253,25 @@ export default {
this.tooltip.title = dateFormat(params.value, dateFormats.default);
this.tooltip.content = [];
params.seriesData.forEach(dataPoint => {
- const [xVal, yVal] = dataPoint.value;
- this.tooltip.isDeployment = dataPoint.componentSubType === graphTypes.deploymentData;
- if (this.tooltip.isDeployment) {
- const [deploy] = this.recentDeployments.filter(
- deployment => deployment.createdAt === xVal,
- );
- this.tooltip.sha = deploy.sha.substring(0, 8);
- this.tooltip.commitUrl = deploy.commitUrl;
- } else {
- const { seriesName, color, dataIndex } = dataPoint;
- const value = yVal.toFixed(3);
- this.tooltip.content.push({
- name: seriesName,
- dataIndex,
- value,
- color,
- });
+ if (dataPoint.value) {
+ const [xVal, yVal] = dataPoint.value;
+ this.tooltip.isDeployment = dataPoint.componentSubType === graphTypes.deploymentData;
+ if (this.tooltip.isDeployment) {
+ const [deploy] = this.recentDeployments.filter(
+ deployment => deployment.createdAt === xVal,
+ );
+ this.tooltip.sha = deploy.sha.substring(0, 8);
+ this.tooltip.commitUrl = deploy.commitUrl;
+ } else {
+ const { seriesName, color, dataIndex } = dataPoint;
+ const value = yVal.toFixed(3);
+ this.tooltip.content.push({
+ name: seriesName,
+ dataIndex,
+ value,
+ color,
+ });
+ }
}
});
},
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 26e2c2568c1..c1ca5449ba3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,6 +1,6 @@
<script>
import _ from 'underscore';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import {
GlButton,
@@ -11,28 +11,27 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
+import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import DateTimePicker from './date_time_picker/date_time_picker.vue';
-import MonitorTimeSeriesChart from './charts/time_series.vue';
-import MonitorSingleStatChart from './charts/single_stat.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
+import GroupEmptyState from './group_empty_state.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
-import { getTimeDiff, isValidDate, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
+import { getTimeDiff, isValidDate, getAddMetricTrackingOptions } from '../utils';
+import { metricStates } from '../constants';
export default {
components: {
VueDraggable,
- MonitorTimeSeriesChart,
- MonitorSingleStatChart,
PanelType,
GraphGroup,
EmptyState,
+ GroupEmptyState,
Icon,
GlButton,
GlDropdown,
@@ -103,6 +102,10 @@ export default {
type: String,
required: true,
},
+ emptyNoDataSmallSvgPath: {
+ type: String,
+ required: true,
+ },
emptyUnableToConnectSvgPath: {
type: String,
required: true,
@@ -180,11 +183,11 @@ export default {
'showEmptyState',
'environments',
'deploymentData',
- 'metricsWithData',
'useDashboardEndpoint',
'allDashboards',
'additionalPanelTypesEnabled',
]),
+ ...mapGetters('monitoringDashboard', ['getMetricStates']),
firstDashboard() {
return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
? this.allDashboards[0]
@@ -252,28 +255,18 @@ export default {
'setEndpoints',
'setPanelGroupMetrics',
]),
- chartsWithData(charts) {
- return charts.filter(chart =>
- chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
- );
- },
- updateMetrics(key, metrics) {
+ updatePanels(key, panels) {
this.setPanelGroupMetrics({
- metrics,
+ panels,
key,
});
},
- removeMetric(key, metrics, graphIndex) {
+ removePanel(key, panels, graphIndex) {
this.setPanelGroupMetrics({
- metrics: metrics.filter((v, i) => i !== graphIndex),
+ panels: panels.filter((v, i) => i !== graphIndex),
key,
});
},
- removeGraph(metrics, graphIndex) {
- // At present graphs will not be removed, they should removed using the vuex store
- // See https://gitlab.com/gitlab-org/gitlab/issues/27835
- metrics.splice(graphIndex, 1);
- },
showInvalidDateError() {
createFlash(s__('Metrics|Link contains an invalid time window.'));
},
@@ -294,14 +287,36 @@ export default {
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
- groupHasData(group) {
- return this.chartsWithData(group.metrics).length > 0;
- },
onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
},
- downloadCSVOptions,
- generateLinkToChartOptions,
+ /**
+ * Return a single empty state for a group.
+ *
+ * If all states are the same a single state is returned to be displayed
+ * Except if the state is OK, in which case the group is displayed.
+ *
+ * @param {String} groupKey - Identifier for group
+ * @returns {String} state code from `metricStates`
+ */
+ groupSingleEmptyState(groupKey) {
+ const states = this.getMetricStates(groupKey);
+ if (states.length === 1 && states[0] !== metricStates.OK) {
+ return states[0];
+ }
+ return null;
+ },
+ /**
+ * A group should be not collapsed if any metric is loaded (OK)
+ *
+ * @param {String} groupKey - Identifier for group
+ * @returns {Boolean} If the group should be collapsed
+ */
+ collapseGroup(groupKey) {
+ // Collapse group if no data is available
+ return !this.getMetricStates(groupKey).includes(metricStates.OK);
+ },
+ getAddMetricTrackingOptions,
},
addMetric: {
title: s__('Metrics|Add metric'),
@@ -393,9 +408,10 @@ export default {
</gl-button>
<gl-button
v-if="addingMetricsAvailable"
+ ref="addMetricBtn"
v-gl-modal="$options.addMetric.modalId"
variant="outline-success"
- class="mr-2 mt-1 js-add-metric-button"
+ class="mr-2 mt-1"
>
{{ $options.addMetric.title }}
</gl-button>
@@ -415,6 +431,8 @@ export default {
<div slot="modal-footer">
<gl-button @click="hideAddMetricModal">{{ __('Cancel') }}</gl-button>
<gl-button
+ ref="submitCustomMetricsFormBtn"
+ v-track-event="getAddMetricTrackingOptions()"
:disabled="!formIsValid"
variant="success"
@click="submitCustomMetricsForm"
@@ -454,42 +472,55 @@ export default {
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
- :collapse-group="groupHasData(groupData)"
+ :collapse-group="collapseGroup(groupData.key)"
>
- <vue-draggable
- :value="groupData.metrics"
- group="metrics-dashboard"
- :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
- :disabled="!isRearrangingPanels"
- @input="updateMetrics(groupData.key, $event)"
- >
- <div
- v-for="(graphData, graphIndex) in groupData.metrics"
- :key="`panel-type-${graphIndex}`"
- class="col-12 col-lg-6 px-2 mb-2 draggable"
- :class="{ 'draggable-enabled': isRearrangingPanels }"
+ <div v-if="!groupSingleEmptyState(groupData.key)">
+ <vue-draggable
+ :value="groupData.panels"
+ group="metrics-dashboard"
+ :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
+ :disabled="!isRearrangingPanels"
+ @input="updatePanels(groupData.key, $event)"
>
- <div class="position-relative draggable-panel js-draggable-panel">
- <div
- v-if="isRearrangingPanels"
- class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
- @click="removeGraph(groupData.metrics, graphIndex)"
- >
- <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
- ><icon name="close"
- /></a>
- </div>
+ <div
+ v-for="(graphData, graphIndex) in groupData.panels"
+ :key="`panel-type-${graphIndex}`"
+ class="col-12 col-lg-6 px-2 mb-2 draggable"
+ :class="{ 'draggable-enabled': isRearrangingPanels }"
+ >
+ <div class="position-relative draggable-panel js-draggable-panel">
+ <div
+ v-if="isRearrangingPanels"
+ class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
+ @click="removePanel(groupData.key, groupData.panels, graphIndex)"
+ >
+ <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
+ ><icon name="close"
+ /></a>
+ </div>
- <panel-type
- :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
- :graph-data="graphData"
- :alerts-endpoint="alertsEndpoint"
- :prometheus-alerts-available="prometheusAlertsAvailable"
- :index="`${index}-${graphIndex}`"
- />
+ <panel-type
+ :clipboard-text="
+ generateLink(groupData.group, graphData.title, graphData.y_label)
+ "
+ :graph-data="graphData"
+ :alerts-endpoint="alertsEndpoint"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
+ :index="`${index}-${graphIndex}`"
+ />
+ </div>
</div>
- </div>
- </vue-draggable>
+ </vue-draggable>
+ </div>
+ <div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
+ <group-empty-state
+ ref="empty-group"
+ :documentation-path="documentationPath"
+ :settings-path="settingsPath"
+ :selected-state="groupSingleEmptyState(groupData.key)"
+ :svg-path="emptyNoDataSmallSvgPath"
+ />
+ </div>
</graph-group>
</div>
<empty-state
@@ -501,6 +532,7 @@ export default {
:empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath"
:empty-no-data-svg-path="emptyNoDataSvgPath"
+ :empty-no-data-small-svg-path="emptyNoDataSmallSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
:compact="smallEmptyState"
/>
diff --git a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue
index 0388a6190d9..c3beae18726 100644
--- a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue
+++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue
@@ -1,7 +1,7 @@
<script>
import _ from 'underscore';
-import { s__, sprintf } from '~/locale';
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
import { dateFormats } from '~/monitoring/constants';
const inputGroupText = {
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index f75839c7c6b..eb8945c1a57 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -1,8 +1,8 @@
<script>
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
import GraphGroup from './graph_group.vue';
-import MonitorTimeSeriesChart from './charts/time_series.vue';
import { sidebarAnimationDuration } from '../constants';
import { getTimeDiff } from '../utils';
@@ -11,7 +11,7 @@ let sidebarMutationObserver;
export default {
components: {
GraphGroup,
- MonitorTimeSeriesChart,
+ PanelType,
},
props: {
dashboardUrl: {
@@ -35,13 +35,17 @@ export default {
};
},
computed: {
- ...mapState('monitoringDashboard', ['dashboard', 'metricsWithData']),
+ ...mapState('monitoringDashboard', ['dashboard']),
+ ...mapGetters('monitoringDashboard', ['metricsWithData']),
charts() {
+ if (!this.dashboard || !this.dashboard.panel_groups) {
+ return [];
+ }
const groupWithMetrics = this.dashboard.panel_groups.find(group =>
- group.metrics.find(chart => this.chartHasData(chart)),
- ) || { metrics: [] };
+ group.panels.find(chart => this.chartHasData(chart)),
+ ) || { panels: [] };
- return groupWithMetrics.metrics.filter(chart => this.chartHasData(chart));
+ return groupWithMetrics.panels.filter(chart => this.chartHasData(chart));
},
isSingleChart() {
return this.charts.length === 1;
@@ -70,7 +74,7 @@ export default {
'setShowErrorBanner',
]),
chartHasData(chart) {
- return chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id));
+ return chart.metrics.some(metric => this.metricsWithData().includes(metric.metric_id));
},
onSidebarMutation() {
setTimeout(() => {
@@ -89,16 +93,12 @@ export default {
<template>
<div class="metrics-embed" :class="{ 'd-inline-flex col-lg-6 p-0': isSingleChart }">
<div v-if="charts.length" class="row w-100 m-n2 pb-4">
- <monitor-time-series-chart
- v-for="graphData in charts"
- :key="graphData.title"
+ <panel-type
+ v-for="(graphData, graphIndex) in charts"
+ :key="`panel-type-${graphIndex}`"
class="w-100"
:graph-data="graphData"
- :container-width="elWidth"
:group-id="dashboardUrl"
- :project-path="null"
- :show-border="true"
- :single-embed="isSingleChart"
/>
</div>
</div>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index 1bb40447a3e..d3157b731b2 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { GlEmptyState } from '@gitlab/ui';
+import { __ } from '~/locale';
export default {
components: {
@@ -37,6 +37,10 @@ export default {
type: String,
required: true,
},
+ emptyNoDataSmallSvgPath: {
+ type: String,
+ required: true,
+ },
emptyUnableToConnectSvgPath: {
type: String,
required: true,
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index 3cb6ccb64b1..5a7981b6534 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -15,34 +15,44 @@ export default {
required: false,
default: true,
},
+ /**
+ * Initial value of collapse on mount.
+ */
collapseGroup: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
},
data() {
return {
- showGroup: true,
+ isCollapsed: this.collapseGroup,
};
},
computed: {
caretIcon() {
- return this.collapseGroup && this.showGroup ? 'angle-down' : 'angle-right';
+ return this.isCollapsed ? 'angle-right' : 'angle-down';
},
},
- created() {
- this.showGroup = this.collapseGroup;
+ watch: {
+ collapseGroup(val) {
+ // Respond to changes in collapseGroup but do not
+ // collapse it once was opened by the user.
+ if (this.showPanels && !val) {
+ this.isCollapsed = false;
+ }
+ },
},
methods: {
collapse() {
- this.showGroup = !this.showGroup;
+ this.isCollapsed = !this.isCollapsed;
},
},
};
</script>
<template>
- <div v-if="showPanels" class="card prometheus-panel">
+ <div v-if="showPanels" ref="graph-group" class="card prometheus-panel">
<div class="card-header d-flex align-items-center">
<h4 class="flex-grow-1">{{ name }}</h4>
<a role="button" class="js-graph-group-toggle" @click="collapse">
@@ -50,12 +60,12 @@ export default {
</a>
</div>
<div
- v-if="collapseGroup"
- v-show="collapseGroup && showGroup"
+ v-show="!isCollapsed"
+ ref="graph-group-content"
class="card-body prometheus-graph-group p-0"
>
<slot></slot>
</div>
</div>
- <div v-else class="prometheus-graph-group"><slot></slot></div>
+ <div v-else ref="graph-group-content" class="prometheus-graph-group"><slot></slot></div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue
new file mode 100644
index 00000000000..dee4e5998ee
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue
@@ -0,0 +1,105 @@
+<script>
+import { __, sprintf } from '~/locale';
+import { GlEmptyState } from '@gitlab/ui';
+import { metricStates } from '../constants';
+
+export default {
+ components: {
+ GlEmptyState,
+ },
+ props: {
+ documentationPath: {
+ type: String,
+ required: true,
+ },
+ settingsPath: {
+ type: String,
+ required: true,
+ },
+ selectedState: {
+ type: String,
+ required: true,
+ },
+ svgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ const documentationLink = `<a href="${this.documentationPath}">${__('More information')}</a>`;
+ return {
+ states: {
+ [metricStates.NO_DATA]: {
+ title: __('No data to display'),
+ slottedDescription: sprintf(
+ __(
+ 'The data source is connected, but there is no data to display. %{documentationLink}',
+ ),
+ { documentationLink },
+ false,
+ ),
+ },
+ [metricStates.TIMEOUT]: {
+ title: __('Connection timed out'),
+ slottedDescription: sprintf(
+ __(
+ "Charts can't be displayed as the request for data has timed out. %{documentationLink}",
+ ),
+ { documentationLink },
+ false,
+ ),
+ },
+ [metricStates.CONNECTION_FAILED]: {
+ title: __('Connection failed'),
+ description: __(`We couldn't reach the Prometheus server.
+ Either the server no longer exists or the configuration details need updating.`),
+ buttonText: __('Verify configuration'),
+ buttonPath: this.settingsPath,
+ },
+ [metricStates.BAD_QUERY]: {
+ title: __('Query cannot be processed'),
+ slottedDescription: sprintf(
+ __(
+ `The Prometheus server responded with "bad request".
+ Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}`,
+ ),
+ { documentationLink },
+ false,
+ ),
+ buttonText: __('Verify configuration'),
+ buttonPath: this.settingsPath,
+ },
+ [metricStates.LOADING]: {
+ title: __('Waiting for performance data'),
+ description: __(`Creating graphs uses the data from the Prometheus server.
+ If this takes a long time, ensure that data is available.`),
+ },
+ [metricStates.UNKNOWN_ERROR]: {
+ title: __('An error has occurred'),
+ description: __('An error occurred while loading the data. Please try again.'),
+ },
+ },
+ };
+ },
+ computed: {
+ currentState() {
+ return this.states[this.selectedState] || this.states[metricStates.UNKNOWN_ERROR];
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="currentState.title"
+ :primary-button-text="currentState.buttonText"
+ :primary-button-link="currentState.buttonPath"
+ :description="currentState.description"
+ :svg-path="svgPath"
+ :compact="true"
+ >
+ <template v-if="currentState.slottedDescription" #description>
+ <div v-html="currentState.slottedDescription"></div>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index cafb4b0b479..ec6a41d0540 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -1,7 +1,6 @@
<script>
import { mapState } from 'vuex';
import _ from 'underscore';
-import { __ } from '~/locale';
import {
GlDropdown,
GlDropdownItem,
@@ -9,6 +8,7 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
+import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorAnomalyChart from './charts/anomaly.vue';
@@ -36,7 +36,8 @@ export default {
props: {
clipboardText: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
graphData: {
type: Object,
@@ -47,6 +48,11 @@ export default {
required: false,
default: '',
},
+ groupId: {
+ type: String,
+ required: false,
+ default: 'panel-type-chart',
+ },
},
computed: {
...mapState('monitoringDashboard', ['deploymentData', 'projectPath']),
@@ -54,10 +60,14 @@ export default {
return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData;
},
graphDataHasMetrics() {
- return this.graphData.queries[0].result.length > 0;
+ return (
+ this.graphData.metrics &&
+ this.graphData.metrics[0].result &&
+ this.graphData.metrics[0].result.length > 0
+ );
},
csvText() {
- const chartData = this.graphData.queries[0].result[0].values;
+ const chartData = this.graphData.metrics[0].result[0].values;
const yLabel = this.graphData.y_label;
const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
return chartData.reduce((csv, data) => {
@@ -112,21 +122,21 @@ export default {
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
- :thresholds="getGraphAlertValues(graphData.queries)"
- group-id="panel-type-chart"
+ :thresholds="getGraphAlertValues(graphData.metrics)"
+ :group-id="groupId"
>
<div class="d-flex align-items-center">
<alert-widget
v-if="alertWidgetAvailable && graphData"
:modal-id="`alert-modal-${index}`"
:alerts-endpoint="alertsEndpoint"
- :relevant-queries="graphData.queries"
- :alerts-to-manage="getGraphAlerts(graphData.queries)"
+ :relevant-queries="graphData.metrics"
+ :alerts-to-manage="getGraphAlerts(graphData.metrics)"
@setAlerts="setAlerts"
/>
<gl-dropdown
v-gl-tooltip
- class="mx-2"
+ class="ml-auto mx-3"
toggle-class="btn btn-transparent border-0"
:right="true"
:no-caret="true"
@@ -143,6 +153,7 @@ export default {
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item
+ v-if="clipboardText"
v-track-event="generateLinkToChartOptions(clipboardText)"
class="js-chart-link"
:data-clipboard-text="clipboardText"
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 1a1fcdd0e66..398b45b9012 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -1,5 +1,52 @@
import { __ } from '~/locale';
+export const PROMETHEUS_TIMEOUT = 120000; // TWO_MINUTES
+
+/**
+ * States and error states in Prometheus Queries (PromQL) for metrics
+ */
+export const metricStates = {
+ /**
+ * Metric data is available
+ */
+ OK: 'OK',
+
+ /**
+ * Metric data is being fetched
+ */
+ LOADING: 'LOADING',
+
+ /**
+ * Connection timed out to prometheus server
+ * the timeout is set to PROMETHEUS_TIMEOUT
+ *
+ */
+ TIMEOUT: 'TIMEOUT',
+
+ /**
+ * The prometheus server replies with an empty data set
+ */
+ NO_DATA: 'NO_DATA',
+
+ /**
+ * The prometheus server cannot be reached
+ */
+ CONNECTION_FAILED: 'CONNECTION_FAILED',
+
+ /**
+ * The prometheus server was reached but it cannot process
+ * the query. This can happen for several reasons:
+ * - PromQL syntax is incorrect
+ * - An operator is not supported
+ */
+ BAD_QUERY: 'BAD_QUERY',
+
+ /**
+ * No specific reason found for error
+ */
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
+};
+
export const sidebarAnimationDuration = 300; // milliseconds.
export const chartHeight = 300;
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index a14145d480b..d296f5b7a66 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
+import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
-import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
import store from './stores';
Vue.use(GlToast);
diff --git a/app/assets/javascripts/monitoring/monitoring_tracking_helper.js b/app/assets/javascripts/monitoring/monitoring_tracking_helper.js
new file mode 100644
index 00000000000..5ae1eca10de
--- /dev/null
+++ b/app/assets/javascripts/monitoring/monitoring_tracking_helper.js
@@ -0,0 +1,10 @@
+import Tracking from '~/tracking';
+
+const trackDashboardLoad = ({ label, value }) =>
+ Tracking.event(document.body.dataset.page, 'dashboard_fetch', {
+ label,
+ property: 'count',
+ value,
+ });
+
+export default trackDashboardLoad;
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 6a8e3cc82f5..1cb82ce0083 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -1,30 +1,25 @@
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
+import trackDashboardLoad from '../monitoring_tracking_helper';
import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils';
-import { s__, __ } from '../../locale';
+import { s__, sprintf } from '../../locale';
-const MAX_REQUESTS = 3;
+import { PROMETHEUS_TIMEOUT } from '../constants';
-export function backOffRequest(makeRequestCallback) {
- let requestCounter = 0;
+function backOffRequest(makeRequestCallback) {
return backOff((next, stop) => {
makeRequestCallback()
.then(resp => {
if (resp.status === statusCodes.NO_CONTENT) {
- requestCounter += 1;
- if (requestCounter < MAX_REQUESTS) {
- next();
- } else {
- stop(new Error(__('Failed to connect to the prometheus server')));
- }
+ next();
} else {
stop(resp);
}
})
.catch(stop);
- });
+ }, PROMETHEUS_TIMEOUT);
}
export const setGettingStartedEmptyState = ({ commit }) => {
@@ -45,17 +40,12 @@ export const requestMetricsDashboard = ({ commit }) => {
export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => {
commit(types.SET_ALL_DASHBOARDS, response.all_dashboards);
commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups);
- dispatch('fetchPrometheusMetrics', params);
+ return dispatch('fetchPrometheusMetrics', params);
};
export const receiveMetricsDashboardFailure = ({ commit }, error) => {
commit(types.RECEIVE_METRICS_DATA_FAILURE, error);
};
-export const requestMetricsData = ({ commit }) => commit(types.REQUEST_METRICS_DATA);
-export const receiveMetricsDataSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_METRICS_DATA_SUCCESS, data);
-export const receiveMetricsDataFailure = ({ commit }, error) =>
- commit(types.RECEIVE_METRICS_DATA_FAILURE, error);
export const receiveDeploymentsDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, data);
export const receiveDeploymentsDataFailure = ({ commit }) =>
@@ -83,16 +73,22 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
.then(resp => resp.data)
- .then(response => {
- dispatch('receiveMetricsDashboardSuccess', {
- response,
- params,
- });
- })
- .catch(error => {
- dispatch('receiveMetricsDashboardFailure', error);
- if (state.setShowErrorBanner) {
- createFlash(s__('Metrics|There was an error while retrieving metrics'));
+ .then(response => dispatch('receiveMetricsDashboardSuccess', { response, params }))
+ .catch(e => {
+ dispatch('receiveMetricsDashboardFailure', e);
+ if (state.showErrorBanner) {
+ if (e.response.data && e.response.data.message) {
+ const { message } = e.response.data;
+ createFlash(
+ sprintf(
+ s__('Metrics|There was an error while retrieving metrics. %{message}'),
+ { message },
+ false,
+ ),
+ );
+ } else {
+ createFlash(s__('Metrics|There was an error while retrieving metrics'));
+ }
}
});
};
@@ -129,12 +125,20 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
step,
};
- return fetchPrometheusResult(metric.prometheus_endpoint_path, queryParams).then(result => {
- commit(types.SET_QUERY_RESULT, { metricId: metric.metric_id, result });
- });
+ commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metric_id });
+
+ return fetchPrometheusResult(metric.prometheus_endpoint_path, queryParams)
+ .then(result => {
+ commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metric_id, result });
+ })
+ .catch(error => {
+ commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metric_id, error });
+ // Continue to throw error so the dashboard can notify using createFlash
+ throw error;
+ });
};
-export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => {
+export const fetchPrometheusMetrics = ({ state, commit, dispatch, getters }, params) => {
commit(types.REQUEST_METRICS_DATA);
const promises = [];
@@ -146,18 +150,25 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => {
});
});
- return Promise.all(promises).then(() => {
- if (state.metricsWithData.length === 0) {
- commit(types.SET_NO_DATA_EMPTY_STATE);
- }
- });
+ return Promise.all(promises)
+ .then(() => {
+ const dashboardType = state.currentDashboard === '' ? 'default' : 'custom';
+ trackDashboardLoad({
+ label: `${dashboardType}_metrics_dashboard`,
+ value: getters.metricsWithData().length,
+ });
+ })
+ .catch(() => {
+ createFlash(s__(`Metrics|There was an error while retrieving metrics`), 'warning');
+ });
};
export const fetchDeploymentsData = ({ state, dispatch }) => {
if (!state.deploymentsEndpoint) {
return Promise.resolve([]);
}
- return backOffRequest(() => axios.get(state.deploymentsEndpoint))
+ return axios
+ .get(state.deploymentsEndpoint)
.then(resp => resp.data)
.then(response => {
if (!response || !response.deployments) {
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
new file mode 100644
index 00000000000..a13157c6f87
--- /dev/null
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -0,0 +1,62 @@
+const metricsIdsInPanel = panel =>
+ panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
+
+/**
+ * Get all state for metric in the dashboard or a group. The
+ * states are not repeated so the dashboard or group can show
+ * a global state.
+ *
+ * @param {Object} state
+ * @returns {Function} A function that returns an array of
+ * states in all the metric in the dashboard or group.
+ */
+export const getMetricStates = state => groupKey => {
+ let groups = state.dashboard.panel_groups;
+ if (groupKey) {
+ groups = groups.filter(group => group.key === groupKey);
+ }
+
+ const metricStates = groups.reduce((acc, group) => {
+ group.panels.forEach(panel => {
+ panel.metrics.forEach(metric => {
+ if (metric.state) {
+ acc.push(metric.state);
+ }
+ });
+ });
+ return acc;
+ }, []);
+
+ // Deduplicate and sort array
+ return Array.from(new Set(metricStates)).sort();
+};
+
+/**
+ * Getter to obtain the list of metric ids that have data
+ *
+ * Useful to understand which parts of the dashboard should
+ * be displayed. It is a Vuex Method-Style Access getter.
+ *
+ * @param {Object} state
+ * @returns {Function} A function that returns an array of
+ * metrics in the dashboard that contain results, optionally
+ * filtered by group key.
+ */
+export const metricsWithData = state => groupKey => {
+ let groups = state.dashboard.panel_groups;
+ if (groupKey) {
+ groups = groups.filter(group => group.key === groupKey);
+ }
+
+ const res = [];
+ groups.forEach(group => {
+ group.panels.forEach(panel => {
+ res.push(...metricsIdsInPanel(panel));
+ });
+ });
+
+ return res;
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/index.js b/app/assets/javascripts/monitoring/stores/index.js
index d58398c54ae..c1c466b7cf0 100644
--- a/app/assets/javascripts/monitoring/stores/index.js
+++ b/app/assets/javascripts/monitoring/stores/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
+import * as getters from './getters';
import mutations from './mutations';
import state from './state';
@@ -12,6 +13,7 @@ export const createStore = () =>
monitoringDashboard: {
namespaced: true,
actions,
+ getters,
mutations,
state,
},
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index fa15a2ba800..74068e1d846 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -1,13 +1,19 @@
export const REQUEST_METRICS_DATA = 'REQUEST_METRICS_DATA';
export const RECEIVE_METRICS_DATA_SUCCESS = 'RECEIVE_METRICS_DATA_SUCCESS';
export const RECEIVE_METRICS_DATA_FAILURE = 'RECEIVE_METRICS_DATA_FAILURE';
+
export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA';
export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS';
export const RECEIVE_DEPLOYMENTS_DATA_FAILURE = 'RECEIVE_DEPLOYMENTS_DATA_FAILURE';
+
export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE';
-export const SET_QUERY_RESULT = 'SET_QUERY_RESULT';
+
+export const REQUEST_METRIC_RESULT = 'REQUEST_METRIC_RESULT';
+export const RECEIVE_METRIC_RESULT_SUCCESS = 'RECEIVE_METRIC_RESULT_SUCCESS';
+export const RECEIVE_METRIC_RESULT_FAILURE = 'RECEIVE_METRIC_RESULT_FAILURE';
+
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 696af5aed75..16a34a6c026 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,11 +1,85 @@
import Vue from 'vue';
import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
-import { normalizeMetrics, normalizeMetric, normalizeQueryResult } from './utils';
+import { normalizeMetric, normalizeQueryResult } from './utils';
+import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
+import { metricStates } from '../constants';
+import httpStatusCodes from '~/lib/utils/http_status';
-const normalizePanel = panel => panel.metrics.map(normalizeMetric);
+const normalizePanelMetrics = (metrics, defaultLabel) =>
+ metrics.map(metric => ({
+ ...normalizeMetric(metric),
+ label: metric.label || defaultLabel,
+ }));
+
+/**
+ * Locate and return a metric in the dashboard by its id
+ * as generated by `uniqMetricsId()`.
+ * @param {String} metricId Unique id in the dashboard
+ * @param {Object} dashboard Full dashboard object
+ */
+const findMetricInDashboard = (metricId, dashboard) => {
+ let res = null;
+ dashboard.panel_groups.forEach(group => {
+ group.panels.forEach(panel => {
+ panel.metrics.forEach(metric => {
+ if (metric.metric_id === metricId) {
+ res = metric;
+ }
+ });
+ });
+ });
+ return res;
+};
+
+/**
+ * Set a new state for a metric.
+ *
+ * Initally metric data is not populated, so `Vue.set` is
+ * used to add new properties to the metric.
+ *
+ * @param {Object} metric - Metric object as defined in the dashboard
+ * @param {Object} state - New state
+ * @param {Array|null} state.result - Array of results
+ * @param {String} state.error - Error code from metricStates
+ * @param {Boolean} state.loading - True if the metric is loading
+ */
+const setMetricState = (metric, { result = null, loading = false, state = null }) => {
+ Vue.set(metric, 'result', result);
+ Vue.set(metric, 'loading', loading);
+ Vue.set(metric, 'state', state);
+};
+
+/**
+ * Maps a backened error state to a `metricStates` constant
+ * @param {Object} error - Error from backend response
+ */
+const emptyStateFromError = error => {
+ if (!error) {
+ return metricStates.UNKNOWN_ERROR;
+ }
+
+ // Special error responses
+ if (error.message === BACKOFF_TIMEOUT) {
+ return metricStates.TIMEOUT;
+ }
+
+ // Axios error responses
+ const { response } = error;
+ if (response && response.status === httpStatusCodes.SERVICE_UNAVAILABLE) {
+ return metricStates.CONNECTION_FAILED;
+ } else if (response && response.status === httpStatusCodes.BAD_REQUEST) {
+ // Note: "error.response.data.error" may contain Prometheus error information
+ return metricStates.BAD_QUERY;
+ }
+
+ return metricStates.UNKNOWN_ERROR;
+};
export default {
+ /**
+ * Dashboard panels structure and global state
+ */
[types.REQUEST_METRICS_DATA](state) {
state.emptyState = 'loading';
state.showEmptyState = true;
@@ -13,28 +87,18 @@ export default {
[types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) {
state.dashboard.panel_groups = groupData.map((group, i) => {
const key = `${slugify(group.group || 'default')}-${i}`;
- let { metrics = [], panels = [] } = group;
+ let { panels = [] } = group;
// each panel has metric information that needs to be normalized
-
panels = panels.map(panel => ({
...panel,
- metrics: normalizePanel(panel),
- }));
-
- // for backwards compatibility, and to limit Vue template changes:
- // for each group alias panels to metrics
- // for each panel alias metrics to queries
- metrics = panels.map(panel => ({
- ...panel,
- queries: panel.metrics,
+ metrics: normalizePanelMetrics(panel.metrics, panel.y_label),
}));
return {
...group,
panels,
key,
- metrics: normalizeMetrics(metrics),
};
});
@@ -46,6 +110,10 @@ export default {
state.emptyState = error ? 'unableToConnect' : 'noData';
state.showEmptyState = true;
},
+
+ /**
+ * Deployments and environments
+ */
[types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](state, deployments) {
state.deploymentData = deployments;
},
@@ -58,26 +126,47 @@ export default {
[types.RECEIVE_ENVIRONMENTS_DATA_FAILURE](state) {
state.environments = [];
},
- [types.SET_QUERY_RESULT](state, { metricId, result }) {
- if (!metricId || !result || result.length === 0) {
+
+ /**
+ * Individual panel/metric results
+ */
+ [types.REQUEST_METRIC_RESULT](state, { metricId }) {
+ const metric = findMetricInDashboard(metricId, state.dashboard);
+ setMetricState(metric, {
+ loading: true,
+ state: metricStates.LOADING,
+ });
+ },
+ [types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, result }) {
+ if (!metricId) {
return;
}
state.showEmptyState = false;
- state.dashboard.panel_groups.forEach(group => {
- group.metrics.forEach(metric => {
- metric.queries.forEach(query => {
- if (query.metric_id === metricId) {
- state.metricsWithData.push(metricId);
- // ensure dates/numbers are correctly formatted for charts
- const normalizedResults = result.map(normalizeQueryResult);
- Vue.set(query, 'result', Object.freeze(normalizedResults));
- }
- });
+ const metric = findMetricInDashboard(metricId, state.dashboard);
+ if (!result || result.length === 0) {
+ setMetricState(metric, {
+ state: metricStates.NO_DATA,
});
+ } else {
+ const normalizedResults = result.map(normalizeQueryResult);
+ setMetricState(metric, {
+ result: Object.freeze(normalizedResults),
+ state: metricStates.OK,
+ });
+ }
+ },
+ [types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) {
+ if (!metricId) {
+ return;
+ }
+ const metric = findMetricInDashboard(metricId, state.dashboard);
+ setMetricState(metric, {
+ state: emptyStateFromError(error),
});
},
+
[types.SET_ENDPOINTS](state, endpoints) {
state.metricsEndpoint = endpoints.metricsEndpoint;
state.environmentsEndpoint = endpoints.environmentsEndpoint;
@@ -101,6 +190,6 @@ export default {
},
[types.SET_PANEL_GROUP_METRICS](state, payload) {
const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key);
- panelGroup.metrics = payload.metrics;
+ panelGroup.panels = payload.panels;
},
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index 87e94311176..ee8a85ea222 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -1,8 +1,6 @@
import invalidUrl from '~/lib/utils/invalid_url';
export default () => ({
- hasMetrics: false,
- showPanels: true,
metricsEndpoint: null,
environmentsEndpoint: null,
deploymentsEndpoint: null,
@@ -10,12 +8,13 @@ export default () => ({
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
+
dashboard: {
panel_groups: [],
},
+
deploymentData: [],
environments: [],
- metricsWithData: [],
allDashboards: [],
currentDashboard: null,
projectPath: null,
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 8a396b15a31..3300d2032d0 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -1,83 +1,21 @@
import _ from 'underscore';
-function checkQueryEmptyData(query) {
- return {
- ...query,
- result: query.result.filter(timeSeries => {
- const newTimeSeries = timeSeries;
- const hasValue = series =>
- !Number.isNaN(series[1]) && (series[1] !== null || series[1] !== undefined);
- const hasNonNullValue = timeSeries.values.find(hasValue);
-
- newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
-
- return newTimeSeries.values.length > 0;
- }),
- };
-}
-
-function removeTimeSeriesNoData(queries) {
- return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []);
-}
-
-// Metrics and queries are currently stored 1:1, so `queries` is an array of length one.
-// We want to group queries onto a single chart by title & y-axis label.
-// This function will no longer be required when metrics:queries are 1:many,
-// though there is no consequence if the function stays in use.
-// @param metrics [Array<Object>]
-// Ex) [
-// { id: 1, title: 'title', y_label: 'MB', queries: [{ ...query1Attrs }] },
-// { id: 2, title: 'title', y_label: 'MB', queries: [{ ...query2Attrs }] },
-// { id: 3, title: 'new title', y_label: 'MB', queries: [{ ...query3Attrs }] }
-// ]
-// @return [Array<Object>]
-// Ex) [
-// { title: 'title', y_label: 'MB', queries: [{ metricId: 1, ...query1Attrs },
-// { metricId: 2, ...query2Attrs }] },
-// { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]}
-// ]
-export function groupQueriesByChartInfo(metrics) {
- const metricsByChart = metrics.reduce((accumulator, metric) => {
- const { queries, ...chart } = metric;
-
- const chartKey = `${chart.title}|${chart.y_label}`;
- accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] };
-
- queries.forEach(queryAttrs => {
- let metricId;
-
- if (chart.id) {
- metricId = chart.id.toString();
- } else if (queryAttrs.metric_id) {
- metricId = queryAttrs.metric_id.toString();
- } else {
- metricId = null;
- }
-
- accumulator[chartKey].queries.push({ metricId, ...queryAttrs });
- });
-
- return accumulator;
- }, {});
-
- return Object.values(metricsByChart);
-}
-
export const uniqMetricsId = metric => `${metric.metric_id}_${metric.id}`;
/**
- * Not to confuse with normalizeMetrics (plural)
* Metrics loaded from project-defined dashboards do not have a metric_id.
* This method creates a unique ID combining metric_id and id, if either is present.
* This is hopefully a temporary solution until BE processes metrics before passing to fE
* @param {Object} metric - metric
* @returns {Object} - normalized metric with a uniqueID
*/
+
export const normalizeMetric = (metric = {}) =>
_.omit(
{
...metric,
metric_id: uniqMetricsId(metric),
+ metricId: uniqMetricsId(metric),
},
'id',
);
@@ -93,6 +31,11 @@ export const normalizeQueryResult = timeSeries => {
Number(value),
]),
};
+ // Check result for empty data
+ normalizedResult.values = normalizedResult.values.filter(series => {
+ const hasValue = d => !Number.isNaN(d[1]) && (d[1] !== null || d[1] !== undefined);
+ return series.find(hasValue);
+ });
} else if (timeSeries.value) {
normalizedResult = {
...timeSeries,
@@ -102,21 +45,3 @@ export const normalizeQueryResult = timeSeries => {
return normalizedResult;
};
-
-export const normalizeMetrics = metrics => {
- const groupedMetrics = groupQueriesByChartInfo(metrics);
-
- return groupedMetrics.map(metric => {
- const queries = metric.queries.map(query => ({
- ...query,
- // custom metrics do not require a label, so we should ensure this attribute is defined
- label: query.label || metric.y_label,
- result: (query.result || []).map(normalizeQueryResult),
- }));
-
- return {
- ...metric,
- queries: removeTimeSeriesNoData(queries),
- };
- });
-};
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 2ae1647011d..c824d6d4ddb 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -72,10 +72,9 @@ export const ISODateToString = date => dateformat(date, dateFormats.dateTimePick
*/
export const graphDataValidatorForValues = (isValues, graphData) => {
const responseValueKeyName = isValues ? 'value' : 'values';
-
return (
- Array.isArray(graphData.queries) &&
- graphData.queries.filter(query => {
+ Array.isArray(graphData.metrics) &&
+ graphData.metrics.filter(query => {
if (Array.isArray(query.result)) {
return (
query.result.filter(res => Array.isArray(res[responseValueKeyName])).length ===
@@ -83,7 +82,7 @@ export const graphDataValidatorForValues = (isValues, graphData) => {
);
}
return false;
- }).length === graphData.queries.length
+ }).length === graphData.metrics.filter(query => query.result).length
);
};
@@ -116,6 +115,7 @@ export const generateLinkToChartOptions = chartLink => {
/**
* Tracks snowplow event when user downloads CSV of cluster metric
* @param {String} chart title that will be sent as a property for the event
+ * @return {Object} config object for event tracking
*/
export const downloadCSVOptions = title => {
const isCLusterHealthBoard = isClusterHealthBoard();
@@ -131,7 +131,19 @@ export const downloadCSVOptions = title => {
};
/**
- * This function validates the graph data contains exactly 3 queries plus
+ * Generate options for snowplow to track adding a new metric via the dashboard
+ * custom metric modal
+ * @return {Object} config object for event tracking
+ */
+export const getAddMetricTrackingOptions = () => ({
+ category: document.body.dataset.page,
+ action: 'click_button',
+ label: 'add_new_metric',
+ property: 'modal',
+});
+
+/**
+ * This function validates the graph data contains exactly 3 metrics plus
* value validations from graphDataValidatorForValues.
* @param {Object} isValues
* @param {Object} graphData the graph data response from a prometheus request
@@ -140,8 +152,8 @@ export const downloadCSVOptions = title => {
export const graphDataValidatorForAnomalyValues = graphData => {
const anomalySeriesCount = 3; // metric, upper, lower
return (
- graphData.queries &&
- graphData.queries.length === anomalySeriesCount &&
+ graphData.metrics &&
+ graphData.metrics.length === anomalySeriesCount &&
graphDataValidatorForValues(false, graphData)
);
};
diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue
index b81600660f6..ce08b0964a1 100644
--- a/app/assets/javascripts/mr_popover/components/mr_popover.vue
+++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue
@@ -45,7 +45,7 @@ export default {
return this.mergeRequest.headPipeline && this.mergeRequest.headPipeline.detailedStatus;
},
formattedTime() {
- return this.timeFormated(this.mergeRequest.createdAt);
+ return this.timeFormatted(this.mergeRequest.createdAt);
},
statusBoxClass() {
switch (this.mergeRequest.state) {
diff --git a/app/assets/javascripts/mr_tabs_popover/components/popover.vue b/app/assets/javascripts/mr_tabs_popover/components/popover.vue
new file mode 100644
index 00000000000..da1e1e70993
--- /dev/null
+++ b/app/assets/javascripts/mr_tabs_popover/components/popover.vue
@@ -0,0 +1,64 @@
+<script>
+import { GlPopover, GlButton, GlLink } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ components: {
+ GlPopover,
+ GlButton,
+ GlLink,
+ Icon,
+ },
+ props: {
+ dismissEndpoint: {
+ type: String,
+ required: true,
+ },
+ featureId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showPopover: false,
+ };
+ },
+ mounted() {
+ setTimeout(() => {
+ this.showPopover = true;
+ }, 2000);
+ },
+ methods: {
+ onDismiss() {
+ this.showPopover = false;
+
+ axios.post(this.dismissEndpoint, {
+ feature_name: this.featureId,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-popover target="#diffs-tab" placement="bottom" :show="showPopover">
+ <p class="mb-2">
+ {{
+ __(
+ 'Now you can access the merge request navigation tabs at the top, where they’re easier to find.',
+ )
+ }}
+ </p>
+ <p>
+ <gl-link href="https://gitlab.com/gitlab-org/gitlab/issues/36125" target="_blank">
+ {{ __('More information and share feedback') }}
+ <icon name="external-link" :size="10" />
+ </gl-link>
+ </p>
+ <gl-button variant="primary" size="sm" @click="onDismiss">
+ {{ __('Got it') }}
+ </gl-button>
+ </gl-popover>
+</template>
diff --git a/app/assets/javascripts/mr_tabs_popover/index.js b/app/assets/javascripts/mr_tabs_popover/index.js
new file mode 100644
index 00000000000..9ee0ba046f0
--- /dev/null
+++ b/app/assets/javascripts/mr_tabs_popover/index.js
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import Popover from './components/popover.vue';
+
+export default el =>
+ new Vue({
+ el,
+ render(createElement) {
+ return createElement(Popover, {
+ props: { dismissEndpoint: el.dataset.dismissEndpoint, featureId: el.dataset.featureId },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index 9e4a92426ee..753aa96bb55 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,7 +1,7 @@
<script>
-/* global katex */
import marked from 'marked';
import sanitize from 'sanitize-html';
+import katex from 'katex';
import Prompt from './prompt.vue';
const renderer = new marked.Renderer();
@@ -70,7 +70,6 @@ renderer.paragraph = t => {
};
marked.setOptions({
- sanitize: true,
renderer,
});
@@ -87,9 +86,66 @@ export default {
computed: {
markdown() {
return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), {
- allowedTags: false,
+ // allowedTags from GitLab's inline HTML guidelines
+ // https://docs.gitlab.com/ee/user/markdown.html#inline-html
+ allowedTags: [
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'h7',
+ 'h8',
+ 'br',
+ 'b',
+ 'i',
+ 'strong',
+ 'em',
+ 'a',
+ 'pre',
+ 'code',
+ 'img',
+ 'tt',
+ 'div',
+ 'ins',
+ 'del',
+ 'sup',
+ 'sub',
+ 'p',
+ 'ol',
+ 'ul',
+ 'table',
+ 'thead',
+ 'tbody',
+ 'tfoot',
+ 'blockquote',
+ 'dl',
+ 'dt',
+ 'dd',
+ 'kbd',
+ 'q',
+ 'samp',
+ 'var',
+ 'hr',
+ 'ruby',
+ 'rt',
+ 'rp',
+ 'li',
+ 'tr',
+ 'td',
+ 'th',
+ 's',
+ 'strike',
+ 'span',
+ 'abbr',
+ 'abbr',
+ 'summary',
+ ],
allowedAttributes: {
- '*': ['class'],
+ '*': ['class', 'style'],
+ a: ['href'],
+ img: ['src'],
},
});
},
@@ -105,6 +161,15 @@ export default {
</template>
<style>
+/*
+ Importing the necessary katex stylesheet from the node_module folder rather
+ than copying the stylesheet into `app/assets/stylesheets/vendors` for
+ automatic importing via `app/assets/stylesheets/application.scss`. The reason
+ is that the katex stylesheet depends on many fonts that are in node_module
+ subfolders - moving all these fonts would make updating katex difficult.
+ */
+@import '~katex/dist/katex.min.css';
+
.markdown .katex {
display: block;
text-align: center;
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index defa278c089..1a8f1c659a4 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,7 +1,7 @@
-/* eslint-disable no-restricted-properties, no-var, camelcase,
-no-unused-expressions, one-var, default-case,
+/* eslint-disable no-restricted-properties, camelcase,
+no-unused-expressions, default-case,
consistent-return, no-alert, no-param-reassign, no-else-return,
-vars-on-top, no-shadow, no-useless-escape,
+no-shadow, no-useless-escape,
class-methods-use-this */
/* global ResolveService */
@@ -16,10 +16,10 @@ import Cookies from 'js-cookie';
import Autosize from 'autosize';
import 'jquery.caret'; // required by at.js
import 'at.js';
-import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
-import syntaxHighlight from '~/syntax_highlight';
import { GlSkeletonLoading } from '@gitlab/ui';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import syntaxHighlight from '~/syntax_highlight';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
@@ -224,18 +224,18 @@ export default class Notes {
}
keydownNoteText(e) {
- var $textarea,
- discussionNoteForm,
- editNote,
- myLastNote,
- myLastNoteEditBtn,
- newText,
- originalText;
+ let discussionNoteForm;
+ let editNote;
+ let myLastNote;
+ let myLastNoteEditBtn;
+ let newText;
+ let originalText;
+
if (isMetaKey(e)) {
return;
}
- $textarea = $(e.target);
+ const $textarea = $(e.target);
// Edit previous note when UP arrow is hit
switch (e.which) {
case 38:
@@ -325,11 +325,10 @@ export default class Notes {
* if there aren't new notes coming from the server
*/
setPollingInterval(shouldReset) {
- var nthInterval;
if (shouldReset == null) {
shouldReset = true;
}
- nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
+ const nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
if (shouldReset) {
this.pollingInterval = this.basePollingInterval;
} else if (this.pollingInterval < nthInterval) {
@@ -339,7 +338,7 @@ export default class Notes {
}
handleQuickActions(noteEntity) {
- var votesBlock;
+ let votesBlock;
if (noteEntity.commands_changes) {
if ('merge' in noteEntity.commands_changes) {
Notes.checkMergeRequestStatus();
@@ -462,14 +461,16 @@ export default class Notes {
* Render note in discussion area. To render inline notes use renderDiscussionNote.
*/
renderDiscussionNote(noteEntity, $form) {
- var discussionContainer, form, row, lineType, diffAvatarContainer;
+ let discussionContainer;
+ let row;
if (!Notes.isNewNote(noteEntity, this.note_ids)) {
return;
}
this.note_ids.push(noteEntity.id);
- form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
+ const form =
+ $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row =
form.length || !noteEntity.discussion_line_code
? form.closest('tr')
@@ -479,8 +480,8 @@ export default class Notes {
row = form;
}
- lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
- diffAvatarContainer = row
+ const lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
+ const diffAvatarContainer = row
.prevAll('.line_holder')
.first()
.find(`.js-avatar-container.${lineType}_line`);
@@ -491,15 +492,17 @@ export default class Notes {
}
if (discussionContainer.length === 0) {
if (noteEntity.diff_discussion_html) {
- var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
+ const $discussion = $(noteEntity.diff_discussion_html).renderGFM();
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
// insert the note and the reply button after the temp row
row.after($discussion);
} else {
// Merge new discussion HTML in
- var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
- var contentContainerClass = $notes
+ const $notes = $discussion.find(
+ `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
+ );
+ const contentContainerClass = $notes
.closest('.notes-content')
.attr('class')
.split(' ')
@@ -537,7 +540,7 @@ export default class Notes {
}
renderDiscussionAvatar(diffAvatarContainer, noteEntity) {
- var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
+ let avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
if (!avatarHolder.length) {
avatarHolder = document.createElement('diff-note-avatars');
@@ -557,8 +560,7 @@ export default class Notes {
* Resets buttons.
*/
resetMainTargetForm(e) {
- var form;
- form = $('.js-main-target-form');
+ const form = $('.js-main-target-form');
// remove validation errors
form.find('.js-errors').remove();
// reset text and preview
@@ -572,7 +574,7 @@ export default class Notes {
.data('autosave')
.reset();
- var event = document.createEvent('Event');
+ const event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
form.find('.js-autosize')[0].dispatchEvent(event);
@@ -580,8 +582,7 @@ export default class Notes {
}
reenableTargetFormSubmitButton() {
- var form;
- form = $('.js-main-target-form');
+ const form = $('.js-main-target-form');
return form.find('.js-note-text').trigger('input');
}
@@ -591,9 +592,8 @@ export default class Notes {
* Sets some hidden fields in the form.
*/
setupMainTargetNoteForm(enableGFM) {
- var form;
// find the form
- form = $('.js-new-note-form');
+ const form = $('.js-new-note-form');
// Set a global clone of the form for later cloning
this.formClone = form.clone();
// show the form
@@ -626,10 +626,9 @@ export default class Notes {
* show the form
*/
setupNoteForm(form, enableGFM = defaultAutocompleteConfig) {
- var textarea, key;
this.glForm = new GLForm(form, enableGFM);
- textarea = form.find('.js-note-text');
- key = [
+ const textarea = form.find('.js-note-text');
+ const key = [
s__('NoteForm|Note'),
form.find('#note_noteable_type').val(),
form.find('#note_noteable_id').val(),
@@ -686,8 +685,8 @@ export default class Notes {
*/
addDiscussionNote($form, note, isNewDiffComment) {
if ($form.attr('data-resolve-all') != null) {
- var discussionId = $form.data('discussionId');
- var mergeRequestId = $form.data('noteableIid');
+ const discussionId = $form.data('discussionId');
+ const mergeRequestId = $form.data('noteableIid');
if (ResolveService != null) {
ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId);
@@ -707,13 +706,12 @@ export default class Notes {
* Updates the current note field.
*/
updateNote(noteEntity, $targetNote) {
- var $noteEntityEl, $note_li;
// Convert returned HTML to a jQuery object so we can modify it further
- $noteEntityEl = $(noteEntity.html);
+ const $noteEntityEl = $(noteEntity.html);
this.revertNoteEditForm($targetNote);
$noteEntityEl.renderGFM();
// Find the note's `li` element by ID and replace it with the updated HTML
- $note_li = $(`.note-row-${noteEntity.id}`);
+ const $note_li = $(`.note-row-${noteEntity.id}`);
$note_li.replaceWith($noteEntityEl);
this.setupNewNote($noteEntityEl);
@@ -724,17 +722,17 @@ export default class Notes {
}
checkContentToAllowEditing($el) {
- var initialContent = $el
+ const initialContent = $el
.find('.original-note-content')
.text()
.trim();
- var currentContent = $el.find('.js-note-text').val();
- var isAllowed = true;
+ const currentContent = $el.find('.js-note-text').val();
+ let isAllowed = true;
if (currentContent === initialContent) {
this.removeNoteEditForm($el);
} else {
- var isWidgetVisible = isInViewport($el.get(0));
+ const isWidgetVisible = isInViewport($el.get(0));
if (!isWidgetVisible) {
scrollToElement($el);
@@ -756,13 +754,13 @@ export default class Notes {
showEditForm(e) {
e.preventDefault();
- var $target = $(e.target);
- var $editForm = $(this.getEditFormSelector($target));
- var $note = $target.closest('.note');
- var $currentlyEditing = $('.note.is-editing:visible');
+ const $target = $(e.target);
+ const $editForm = $(this.getEditFormSelector($target));
+ const $note = $target.closest('.note');
+ const $currentlyEditing = $('.note.is-editing:visible');
if ($currentlyEditing.length) {
- var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);
+ const isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);
if (!isEditAllowed) {
return;
@@ -802,8 +800,8 @@ export default class Notes {
revertNoteEditForm($target) {
$target = $target || $('.note.is-editing:visible');
- var selector = this.getEditFormSelector($target);
- var $editForm = $(selector);
+ const selector = this.getEditFormSelector($target);
+ const $editForm = $(selector);
$editForm.insertBefore('.diffs');
$editForm.find('.js-comment-save-button').enable();
@@ -811,7 +809,7 @@ export default class Notes {
}
getEditFormSelector($el) {
- var selector = '.note-edit-form:not(.mr-note-edit-form)';
+ let selector = '.note-edit-form:not(.mr-note-edit-form)';
if ($el.parents('#diffs').length) {
selector = '.note-edit-form.mr-note-edit-form';
@@ -821,7 +819,7 @@ export default class Notes {
}
removeNoteEditForm($note) {
- var form = $note.find('.diffs .current-note-edit-form');
+ const form = $note.find('.diffs .current-note-edit-form');
$note.removeClass('is-editing');
form.removeClass('current-note-edit-form');
@@ -837,9 +835,8 @@ export default class Notes {
* Removes the whole discussion if the last note is being removed.
*/
removeNote(e) {
- var noteElId, $note;
- $note = $(e.currentTarget).closest('.note');
- noteElId = $note.attr('id');
+ const $note = $(e.currentTarget).closest('.note');
+ const noteElId = $note.attr('id');
$(`.note[id="${noteElId}"]`).each((i, el) => {
// A same note appears in the "Discussion" and in the "Changes" tab, we have
// to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
@@ -915,9 +912,8 @@ export default class Notes {
}
replyToDiscussionNote(target) {
- var form, replyLink;
- form = this.cleanForm(this.formClone.clone());
- replyLink = $(target).closest('.js-discussion-reply-button');
+ const form = this.cleanForm(this.formClone.clone());
+ const replyLink = $(target).closest('.js-discussion-reply-button');
// insert the form after the button
replyLink
.closest('.discussion-reply-holder')
@@ -942,7 +938,7 @@ export default class Notes {
diffFileData = dataHolder.closest('.image');
}
- var discussionID = dataHolder.data('discussionId');
+ const discussionID = dataHolder.data('discussionId');
if (discussionID) {
form.attr('data-discussion-id', discussionID);
@@ -985,7 +981,7 @@ export default class Notes {
form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- var $commentBtn = form.find('comment-and-resolve-btn');
+ const $commentBtn = form.find('comment-and-resolve-btn');
$commentBtn.attr(':discussion-id', `'${discussionID}'`);
gl.diffNotesCompileComponents();
@@ -1042,16 +1038,20 @@ export default class Notes {
}
toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) {
- var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd;
- $link = $(target);
- row = $link.closest('tr');
+ let addForm;
+ let newForm;
+ let noteForm;
+ let replyButton;
+ let rowCssToAdd;
+ const $link = $(target);
+ const row = $link.closest('tr');
const nextRow = row.next();
let targetRow = row;
if (nextRow.is('.notes_holder')) {
targetRow = nextRow;
}
- hasNotes = nextRow.is('.notes_holder');
+ const hasNotes = nextRow.is('.notes_holder');
addForm = false;
let lineTypeSelector = '';
rowCssToAdd =
@@ -1111,9 +1111,8 @@ export default class Notes {
* Removes the form and if necessary it's temporary row.
*/
removeDiscussionNoteForm(form) {
- var glForm, row;
- row = form.closest('tr');
- glForm = form.data('glForm');
+ const row = form.closest('tr');
+ const glForm = form.data('glForm');
glForm.destroy();
form
.find('.js-note-text')
@@ -1158,10 +1157,9 @@ export default class Notes {
* Updates the file name for the selected attachment.
*/
updateFormAttachment() {
- var filename, form;
- form = $(this).closest('form');
+ const form = $(this).closest('form');
// get only the basename
- filename = $(this)
+ const filename = $(this)
.val()
.replace(/^.*[\\\/]/, '');
return form.find('.js-attachment-filename').text(filename);
@@ -1175,11 +1173,12 @@ export default class Notes {
}
updateTargetButtons(e) {
- var closebtn, closetext, form, reopenbtn, reopentext, textarea;
- textarea = $(e.target);
- form = textarea.parents('form');
- reopenbtn = form.find('.js-note-target-reopen');
- closebtn = form.find('.js-note-target-close');
+ let closetext;
+ let reopentext;
+ const textarea = $(e.target);
+ const form = textarea.parents('form');
+ const reopenbtn = form.find('.js-note-target-reopen');
+ const closebtn = form.find('.js-note-target-close');
if (textarea.val().trim().length > 0) {
reopentext = reopenbtn.attr('data-alternative-text');
@@ -1215,16 +1214,16 @@ export default class Notes {
}
putEditFormInPlace($el) {
- var $editForm = $(this.getEditFormSelector($el));
- var $note = $el.closest('.note');
+ const $editForm = $(this.getEditFormSelector($el));
+ const $note = $el.closest('.note');
$editForm.insertAfter($note.find('.note-text'));
- var $originalContentEl = $note.find('.original-note-content');
- var originalContent = $originalContentEl.text().trim();
- var postUrl = $originalContentEl.data('postUrl');
- var targetId = $originalContentEl.data('targetId');
- var targetType = $originalContentEl.data('targetType');
+ const $originalContentEl = $note.find('.original-note-content');
+ const originalContent = $originalContentEl.text().trim();
+ const postUrl = $originalContentEl.data('postUrl');
+ const targetId = $originalContentEl.data('targetId');
+ const targetType = $originalContentEl.data('targetType');
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index fda494fec07..492d8de3802 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -193,23 +193,10 @@ export default {
this.stopPolling();
this.saveNote(noteData)
- .then(res => {
+ .then(() => {
this.enableButton();
this.restartPolling();
-
- if (res.errors) {
- if (res.errors.commands_only) {
- this.discard();
- } else {
- Flash(
- __('Something went wrong while adding your comment. Please try again.'),
- 'alert',
- this.$refs.commentForm,
- );
- }
- } else {
- this.discard();
- }
+ this.discard();
if (withIssueAction) {
this.toggleIssueState();
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index df537ba1ed2..fe22737c7fc 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,10 +1,10 @@
<script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import { mapState, mapActions } from 'vuex';
+import { GlSkeletonLoading } from '@gitlab/ui';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
-import { GlSkeletonLoading } from '@gitlab/ui';
import { getDiffMode } from '~/diffs/store/utils';
import { diffViewerModes } from '~/ide/constants';
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index d7ffa0abb79..98f1f385e9b 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -19,6 +19,7 @@ export default {
'resolvableDiscussionsCount',
'firstUnresolvedDiscussionId',
'unresolvedDiscussionsCount',
+ 'getDiscussion',
]),
isLoggedIn() {
return this.getUserData.id;
@@ -40,9 +41,10 @@ export default {
...mapActions(['expandDiscussion']),
jumpToFirstUnresolvedDiscussion() {
const diffTab = window.mrTabs.currentAction === 'diffs';
- const discussionId = this.firstUnresolvedDiscussionId(diffTab);
-
- this.jumpToDiscussion(discussionId);
+ const discussionId =
+ this.firstUnresolvedDiscussionId(diffTab) || this.firstUnresolvedDiscussionId();
+ const firstDiscussion = this.getDiscussion(discussionId);
+ this.jumpToDiscussion(firstDiscussion);
},
},
};
diff --git a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
index 07a5bda6bcb..f87ca097b40 100644
--- a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
@@ -1,6 +1,6 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
import { GlTooltipDirective } from '@gitlab/ui';
+import icon from '~/vue_shared/components/icon.vue';
export default {
name: 'JumpToNextDiscussionButton',
diff --git a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
index 7fbfe8eebb2..7d742fbfeee 100644
--- a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
+++ b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
@@ -19,7 +19,11 @@ export default {
};
},
computed: {
- ...mapGetters(['nextUnresolvedDiscussionId', 'previousUnresolvedDiscussionId']),
+ ...mapGetters([
+ 'nextUnresolvedDiscussionId',
+ 'previousUnresolvedDiscussionId',
+ 'getDiscussion',
+ ]),
},
mounted() {
Mousetrap.bind('n', () => this.jumpToNextDiscussion());
@@ -33,14 +37,14 @@ export default {
...mapActions(['expandDiscussion']),
jumpToNextDiscussion() {
const nextId = this.nextUnresolvedDiscussionId(this.currentDiscussionId, this.isDiffView);
-
- this.jumpToDiscussion(nextId);
+ const nextDiscussion = this.getDiscussion(nextId);
+ this.jumpToDiscussion(nextDiscussion);
this.currentDiscussionId = nextId;
},
jumpToPreviousDiscussion() {
const prevId = this.previousUnresolvedDiscussionId(this.currentDiscussionId, this.isDiffView);
-
- this.jumpToDiscussion(prevId);
+ const prevDiscussion = this.getDiscussion(prevId);
+ this.jumpToDiscussion(prevDiscussion);
this.currentDiscussionId = prevId;
},
},
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index 53f509185a8..8636984c6af 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -12,6 +12,9 @@ export default {
},
mixins: [Issuable, issuableStateMixin],
computed: {
+ projectArchivedWarning() {
+ return __('This project is archived and cannot be commented on.');
+ },
lockedIssueWarning() {
return sprintf(
__('This %{issuableDisplayName} is locked. Only project members can comment.'),
@@ -26,9 +29,15 @@ export default {
<div class="disabled-comment text-center">
<span class="issuable-note-warning inline">
<icon :size="16" name="lock" class="icon" />
- <span>
- {{ lockedIssueWarning }}
+ <span v-if="isProjectArchived">
+ {{ projectArchivedWarning }}
+ <gl-link :href="archivedProjectDocsPath" target="_blank" class="learn-more">
+ {{ __('Learn more') }}
+ </gl-link>
+ </span>
+ <span v-else>
+ {{ lockedIssueWarning }}
<gl-link :href="lockedIssueDocsPath" target="_blank" class="learn-more">
{{ __('Learn more') }}
</gl-link>
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
index f03e6fd73d7..1d1529bfa5b 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -1,6 +1,6 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'ResolveWithIssueButton',
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 89d434a60ba..dc514f00801 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import resolvedStatusMixin from 'ee_else_ce/batch_comments/mixins/resolved_status';
+import Icon from '~/vue_shared/components/icon.vue';
import ReplyButton from './note_actions/reply_button.vue';
export default {
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index d4a57d5d58d..df62e379017 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -116,16 +116,20 @@ export default {
// We have 10+ awarded user, join them with comma and add `and x more`.
if (remainingAwardList.length) {
- title = sprintf(__(`%{listToShow}, and %{awardsListLength} more.`), {
- listToShow: namesToShow.join(', '),
- awardsListLength: remainingAwardList.length,
- });
+ title = sprintf(
+ __(`%{listToShow}, and %{awardsListLength} more.`),
+ {
+ listToShow: namesToShow.join(', '),
+ awardsListLength: remainingAwardList.length,
+ },
+ false,
+ );
} else if (namesToShow.length > 1) {
// Join all names with comma but not the last one, it will be added with and text.
title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
// If we have more than 2 users we need an extra comma before and text.
title += namesToShow.length > 2 ? ',' : '';
- title += sprintf(__(` and %{sliced}`), { sliced: namesToShow.slice(-1) }); // Append and text
+ title += sprintf(__(` and %{sliced}`), { sliced: namesToShow.slice(-1) }, false); // Append and text
} else {
// We have only 2 users so join them with and.
title = namesToShow.join(__(' and '));
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 222badf70d1..b024884bea0 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,7 +1,7 @@
<script>
-import { mergeUrlParams } from '~/lib/utils/url_utility';
import { mapGetters, mapActions } from 'vuex';
import noteFormMixin from 'ee_else_ce/notes/mixins/note_form';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 47ec740b63a..1f31720ff40 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,10 +1,10 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
+import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import { s__, __ } from '~/locale';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import icon from '~/vue_shared/components/icon.vue';
-import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -84,6 +84,7 @@ export default {
'hasUnresolvedDiscussions',
'showJumpToNextDiscussion',
'getUserData',
+ 'getDiscussion',
]),
currentUser() {
return this.getUserData;
@@ -197,23 +198,22 @@ export default {
data: postData,
};
- this.isReplying = false;
this.saveNote(replyData)
- .then(() => {
- clearDraft(this.autosaveKey);
+ .then(res => {
+ if (res.hasFlash !== true) {
+ this.isReplying = false;
+ clearDraft(this.autosaveKey);
+ }
callback();
})
.catch(err => {
this.removePlaceholderNotes();
- this.isReplying = true;
- this.$nextTick(() => {
- const msg = __(
- 'Your comment could not be submitted! Please check your network connection and try again.',
- );
- Flash(msg, 'alert', this.$el);
- this.$refs.noteForm.note = noteText;
- callback(err);
- });
+ const msg = __(
+ 'Your comment could not be submitted! Please check your network connection and try again.',
+ );
+ Flash(msg, 'alert', this.$el);
+ this.$refs.noteForm.note = noteText;
+ callback(err);
});
},
jumpToNextDiscussion() {
@@ -221,8 +221,9 @@ export default {
this.discussion.id,
this.discussionsByDiffOrder,
);
+ const nextDiscussion = this.getDiscussion(nextId);
- this.jumpToDiscussion(nextId);
+ this.jumpToDiscussion(nextDiscussion);
},
deleteNoteHandler(note) {
this.$emit('noteDeleted', this.discussion, note);
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index fa8fc7d02e4..b3dae69d0bc 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -2,9 +2,9 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
+import draftMixin from 'ee_else_ce/notes/mixins/draft';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import draftMixin from 'ee_else_ce/notes/mixins/draft';
import { __, s__, sprintf } from '../../locale';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 9d1de4ef8a0..be2adb07526 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,5 +1,4 @@
<script>
-import { __ } from '~/locale';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash';
@@ -14,6 +13,7 @@ import placeholderNote from '../../vue_shared/components/notes/placeholder_note.
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
+import { __ } from '~/locale';
import initUserPopovers from '../../user_popovers';
export default {
@@ -71,6 +71,9 @@ export default {
'userCanReply',
'discussionTabCounter',
]),
+ discussionTabCounterText() {
+ return this.isLoading ? '' : this.discussionTabCounter;
+ },
noteableType() {
return this.noteableData.noteableType;
},
@@ -95,9 +98,9 @@ export default {
this.fetchNotes();
}
},
- allDiscussions() {
- if (this.discussionsCount && !this.isLoading) {
- this.discussionsCount.textContent = this.discussionTabCounter;
+ discussionTabCounterText(val) {
+ if (this.discussionsCount) {
+ this.discussionsCount.textContent = val;
}
},
},
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index 3d89d907777..94ca01e44cc 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -35,20 +35,26 @@ export default {
return false;
},
- jumpToDiscussion(id) {
+
+ switchToDiscussionsTabAndJumpTo(id) {
+ window.mrTabs.eventHub.$once('MergeRequestTabChange', () => {
+ setTimeout(() => this.discussionJump(id), 0);
+ });
+
+ window.mrTabs.tabShown('show');
+ },
+
+ jumpToDiscussion(discussion) {
+ const { id, diff_discussion: isDiffDiscussion } = discussion;
if (id) {
const activeTab = window.mrTabs.currentAction;
- if (activeTab === 'diffs') {
+ if (activeTab === 'diffs' && isDiffDiscussion) {
this.diffsJump(id);
- } else if (activeTab === 'commits' || activeTab === 'pipelines') {
- window.mrTabs.eventHub.$once('MergeRequestTabChange', () => {
- setTimeout(() => this.discussionJump(id), 0);
- });
-
- window.mrTabs.tabShown('show');
- } else {
+ } else if (activeTab === 'show') {
this.discussionJump(id);
+ } else {
+ this.switchToDiscussionsTabAndJumpTo(id);
}
}
},
diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js
index d97d9f6850a..0ca8c8c98a3 100644
--- a/app/assets/javascripts/notes/mixins/issuable_state.js
+++ b/app/assets/javascripts/notes/mixins/issuable_state.js
@@ -3,6 +3,12 @@ import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters(['getNoteableDataByProp']),
+ isProjectArchived() {
+ return this.getNoteableDataByProp('is_project_archived');
+ },
+ archivedProjectDocsPath() {
+ return this.getNoteableDataByProp('archived_project_docs_path');
+ },
lockedIssueDocsPath() {
return this.getNoteableDataByProp('locked_discussion_docs_path');
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 82c291379ec..9bd245c094d 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import $ from 'jquery';
-import axios from '~/lib/utils/axios_utils';
import Visibility from 'visibilityjs';
+import axios from '~/lib/utils/axios_utils';
import TaskList from '../../task_list';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
@@ -14,7 +14,7 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import Api from '~/api';
let eTagPoll;
@@ -252,29 +252,22 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}
}
- const processErrors = res => {
- const { errors } = res;
- if (!errors || !Object.keys(errors).length) {
- return res;
- }
-
+ const processQuickActions = res => {
+ const { errors: { commands_only: message } = { commands_only: null } } = res;
/*
The following reply means that quick actions have been successfully applied:
{"commands_changes":{},"valid":false,"errors":{"commands_only":["Commands applied"]}}
*/
- if (hasQuickActions) {
+ if (hasQuickActions && message) {
eTagPoll.makeRequest();
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
- const { commands_only: message } = errors;
Flash(message || __('Commands applied'), 'notice', noteData.flashContainer);
-
- return res;
}
- throw new Error(__('Failed to save comment!'));
+ return res;
};
const processEmojiAward = res => {
@@ -321,11 +314,33 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
return res;
};
+ const processErrors = error => {
+ if (error.response) {
+ const {
+ response: { data = {} },
+ } = error;
+ const { errors = {} } = data;
+ const { base = [] } = errors;
+
+ // we handle only errors.base for now
+ if (base.length > 0) {
+ const errorMsg = sprintf(__('Your comment could not be submitted because %{error}'), {
+ error: base[0].toLowerCase(),
+ });
+ Flash(errorMsg, 'alert', noteData.flashContainer);
+ return { ...data, hasFlash: true };
+ }
+ }
+
+ throw error;
+ };
+
return dispatch(methodToDispatch, postData, { root: true })
- .then(processErrors)
+ .then(processQuickActions)
.then(processEmojiAward)
.then(processTimeTracking)
- .then(removePlaceholder);
+ .then(removePlaceholder)
+ .catch(processErrors);
};
const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 78aaa9df0ec..b43d6ba17d7 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -109,7 +109,7 @@ export default {
<template>
<p v-html="text"></p>
<p v-html="confirmationTextLabel"></p>
- <form ref="form" :action="deleteUserUrl" method="post">
+ <form ref="form" :action="deleteUserUrl" method="post" @submit.prevent>
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<gl-form-input
diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
index ff758fcb4fe..24d7b592948 100644
--- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js
+++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
@@ -1,6 +1,6 @@
+import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
-import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
diff --git a/app/assets/javascripts/pages/groups/group_members/index/index.js b/app/assets/javascripts/pages/groups/group_members/index/index.js
index e4f4c3b574e..e77a7cf8e0a 100644
--- a/app/assets/javascripts/pages/groups/group_members/index/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index/index.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
-import memberExpirationDate from '~/member_expiration_date';
import Members from 'ee_else_ce/members';
+import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
document.addEventListener('DOMContentLoaded', () => {
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 090e1a2bc6d..4f15f5ec58c 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,9 +1,9 @@
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import initIssuablesList from '~/issuables_list';
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/pages/constants';
-import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import initManualOrdering from '~/manual_ordering';
const ISSUE_BULK_UPDATE_PREFIX = 'issue_';
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index 7520cfb6da0..13c5c350c24 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -1,8 +1,8 @@
+import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import { FILTERED_SEARCH } from '~/pages/constants';
const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
index 2021ad117e8..f1e7ff87e5a 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -1,6 +1,6 @@
+import _ from 'underscore';
import InputValidator from '~/validators/input_validator';
-import _ from 'underscore';
import fetchGroupPathAvailability from './fetch_group_path_availability';
import flash from '~/flash';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/pages/groups/registry/repositories/index.js b/app/assets/javascripts/pages/groups/registry/repositories/index.js
index b663defad0e..635513afd95 100644
--- a/app/assets/javascripts/pages/groups/registry/repositories/index.js
+++ b/app/assets/javascripts/pages/groups/registry/repositories/index.js
@@ -1,3 +1,3 @@
-import initRegistryImages from '~/registry';
+import initRegistryImages from '~/registry/list';
document.addEventListener('DOMContentLoaded', initRegistryImages);
diff --git a/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js b/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js
index c1056537f90..c1056537f90 100644
--- a/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js
+++ b/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index 13cb0d6f74b..ad003181728 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import createFlash from '~/flash';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import emojiRegex from 'emoji-regex';
+import createFlash from '~/flash';
import EmojiMenu from './emoji_menu';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js
index 96e47187fed..34c7ee2e603 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/pages/projects/issues/form.js
@@ -1,8 +1,8 @@
/* eslint-disable no-new */
import $ from 'jquery';
-import GLForm from '~/gl_form';
import IssuableForm from 'ee_else_ce/issuable_form';
+import GLForm from '~/gl_form';
import LabelsSelect from '~/labels_select';
import MilestoneSelect from '~/milestone_select';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index c73ebb31eb3..bf54ca972b2 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -1,12 +1,12 @@
/* eslint-disable no-new */
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
-import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import initManualOrdering from '~/manual_ordering';
document.addEventListener('DOMContentLoaded', () => {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index 0bcca22e40f..8f93cbb2a42 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -1,8 +1,8 @@
+import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
-import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index e51ab79a51d..76d72efb11b 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -1,10 +1,10 @@
/* eslint-disable no-new */
import $ from 'jquery';
+import IssuableForm from 'ee_else_ce/issuable_form';
import Diff from '~/diff';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
-import IssuableForm from 'ee_else_ce/issuable_form';
import LabelsSelect from '~/labels_select';
import MilestoneSelect from '~/milestone_select';
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 16034313af2..1f8befc07c8 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -6,6 +6,7 @@ import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
import initSourcegraph from '~/sourcegraph';
+import initPopover from '~/mr_tabs_popover';
import initWidget from '../../../vue_merge_request_widget';
export default function() {
@@ -21,4 +22,10 @@ export default function() {
howToMerge();
initWidget();
initSourcegraph();
+
+ const tabHighlightEl = document.querySelector('.js-tabs-feature-highlight');
+
+ if (tabHighlightEl) {
+ initPopover(tabHighlightEl);
+ }
}
diff --git a/app/assets/javascripts/pages/projects/pages_domains/edit/index.js b/app/assets/javascripts/pages/projects/pages_domains/show/index.js
index 27e4433ad4d..27e4433ad4d 100644
--- a/app/assets/javascripts/pages/projects/pages_domains/edit/index.js
+++ b/app/assets/javascripts/pages/projects/pages_domains/show/index.js
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 01acfca158f..739ae1cea16 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, no-return-assign */
+/* eslint-disable func-names, no-return-assign */
import $ from 'jquery';
import Cookies from 'js-cookie';
@@ -90,19 +90,19 @@ export default class Project {
}
static initRefSwitcher() {
- var refListItem = document.createElement('li');
- var refLink = document.createElement('a');
+ const refListItem = document.createElement('li');
+ const refLink = document.createElement('a');
refLink.href = '#';
return $('.js-project-refs-dropdown').each(function() {
- var $dropdown = $(this);
- var selected = $dropdown.data('selected');
- var fieldName = $dropdown.data('fieldName');
- var shouldVisit = Boolean($dropdown.data('visit'));
- var $form = $dropdown.closest('form');
- var action = $form.attr('action');
- var linkTarget = mergeUrlParams(serializeForm($form[0]), action);
+ const $dropdown = $(this);
+ const selected = $dropdown.data('selected');
+ const fieldName = $dropdown.data('fieldName');
+ const shouldVisit = Boolean($dropdown.data('visit'));
+ const $form = $dropdown.closest('form');
+ const action = $form.attr('action');
+ const linkTarget = mergeUrlParams(serializeForm($form[0]), action);
return $dropdown.glDropdown({
data(term, callback) {
@@ -123,9 +123,9 @@ export default class Project {
inputFieldName: $dropdown.data('inputFieldName'),
fieldName,
renderRow(ref) {
- var li = refListItem.cloneNode(false);
+ const li = refListItem.cloneNode(false);
- var link = refLink.cloneNode(false);
+ const link = refLink.cloneNode(false);
if (ref === selected) {
link.className = 'is-active';
diff --git a/app/assets/javascripts/pages/projects/registry/repositories/index.js b/app/assets/javascripts/pages/projects/registry/repositories/index.js
index 35564754ee0..59310b3f76f 100644
--- a/app/assets/javascripts/pages/projects/registry/repositories/index.js
+++ b/app/assets/javascripts/pages/projects/registry/repositories/index.js
@@ -1,3 +1,3 @@
-import initRegistryImages from '~/registry/index';
+import initRegistryImages from '~/registry/list/index';
document.addEventListener('DOMContentLoaded', initRegistryImages);
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 885247335a4..b4aac8eea2b 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -1,6 +1,7 @@
import initSettingsPanels from '~/settings_panels';
import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
+import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
@@ -32,4 +33,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
});
+
+ registrySettingsApp();
});
diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js
index c35b9c30058..738bf08f1bf 100644
--- a/app/assets/javascripts/pages/projects/snippets/show/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/show/index.js
@@ -3,11 +3,16 @@ import ZenMode from '~/zen_mode';
import LineHighlighter from '~/line_highlighter';
import BlobViewer from '~/blob/viewer';
import snippetEmbed from '~/snippet/snippet_embed';
+import initSnippetsApp from '~/snippets';
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();
+ if (!gon.features.snippetsVue) {
+ new LineHighlighter(); // eslint-disable-line no-new
+ new BlobViewer(); // eslint-disable-line no-new
+ initNotes();
+ new ZenMode(); // eslint-disable-line no-new
+ snippetEmbed();
+ } else {
+ initSnippetsApp();
+ }
});
diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
index b0c9ca3ec0d..2176309ac84 100644
--- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -1,7 +1,7 @@
<script>
import _ from 'underscore';
-import { s__, sprintf } from '~/locale';
import { GlModal, GlModalDirective } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index 36d1e773134..25be71d9ed4 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -1,6 +1,6 @@
+import _ from 'underscore';
import InputValidator from '~/validators/input_validator';
-import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js
index 26936110402..6e00c14f43e 100644
--- a/app/assets/javascripts/pages/snippets/show/index.js
+++ b/app/assets/javascripts/pages/snippets/show/index.js
@@ -3,11 +3,16 @@ import BlobViewer from '~/blob/viewer';
import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes';
import snippetEmbed from '~/snippet/snippet_embed';
+import initSnippetsApp from '~/snippets';
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();
+ if (!gon.features.snippetsVue) {
+ new LineHighlighter(); // eslint-disable-line no-new
+ new BlobViewer(); // eslint-disable-line no-new
+ initNotes();
+ new ZenMode(); // eslint-disable-line no-new
+ snippetEmbed();
+ } else {
+ initSnippetsApp();
+ }
});
diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js
index a191df00dfa..cfc6dc61f9f 100644
--- a/app/assets/javascripts/pages/users/index.js
+++ b/app/assets/javascripts/pages/users/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import UserCallout from '~/user_callout';
import Cookies from 'js-cookie';
+import UserCallout from '~/user_callout';
import UserTabs from './user_tabs';
function initUserProfile(action) {
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 8ce653bf1fb..d17c2f33adc 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -52,6 +52,11 @@ export default {
header: s__('PerformanceBar|Redis calls'),
keys: ['cmd'],
},
+ {
+ metric: 'total',
+ header: s__('PerformanceBar|Frontend resources'),
+ keys: ['name', 'size'],
+ },
],
data() {
return { currentRequestId: '' };
@@ -80,6 +85,15 @@ export default {
}
return '';
},
+ downloadPath() {
+ const data = JSON.stringify(this.requests);
+ const blob = new Blob([data], { type: 'text/plain' });
+ return window.URL.createObjectURL(blob);
+ },
+ downloadName() {
+ const fileName = this.requests[0].truncatedUrl;
+ return `${fileName}_perf_bar_${Date.now()}.json`;
+ },
},
mounted() {
this.currentRequest = this.requestId;
@@ -121,6 +135,9 @@ export default {
<a :href="currentRequest.details.tracing.tracing_url">{{ s__('PerformanceBar|trace') }}</a>
</div>
<add-request v-on="$listeners" />
+ <div v-if="currentRequest.details" id="peek-download" class="view">
+ <a :download="downloadName" :href="downloadPath">{{ s__('PerformanceBar|Download') }}</a>
+ </div>
<request-selector
v-if="currentRequest"
:current-request="currentRequest"
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index 793aba3189b..1610534ae0d 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -1,7 +1,7 @@
<script>
+import { GlPopover } from '@gitlab/ui';
import { glEmojiTag } from '~/emoji';
import { n__ } from '~/locale';
-import { GlPopover } from '@gitlab/ui';
export default {
components: {
@@ -40,16 +40,6 @@ export default {
},
},
methods: {
- truncatedUrl(requestUrl) {
- const components = requestUrl.replace(/\/$/, '').split('/');
- let truncated = components[components.length - 1];
-
- if (truncated.match(/^\d+$/)) {
- truncated = `${components[components.length - 2]}/${truncated}`;
- }
-
- return truncated;
- },
glEmojiTag,
},
};
@@ -63,7 +53,7 @@ export default {
:value="request.id"
class="qa-performance-bar-request"
>
- {{ truncatedUrl(request.url) }}
+ {{ request.truncatedUrl }}
<span v-if="request.hasWarnings">(!)</span>
</option>
</select>
diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue
index 0da3c271214..0128d5bd733 100644
--- a/app/assets/javascripts/performance_bar/components/request_warning.vue
+++ b/app/assets/javascripts/performance_bar/components/request_warning.vue
@@ -1,6 +1,6 @@
<script>
-import { glEmojiTag } from '~/emoji';
import { GlPopover } from '@gitlab/ui';
+import { glEmojiTag } from '~/emoji';
export default {
components: {
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index 735c9d804ee..7b373a8ce22 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -1,3 +1,4 @@
+/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
@@ -53,12 +54,57 @@ export default ({ container }) =>
PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId)
.then(res => {
this.store.addRequestDetails(requestId, res.data);
+
+ if (this.requestId === requestId) this.collectFrontendPerformanceMetrics();
})
.catch(() =>
// eslint-disable-next-line no-console
console.warn(`Error getting performance bar results for ${requestId}`),
);
},
+ collectFrontendPerformanceMetrics() {
+ if (performance) {
+ const navigationEntries = performance.getEntriesByType('navigation');
+ const paintEntries = performance.getEntriesByType('paint');
+ const resourceEntries = performance.getEntriesByType('resource');
+
+ let durationString = '';
+ if (navigationEntries.length > 0) {
+ durationString = `${Math.round(navigationEntries[0].responseEnd)} | `;
+ durationString += `${Math.round(paintEntries[1].startTime)} | `;
+ durationString += ` ${Math.round(navigationEntries[0].domContentLoadedEventEnd)}`;
+ }
+
+ let newEntries = resourceEntries.map(this.transformResourceEntry);
+
+ this.updateFrontendPerformanceMetrics(durationString, newEntries);
+
+ if ('PerformanceObserver' in window) {
+ // We start observing for more incoming timings
+ const observer = new PerformanceObserver(list => {
+ newEntries = newEntries.concat(list.getEntries().map(this.transformResourceEntry));
+ this.updateFrontendPerformanceMetrics(durationString, newEntries);
+ });
+
+ observer.observe({ entryTypes: ['resource'] });
+ }
+ }
+ },
+ updateFrontendPerformanceMetrics(durationString, requestEntries) {
+ this.store.setRequestDetailsData(this.requestId, 'total', {
+ duration: durationString,
+ calls: requestEntries.length,
+ details: requestEntries,
+ });
+ },
+ transformResourceEntry(entry) {
+ const nf = new Intl.NumberFormat();
+ return {
+ name: entry.name.replace(document.location.origin, ''),
+ duration: Math.round(entry.duration),
+ size: entry.transferSize ? `${nf.format(entry.transferSize)} bytes` : 'cached',
+ };
+ },
},
render(createElement) {
return createElement('performance-bar-app', {
diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
index 64f4f5e0c76..6f443db47ed 100644
--- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
+++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
@@ -5,9 +5,12 @@ export default class PerformanceBarStore {
addRequest(requestId, requestUrl) {
if (!this.findRequest(requestId)) {
+ const shortUrl = PerformanceBarStore.truncateUrl(requestUrl);
+
this.requests.push({
id: requestId,
url: requestUrl,
+ truncatedUrl: shortUrl,
details: {},
hasWarnings: false,
});
@@ -29,6 +32,16 @@ export default class PerformanceBarStore {
return request;
}
+ setRequestDetailsData(requestId, metricKey, requestDetailsData) {
+ const selectedRequest = this.findRequest(requestId);
+ if (selectedRequest) {
+ selectedRequest.details = {
+ ...selectedRequest.details,
+ [metricKey]: requestDetailsData,
+ };
+ }
+ }
+
requestsWithDetails() {
return this.requests.filter(request => request.details);
}
@@ -36,4 +49,20 @@ export default class PerformanceBarStore {
canTrackRequest(requestUrl) {
return this.requests.filter(request => request.url === requestUrl).length < 2;
}
+
+ static truncateUrl(requestUrl) {
+ const [rootAndQuery] = requestUrl.split('#');
+ const [root, query] = rootAndQuery.split('?');
+ const components = root.replace(/\/$/, '').split('/');
+
+ let truncated = components[components.length - 1];
+ if (truncated.match(/^\d+$/)) {
+ truncated = `${components[components.length - 2]}/${truncated}`;
+ }
+ if (query) {
+ truncated += `?${query}`;
+ }
+
+ return truncated;
+ }
}
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index 8d6a3781048..4598626718c 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -6,8 +6,8 @@ import Flash from './flash';
const DEFERRED_LINK_CLASS = 'deferred-link';
export default class PersistentUserCallout {
- constructor(container) {
- const { dismissEndpoint, featureId, deferLinks } = container.dataset;
+ constructor(container, options = container.dataset) {
+ const { dismissEndpoint, featureId, deferLinks } = options;
this.container = container;
this.dismissEndpoint = dismissEndpoint;
this.featureId = featureId;
@@ -53,11 +53,11 @@ export default class PersistentUserCallout {
});
}
- static factory(container) {
+ static factory(container, options) {
if (!container) {
return undefined;
}
- return new PersistentUserCallout(container);
+ return new PersistentUserCallout(container, options);
}
}
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index e29509ce074..429122c8083 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -100,9 +100,6 @@ export default {
hasOnlyOneJob(stage) {
return stage.groups.length === 1;
},
- hasDownstream(index, length) {
- return index === length - 1 && this.hasTriggered;
- },
hasUpstream(index) {
return index === 0 && this.hasTriggeredBy;
},
@@ -160,7 +157,6 @@ export default {
:key="stage.name"
:class="{
'has-upstream prepend-left-64': hasUpstream(index),
- 'has-downstream': hasDownstream(index, graph.length),
'has-only-one-job': hasOnlyOneJob(stage),
'append-right-46': shouldAddRightMargin(index),
}"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 0d5afe04e8e..bfd314e0439 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import ActionComponent from './action_component.vue';
import JobNameComponent from './job_name_component.vue';
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { sprintf } from '~/locale';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
@@ -111,7 +111,7 @@ export default {
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
- class="js-pipeline-graph-job-link qa-job-link"
+ class="js-pipeline-graph-job-link qa-job-link menu-item"
>
<job-name-component :name="job.name" :status="job.status" />
</gl-link>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index 4e7d77863b9..82335e71403 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -42,7 +42,6 @@ export default {
<template>
<li class="linked-pipeline build">
- <div class="curve"></div>
<gl-button
:id="buttonId"
v-gl-tooltip
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index 6efdde2b17e..998519f9df1 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -1,5 +1,6 @@
<script>
import LinkedPipeline from './linked_pipeline.vue';
+import { __ } from '~/locale';
export default {
components: {
@@ -27,6 +28,9 @@ export default {
};
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
},
+ isUpstream() {
+ return this.columnTitle === __('Upstream');
+ },
},
};
</script>
@@ -34,13 +38,12 @@ export default {
<template>
<div :class="columnClass" class="stage-column linked-pipelines-column">
<div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
- <div class="cross-project-triangle"></div>
+ <div v-if="isUpstream" class="cross-project-triangle"></div>
<ul>
<linked-pipeline
v-for="(pipeline, index) in linkedPipelines"
:key="pipeline.id"
:class="{
- 'flat-connector-before': index === 0 && graphPosition === 'right',
active: pipeline.isExpanded,
'left-connector': pipeline.isExpanded && graphPosition === 'left',
}"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipeline_stop_modal.vue
index 2e71b3c637b..7c4e651373f 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_stop_modal.vue
@@ -1,7 +1,7 @@
<script>
import _ from 'underscore';
-import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { GlLink } from '@gitlab/ui';
+import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { s__, sprintf } from '~/locale';
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
index dce8b020d6f..1bac7ce9ac5 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
@@ -28,7 +28,9 @@ export default {
return this.report.name || __('Summary');
},
successPercentage() {
- return Math.round((this.report.success_count / this.report.total_count) * 100) || 0;
+ // Returns a full number when the decimals equal .00.
+ // Otherwise returns a float to two decimal points
+ return Number(((this.report.success_count / this.report.total_count) * 100 || 0).toFixed(2));
},
formattedDuration() {
return formatTime(secondsToMilliseconds(this.report.total_time));
diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue
index 2ed0c24825c..2a23a0f6744 100644
--- a/app/assets/javascripts/pipelines/components/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/time_ago.vue
@@ -31,7 +31,7 @@ export default {
hasFinishedTime() {
return this.finishedTime !== '';
},
- durationFormated() {
+ durationFormatted() {
const date = new Date(this.duration * 1000);
let hh = date.getUTCHours();
@@ -59,7 +59,7 @@ export default {
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Duration') }}</div>
<div class="table-mobile-content">
<p v-if="hasDuration" class="duration">
- <span v-html="iconTimerSvg"> </span> {{ durationFormated }}
+ <span v-html="iconTimerSvg"> </span> {{ durationFormatted }}
</p>
<p v-if="hasFinishedTime" class="finished-at d-none d-sm-none d-md-block">
@@ -71,7 +71,7 @@ export default {
data-placement="top"
data-container="body"
>
- {{ timeFormated(finishedTime) }}
+ {{ timeFormatted(finishedTime) }}
</time>
</p>
</div>
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
index 95466587d6b..16fa6935cbe 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
@@ -1,7 +1,7 @@
import { TestStatus } from '~/pipelines/constants';
import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
-function iconForTestStatus(status) {
+export function iconForTestStatus(status) {
switch (status) {
case 'success':
return 'status_success_borderless';
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 031c54d2336..d6cdd37a2c3 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -2,10 +2,11 @@
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import sanitize from 'sanitize-html';
import axios from '~/lib/utils/axios_utils';
+import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import { __ } from '~/locale';
-import sanitize from 'sanitize-html';
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
const highlighter = function(element, text, matches) {
@@ -116,7 +117,7 @@ export default class ProjectFindFile {
if (searchText) {
matches = fuzzaldrinPlus.match(filePath, searchText);
}
- const blobItemUrl = `${this.options.blobUrlTemplate}/${encodeURIComponent(filePath)}`;
+ const blobItemUrl = joinPaths(this.options.blobUrlTemplate, escapeFileUrl(filePath));
const html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find('.tree-table > tbody').append(html));
}
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 2429da9c061..92c4c05bd87 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -128,15 +128,15 @@ const bindEvents = () => {
},
iosswift: {
text: s__('ProjectTemplates|iOS (Swift)'),
- icon: '.template-option svg.icon-gitlab',
+ icon: '.template-option .icon-iosswift',
},
dotnetcore: {
text: s__('ProjectTemplates|.NET Core'),
- icon: '.template-option .icon-dotnet',
+ icon: '.template-option .icon-dotnetcore',
},
android: {
text: s__('ProjectTemplates|Android'),
- icon: '.template-option svg.icon-android',
+ icon: '.template-option .icon-android',
},
gomicro: {
text: s__('ProjectTemplates|Go Micro'),
@@ -164,23 +164,27 @@ const bindEvents = () => {
},
nfhugo: {
text: s__('ProjectTemplates|Netlify/Hugo'),
- icon: '.template-option .icon-netlify',
+ icon: '.template-option .icon-nfhugo',
},
nfjekyll: {
text: s__('ProjectTemplates|Netlify/Jekyll'),
- icon: '.template-option .icon-netlify',
+ icon: '.template-option .icon-nfjekyll',
},
nfplainhtml: {
text: s__('ProjectTemplates|Netlify/Plain HTML'),
- icon: '.template-option .icon-netlify',
+ icon: '.template-option .icon-nfplainhtml',
},
nfgitbook: {
text: s__('ProjectTemplates|Netlify/GitBook'),
- icon: '.template-option .icon-netlify',
+ icon: '.template-option .icon-nfgitbook',
},
nfhexo: {
text: s__('ProjectTemplates|Netlify/Hexo'),
- icon: '.template-option .icon-netlify',
+ icon: '.template-option .icon-nfhexo',
+ },
+ salesforcedx: {
+ text: s__('ProjectTemplates|SalesforceDX'),
+ icon: '.template-option .icon-salesforcedx',
},
serverless_framework: {
text: s__('ProjectTemplates|Serverless Framework/JS'),
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index 60fd3ed5ea7..f1106dc6ae9 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -1,11 +1,11 @@
<script>
import Visibility from 'visibilityjs';
+import { GlLoadingIcon } from '@gitlab/ui';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import Poll from '~/lib/utils/poll';
import Flash from '~/flash';
import { __, s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
-import { GlLoadingIcon } from '@gitlab/ui';
import CommitPipelineService from '../services/commit_pipeline_service';
export default {
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/list/components/app.vue
index 11b2c3b7016..c555c2b04d1 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/list/components/app.vue
@@ -5,7 +5,7 @@ import store from '../stores';
import CollapsibleContainer from './collapsible_container.vue';
import ProjectEmptyState from './project_empty_state.vue';
import GroupEmptyState from './group_empty_state.vue';
-import { s__, sprintf } from '../../locale';
+import { s__, sprintf } from '~/locale';
export default {
name: 'RegistryListApp',
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/list/components/collapsible_container.vue
index 5a6f9370564..86bb2d8092e 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/list/components/collapsible_container.vue
@@ -31,7 +31,7 @@ export default {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
- mixins: [Tracking.mixin({})],
+ mixins: [Tracking.mixin()],
props: {
repo: {
type: Object,
@@ -43,7 +43,6 @@ export default {
isOpen: false,
modalId: `confirm-repo-deletion-modal-${this.repo.id}`,
tracking: {
- category: document.body.dataset.page,
label: 'registry_repository_delete',
},
};
@@ -67,7 +66,7 @@ export default {
}
},
handleDeleteRepository() {
- this.track('confirm_delete', {});
+ this.track('confirm_delete');
return this.deleteItem(this.repo)
.then(() => {
createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
@@ -103,7 +102,7 @@ export default {
:aria-label="s__('ContainerRegistry|Remove repository')"
class="js-remove-repo btn-inverted"
variant="danger"
- @click="track('click_button', {})"
+ @click="track('click_button')"
>
<icon name="remove" />
</gl-button>
@@ -132,7 +131,7 @@ export default {
:modal-id="modalId"
ok-variant="danger"
@ok="handleDeleteRepository"
- @cancel="track('cancel_delete', {})"
+ @cancel="track('cancel_delete')"
>
<template v-slot:modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
<p
diff --git a/app/assets/javascripts/registry/components/group_empty_state.vue b/app/assets/javascripts/registry/list/components/group_empty_state.vue
index 7885fd2146d..7885fd2146d 100644
--- a/app/assets/javascripts/registry/components/group_empty_state.vue
+++ b/app/assets/javascripts/registry/list/components/group_empty_state.vue
diff --git a/app/assets/javascripts/registry/components/project_empty_state.vue b/app/assets/javascripts/registry/list/components/project_empty_state.vue
index 80ef31004c8..80ef31004c8 100644
--- a/app/assets/javascripts/registry/components/project_empty_state.vue
+++ b/app/assets/javascripts/registry/list/components/project_empty_state.vue
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/list/components/table_registry.vue
index caa5fd4ff4e..4e14db7f578 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/list/components/table_registry.vue
@@ -23,7 +23,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [timeagoMixin],
+ mixins: [timeagoMixin, Tracking.mixin()],
props: {
repo: {
type: Object,
@@ -71,9 +71,6 @@ export default {
},
methods: {
...mapActions(['fetchList', 'deleteItem', 'multiDeleteItems']),
- track(action) {
- Tracking.event(document.body.dataset.page, action, this.tracking);
- },
setModalDescription(itemIndex = -1) {
if (itemIndex === -1) {
this.modalDescription = sprintf(
@@ -196,7 +193,7 @@ export default {
/>
</th>
<th>{{ s__('ContainerRegistry|Tag') }}</th>
- <th>{{ s__('ContainerRegistry|Tag ID') }}</th>
+ <th ref="imageId">{{ s__('ContainerRegistry|Image ID') }}</th>
<th>{{ s__('ContainerRegistry|Size') }}</th>
<th>{{ s__('ContainerRegistry|Last Updated') }}</th>
<th>
@@ -250,7 +247,7 @@ export default {
<td>
<span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">{{
- timeFormated(item.createdAt)
+ timeFormatted(item.createdAt)
}}</span>
</td>
diff --git a/app/assets/javascripts/registry/constants.js b/app/assets/javascripts/registry/list/constants.js
index db798fb88ac..e55ea9cc9d9 100644
--- a/app/assets/javascripts/registry/constants.js
+++ b/app/assets/javascripts/registry/list/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '../locale';
+import { __ } from '~/locale';
export const FETCH_REGISTRY_ERROR_MESSAGE = __(
'Something went wrong while fetching the registry list.',
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/list/index.js
index 18fd360f586..3d0ff327b42 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/list/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import registryApp from './components/app.vue';
-import Translate from '../vue_shared/translate';
+import Translate from '~/vue_shared/translate';
Vue.use(Translate);
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/list/stores/actions.js
index 6afba618486..6afba618486 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/list/stores/actions.js
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/list/stores/getters.js
index ac90bde1b2a..ac90bde1b2a 100644
--- a/app/assets/javascripts/registry/stores/getters.js
+++ b/app/assets/javascripts/registry/list/stores/getters.js
diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/list/stores/index.js
index 1bb06bd6e81..1bb06bd6e81 100644
--- a/app/assets/javascripts/registry/stores/index.js
+++ b/app/assets/javascripts/registry/list/stores/index.js
diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/list/stores/mutation_types.js
index 6740bfede1a..6740bfede1a 100644
--- a/app/assets/javascripts/registry/stores/mutation_types.js
+++ b/app/assets/javascripts/registry/list/stores/mutation_types.js
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/list/stores/mutations.js
index 419de848883..419de848883 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/list/stores/mutations.js
diff --git a/app/assets/javascripts/registry/stores/state.js b/app/assets/javascripts/registry/list/stores/state.js
index 724c64b4994..724c64b4994 100644
--- a/app/assets/javascripts/registry/stores/state.js
+++ b/app/assets/javascripts/registry/list/stores/state.js
diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
new file mode 100644
index 00000000000..b2c700b817c
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
@@ -0,0 +1,43 @@
+<script>
+import { mapState } from 'vuex';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ components: {},
+ computed: {
+ ...mapState({
+ helpPagePath: 'helpPagePath',
+ }),
+
+ helpText() {
+ return sprintf(
+ s__(
+ 'PackageRegistry|Read more about the %{helpLinkStart}Container Registry tag retention policies%{helpLinkEnd}',
+ ),
+ {
+ helpLinkStart: `<a href="${this.helpPagePath}" target="_blank">`,
+ helpLinkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <p>
+ {{ s__('PackageRegistry|Tag retention policies are designed to:') }}
+ </p>
+ <ul>
+ <li>{{ s__('PackageRegistry|Keep and protect the images that matter most.') }}</li>
+ <li>
+ {{
+ s__("PackageRegistry|Automatically remove extra images that aren't designed to be kept.")
+ }}
+ </li>
+ </ul>
+ <p ref="help-link" v-html="helpText"></p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
new file mode 100644
index 00000000000..2938178ea86
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import store from './stores/';
+import RegistrySettingsApp from './components/registry_settings_app.vue';
+
+Vue.use(Translate);
+
+export default () => {
+ const el = document.getElementById('js-registry-settings');
+ if (!el) {
+ return null;
+ }
+ store.dispatch('setInitialState', el.dataset);
+ return new Vue({
+ el,
+ store,
+ components: {
+ RegistrySettingsApp,
+ },
+ render(createElement) {
+ return createElement('registry-settings-app', {});
+ },
+ });
+};
diff --git a/app/assets/javascripts/registry/settings/stores/actions.js b/app/assets/javascripts/registry/settings/stores/actions.js
new file mode 100644
index 00000000000..f2c469d4edb
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/stores/actions.js
@@ -0,0 +1,6 @@
+import * as types from './mutation_types';
+
+export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
+
+// to avoid eslint error until more actions are added to the store
+export default () => {};
diff --git a/app/assets/javascripts/registry/settings/stores/index.js b/app/assets/javascripts/registry/settings/stores/index.js
new file mode 100644
index 00000000000..91a35aac149
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/stores/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ state,
+ actions,
+ mutations,
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/registry/settings/stores/mutation_types.js b/app/assets/javascripts/registry/settings/stores/mutation_types.js
new file mode 100644
index 00000000000..8a0f519eabd
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/stores/mutation_types.js
@@ -0,0 +1,4 @@
+export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
+
+// to avoid eslint error until more actions are added to the store
+export default () => {};
diff --git a/app/assets/javascripts/registry/settings/stores/mutations.js b/app/assets/javascripts/registry/settings/stores/mutations.js
new file mode 100644
index 00000000000..4f32e11ed52
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/stores/mutations.js
@@ -0,0 +1,8 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_INITIAL_STATE](state, initialState) {
+ state.helpPagePath = initialState.helpPagePath;
+ state.registrySettingsEndpoint = initialState.registrySettingsEndpoint;
+ },
+};
diff --git a/app/assets/javascripts/registry/settings/stores/state.js b/app/assets/javascripts/registry/settings/stores/state.js
new file mode 100644
index 00000000000..4c0439458b6
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/stores/state.js
@@ -0,0 +1,10 @@
+export default () => ({
+ /*
+ * Help page path to generate the link
+ */
+ helpPagePath: '',
+ /*
+ * Settings endpoint to call to fetch and update the settings
+ */
+ registrySettingsEndpoint: '',
+});
diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
index f0112a5a623..dc7c9d9f174 100644
--- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
@@ -72,7 +72,7 @@ export default {
{{ __('Related merge requests') }}
</span>
<div v-if="totalCount" class="d-inline-flex lh-100 align-middle">
- <div class="mr-count-badge">
+ <div class="mr-count-badge border-width-1px border-style-solid border-color-default">
<div class="mr-count-badge-count">
<svg class="s16 mr-1 text-secondary">
<icon name="merge-request" class="mr-1 text-secondary" />
diff --git a/app/assets/javascripts/releases/list/components/app.vue b/app/assets/javascripts/releases/list/components/app.vue
index 5a06c4fec58..a414b3ccd4e 100644
--- a/app/assets/javascripts/releases/list/components/app.vue
+++ b/app/assets/javascripts/releases/list/components/app.vue
@@ -1,6 +1,12 @@
<script>
import { mapState, mapActions } from 'vuex';
import { GlSkeletonLoading, GlEmptyState } from '@gitlab/ui';
+import {
+ getParameterByName,
+ historyPushState,
+ buildUrlWithCurrentLocation,
+} from '~/lib/utils/common_utils';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import ReleaseBlock from './release_block.vue';
export default {
@@ -9,6 +15,7 @@ export default {
GlSkeletonLoading,
GlEmptyState,
ReleaseBlock,
+ TablePagination,
},
props: {
projectId: {
@@ -25,7 +32,7 @@ export default {
},
},
computed: {
- ...mapState(['isLoading', 'releases', 'hasError']),
+ ...mapState(['isLoading', 'releases', 'hasError', 'pageInfo']),
shouldRenderEmptyState() {
return !this.releases.length && !this.hasError && !this.isLoading;
},
@@ -34,10 +41,17 @@ export default {
},
},
created() {
- this.fetchReleases(this.projectId);
+ this.fetchReleases({
+ page: getParameterByName('page'),
+ projectId: this.projectId,
+ });
},
methods: {
...mapActions(['fetchReleases']),
+ onChangePage(page) {
+ historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
+ this.fetchReleases({ page, projectId: this.projectId });
+ },
},
};
</script>
@@ -67,6 +81,8 @@ export default {
:class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
/>
</div>
+
+ <table-pagination v-if="!isLoading" :change="onChangePage" :page-info="pageInfo" />
</div>
</template>
<style>
diff --git a/app/assets/javascripts/releases/list/components/evidence_block.vue b/app/assets/javascripts/releases/list/components/evidence_block.vue
new file mode 100644
index 00000000000..d9abd195fee
--- /dev/null
+++ b/app/assets/javascripts/releases/list/components/evidence_block.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import { truncateSha } from '~/lib/utils/text_utility';
+import Icon from '~/vue_shared/components/icon.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ExpandButton from '~/vue_shared/components/expand_button.vue';
+
+export default {
+ name: 'EvidenceBlock',
+ components: {
+ ClipboardButton,
+ ExpandButton,
+ GlLink,
+ Icon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ release: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ evidenceTitle() {
+ return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tag_name });
+ },
+ evidenceUrl() {
+ return this.release.assets && this.release.assets.evidence_file_path;
+ },
+ shortSha() {
+ return truncateSha(this.sha);
+ },
+ sha() {
+ return this.release.evidence_sha;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="card-text prepend-top-default">
+ <b>
+ {{ __('Evidence collection') }}
+ </b>
+ </div>
+ <div class="d-flex align-items-baseline">
+ <gl-link
+ v-gl-tooltip
+ class="monospace"
+ :title="__('Download evidence JSON')"
+ :download="evidenceTitle"
+ :href="evidenceUrl"
+ >
+ <icon name="review-list" class="align-top append-right-4" /><span>{{ evidenceTitle }}</span>
+ </gl-link>
+
+ <expand-button>
+ <template slot="short">
+ <span class="js-short monospace">{{ shortSha }}</span>
+ </template>
+ <template slot="expanded">
+ <span class="js-expanded monospace gl-pl-1">{{ sha }}</span>
+ </template>
+ </expand-button>
+ <clipboard-button
+ :title="__('Copy commit SHA')"
+ :text="sha"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/list/components/release_block.vue b/app/assets/javascripts/releases/list/components/release_block.vue
index 2b6aa6aeff9..4d8d8682401 100644
--- a/app/assets/javascripts/releases/list/components/release_block.vue
+++ b/app/assets/javascripts/releases/list/components/release_block.vue
@@ -11,16 +11,20 @@ import { getLocationHash } from '~/lib/utils/url_utility';
import { scrollToElement } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReleaseBlockFooter from './release_block_footer.vue';
+import EvidenceBlock from './evidence_block.vue';
+import ReleaseBlockMilestoneInfo from './release_block_milestone_info.vue';
export default {
name: 'ReleaseBlock',
components: {
+ EvidenceBlock,
GlLink,
GlBadge,
GlButton,
Icon,
UserAvatarLink,
ReleaseBlockFooter,
+ ReleaseBlockMilestoneInfo,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -44,7 +48,7 @@ export default {
},
releasedTimeAgo() {
return sprintf(__('released %{time}'), {
- time: this.timeFormated(this.release.released_at),
+ time: this.timeFormatted(this.release.released_at),
});
},
userImageAltDescription() {
@@ -70,6 +74,9 @@ export default {
hasAuthor() {
return !_.isEmpty(this.author);
},
+ hasEvidence() {
+ return Boolean(this.release.evidence_sha);
+ },
shouldRenderMilestones() {
return !_.isEmpty(this.release.milestones);
},
@@ -77,13 +84,20 @@ export default {
return n__('Milestone', 'Milestones', this.release.milestones.length);
},
shouldShowEditButton() {
- return Boolean(
- this.glFeatures.releaseEditPage && this.release._links && this.release._links.edit_url,
- );
+ return Boolean(this.release._links && this.release._links.edit_url);
+ },
+ shouldShowEvidence() {
+ return this.glFeatures.releaseEvidenceCollection;
},
shouldShowFooter() {
return this.glFeatures.releaseIssueSummary;
},
+ shouldRenderReleaseMetaData() {
+ return !this.glFeatures.releaseIssueSummary;
+ },
+ shouldRenderMilestoneInfo() {
+ return Boolean(this.glFeatures.releaseIssueSummary && !_.isEmpty(this.release.milestones));
+ },
},
mounted() {
const hash = getLocationHash();
@@ -100,26 +114,30 @@ export default {
</script>
<template>
<div :id="id" :class="{ 'bg-line-target-blue': isHighlighted }" class="card release-block">
+ <div class="card-header d-flex align-items-center bg-white pr-0">
+ <h2 class="card-title my-2 mr-auto gl-font-size-20">
+ {{ release.name }}
+ <gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
+ __('Upcoming Release')
+ }}</gl-badge>
+ </h2>
+ <gl-link
+ v-if="shouldShowEditButton"
+ v-gl-tooltip
+ class="btn btn-default append-right-10 js-edit-button ml-2"
+ :title="__('Edit this release')"
+ :href="release._links.edit_url"
+ >
+ <icon name="pencil" />
+ </gl-link>
+ </div>
<div class="card-body">
- <div class="d-flex align-items-start">
- <h2 class="card-title mt-0 mr-auto">
- {{ release.name }}
- <gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
- __('Upcoming Release')
- }}</gl-badge>
- </h2>
- <gl-link
- v-if="shouldShowEditButton"
- v-gl-tooltip
- class="btn btn-default js-edit-button ml-2"
- :title="__('Edit this release')"
- :href="release._links.edit_url"
- >
- <icon name="pencil" />
- </gl-link>
+ <div v-if="shouldRenderMilestoneInfo">
+ <release-block-milestone-info :milestones="release.milestones" />
+ <hr class="mb-3 mt-0" />
</div>
- <div class="card-subtitle d-flex flex-wrap text-secondary">
+ <div v-if="shouldRenderReleaseMetaData" class="card-subtitle d-flex flex-wrap text-secondary">
<div class="append-right-8">
<icon name="commit" class="align-middle" />
<gl-link v-if="commitUrl" v-gl-tooltip.bottom :title="commit.title" :href="commitUrl">
@@ -217,6 +235,8 @@ export default {
</div>
</div>
+ <evidence-block v-if="hasEvidence && shouldShowEvidence" :release="release" />
+
<div class="card-text prepend-top-default">
<div v-html="release.description_html"></div>
</div>
diff --git a/app/assets/javascripts/releases/list/components/release_block_footer.vue b/app/assets/javascripts/releases/list/components/release_block_footer.vue
index 5659f0e530b..8533fc17ffd 100644
--- a/app/assets/javascripts/releases/list/components/release_block_footer.vue
+++ b/app/assets/javascripts/releases/list/components/release_block_footer.vue
@@ -1,6 +1,6 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { __, sprintf } from '~/locale';
@@ -50,7 +50,7 @@ export default {
},
computed: {
releasedAtTimeAgo() {
- return this.timeFormated(this.releasedAt);
+ return this.timeFormatted(this.releasedAt);
},
userImageAltDescription() {
return this.author && this.author.username
diff --git a/app/assets/javascripts/releases/list/components/release_block_milestone_info.vue b/app/assets/javascripts/releases/list/components/release_block_milestone_info.vue
new file mode 100644
index 00000000000..d3e354d6157
--- /dev/null
+++ b/app/assets/javascripts/releases/list/components/release_block_milestone_info.vue
@@ -0,0 +1,136 @@
+<script>
+import { GlProgressBar, GlLink, GlBadge, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { __, n__, sprintf } from '~/locale';
+import { MAX_MILESTONES_TO_DISPLAY } from '../constants';
+
+/** Sums the values of an array. For use with Array.reduce. */
+const sumReducer = (acc, curr) => acc + curr;
+
+export default {
+ name: 'ReleaseBlockMilestoneInfo',
+ components: {
+ GlProgressBar,
+ GlLink,
+ GlBadge,
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ milestones: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showAllMilestones: false,
+ };
+ },
+ computed: {
+ percentCompleteText() {
+ return sprintf(__('%{percent}%{percentSymbol} complete'), {
+ percent: this.percentComplete,
+ percentSymbol: '%',
+ });
+ },
+ percentComplete() {
+ const percent = Math.round((this.closedIssuesCount / this.totalIssuesCount) * 100);
+ return Number.isNaN(percent) ? 0 : percent;
+ },
+ allIssueStats() {
+ return this.milestones.map(m => m.issue_stats || {});
+ },
+ openIssuesCount() {
+ return this.allIssueStats.map(stats => stats.opened || 0).reduce(sumReducer);
+ },
+ closedIssuesCount() {
+ return this.allIssueStats.map(stats => stats.closed || 0).reduce(sumReducer);
+ },
+ totalIssuesCount() {
+ return this.openIssuesCount + this.closedIssuesCount;
+ },
+ milestoneLabelText() {
+ return n__('Milestone', 'Milestones', this.milestones.length);
+ },
+ issueCountsText() {
+ return sprintf(__('Open: %{open} • Closed: %{closed}'), {
+ open: this.openIssuesCount,
+ closed: this.closedIssuesCount,
+ });
+ },
+ milestonesToDisplay() {
+ return this.showAllMilestones
+ ? this.milestones
+ : this.milestones.slice(0, MAX_MILESTONES_TO_DISPLAY);
+ },
+ showMoreLink() {
+ return this.milestones.length > MAX_MILESTONES_TO_DISPLAY;
+ },
+ moreText() {
+ return this.showAllMilestones
+ ? __('show fewer')
+ : sprintf(__('show %{count} more'), {
+ count: this.milestones.length - MAX_MILESTONES_TO_DISPLAY,
+ });
+ },
+ },
+ methods: {
+ toggleShowAll() {
+ this.showAllMilestones = !this.showAllMilestones;
+ },
+ shouldRenderBullet(milestoneIndex) {
+ return Boolean(milestoneIndex !== this.milestonesToDisplay.length - 1 || this.showMoreLink);
+ },
+ shouldRenderShowMoreLink(milestoneIndex) {
+ return Boolean(milestoneIndex === this.milestonesToDisplay.length - 1 && this.showMoreLink);
+ },
+ },
+};
+</script>
+<template>
+ <div class="release-block-milestone-info d-flex align-items-start flex-wrap">
+ <div
+ v-gl-tooltip
+ class="milestone-progress-bar-container js-milestone-progress-bar-container d-flex flex-column align-items-start flex-shrink-1 mr-4 mb-3"
+ :title="__('Closed issues')"
+ >
+ <span class="mb-2">{{ percentCompleteText }}</span>
+ <span class="w-100">
+ <gl-progress-bar :value="closedIssuesCount" :max="totalIssuesCount" variant="success" />
+ </span>
+ </div>
+ <div class="d-flex flex-column align-items-start mr-4 mb-3 js-milestone-list-container">
+ <span class="mb-1">{{ milestoneLabelText }}</span>
+ <div class="d-flex flex-wrap align-items-end">
+ <template v-for="(milestone, index) in milestonesToDisplay">
+ <gl-link
+ :key="milestone.id"
+ v-gl-tooltip
+ :title="milestone.description"
+ :href="milestone.web_url"
+ class="append-right-4"
+ >
+ {{ milestone.title }}
+ </gl-link>
+ <template v-if="shouldRenderBullet(index)">
+ <span :key="'bullet-' + milestone.id" class="append-right-4">&bull;</span>
+ </template>
+ <template v-if="shouldRenderShowMoreLink(index)">
+ <gl-button :key="'more-button-' + milestone.id" variant="link" @click="toggleShowAll">
+ {{ moreText }}
+ </gl-button>
+ </template>
+ </template>
+ </div>
+ </div>
+ <div class="d-flex flex-column align-items-start flex-shrink-0 mr-4 mb-3 js-issues-container">
+ <span class="mb-1">
+ {{ __('Issues') }}
+ <gl-badge pill variant="light" class="font-weight-bold">{{ totalIssuesCount }}</gl-badge>
+ </span>
+ {{ issueCountsText }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/list/constants.js b/app/assets/javascripts/releases/list/constants.js
new file mode 100644
index 00000000000..defcd917465
--- /dev/null
+++ b/app/assets/javascripts/releases/list/constants.js
@@ -0,0 +1,7 @@
+/* eslint-disable import/prefer-default-export */
+// This eslint-disable ^^^ can be removed when at least
+// one more constant is added to this file. Currently
+// constants.js files with only a single constant
+// are flagged by this rule.
+
+export const MAX_MILESTONES_TO_DISPLAY = 5;
diff --git a/app/assets/javascripts/releases/list/store/actions.js b/app/assets/javascripts/releases/list/store/actions.js
index e0a922d5ef6..b15fb69226f 100644
--- a/app/assets/javascripts/releases/list/store/actions.js
+++ b/app/assets/javascripts/releases/list/store/actions.js
@@ -2,6 +2,7 @@ import * as types from './mutation_types';
import createFlash from '~/flash';
import { __ } from '~/locale';
import api from '~/api';
+import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
/**
* Commits a mutation to update the state while the main endpoint is being requested.
@@ -16,17 +17,19 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
*
* @param {String} projectId
*/
-export const fetchReleases = ({ dispatch }, projectId) => {
+export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => {
dispatch('requestReleases');
api
- .releases(projectId)
- .then(({ data }) => dispatch('receiveReleasesSuccess', data))
+ .releases(projectId, { page })
+ .then(response => dispatch('receiveReleasesSuccess', response))
.catch(() => dispatch('receiveReleasesError'));
};
-export const receiveReleasesSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_RELEASES_SUCCESS, data);
+export const receiveReleasesSuccess = ({ commit }, { data, headers }) => {
+ const pageInfo = parseIntPagination(normalizeHeaders(headers));
+ commit(types.RECEIVE_RELEASES_SUCCESS, { data, pageInfo });
+};
export const receiveReleasesError = ({ commit }) => {
commit(types.RECEIVE_RELEASES_ERROR);
diff --git a/app/assets/javascripts/releases/list/store/mutations.js b/app/assets/javascripts/releases/list/store/mutations.js
index b97dc6cb0ab..99fc096264a 100644
--- a/app/assets/javascripts/releases/list/store/mutations.js
+++ b/app/assets/javascripts/releases/list/store/mutations.js
@@ -13,13 +13,15 @@ export default {
* Sets isLoading to false.
* Sets hasError to false.
* Sets the received data
+ * Sets the received pagination information
* @param {Object} state
- * @param {Object} data
+ * @param {Object} resp
*/
- [types.RECEIVE_RELEASES_SUCCESS](state, data) {
+ [types.RECEIVE_RELEASES_SUCCESS](state, { data, pageInfo }) {
state.hasError = false;
state.isLoading = false;
state.releases = data;
+ state.pageInfo = pageInfo;
},
/**
diff --git a/app/assets/javascripts/releases/list/store/state.js b/app/assets/javascripts/releases/list/store/state.js
index bf25e651c99..c251f56c9c5 100644
--- a/app/assets/javascripts/releases/list/store/state.js
+++ b/app/assets/javascripts/releases/list/store/state.js
@@ -2,4 +2,5 @@ export default () => ({
isLoading: false,
hasError: false,
releases: [],
+ pageInfo: {},
});
diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue
index 3c8a9e6ebef..51062cd7928 100644
--- a/app/assets/javascripts/reports/components/report_item.vue
+++ b/app/assets/javascripts/reports/components/report_item.vue
@@ -1,6 +1,6 @@
<script>
-import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { components, componentNames } from 'ee_else_ce/reports/components/issue_body';
+import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
export default {
name: 'ReportItem',
diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue
index aba798e63d0..1191e43d0d9 100644
--- a/app/assets/javascripts/reports/components/summary_row.vue
+++ b/app/assets/javascripts/reports/components/summary_row.vue
@@ -1,7 +1,7 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
/**
* Renders the summary row for each report
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index afb58a60155..f6b9ea5d30d 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -124,7 +124,7 @@ export default {
},
{
attrs: {
- href: this.newBlobPath,
+ href: `${this.newBlobPath}${this.currentPath}`,
class: 'qa-new-file-option',
},
text: __('New file'),
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index 7f974838359..6b3822151ff 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -35,11 +35,13 @@ export default {
<template>
<article class="file-holder limited-width-container readme-holder">
- <div class="file-title">
- <i aria-hidden="true" class="fa fa-file-text-o fa-fw"></i>
- <gl-link :href="blob.webUrl">
- <strong>{{ blob.name }}</strong>
- </gl-link>
+ <div class="js-file-title file-title-flex-parent">
+ <div class="file-header-content">
+ <i aria-hidden="true" class="fa fa-file-text-o fa-fw"></i>
+ <gl-link :href="blob.webUrl">
+ <strong>{{ blob.name }}</strong>
+ </gl-link>
+ </div>
</div>
<div class="blob-viewer">
<gl-loading-icon v-if="loading > 0" size="md" class="my-4 mx-auto" />
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index d826f209815..ae6409a0ac9 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -7,6 +7,7 @@ import TreeActionLink from './components/tree_action_link.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
import apolloProvider from './graphql';
import { setTitle } from './utils/title';
+import { updateFormAction } from './utils/dom';
import { parseBoolean } from '../lib/utils/common_utils';
import { webIDEUrl } from '../lib/utils/url_utility';
import { __ } from '../locale';
@@ -42,8 +43,15 @@ export default function setupVueRepositoryList() {
forkNewBlobPath,
forkNewDirectoryPath,
forkUploadBlobPath,
+ uploadPath,
+ newDirPath,
} = breadcrumbEl.dataset;
+ router.afterEach(({ params: { pathMatch = '/' } }) => {
+ updateFormAction('.js-upload-blob-form', uploadPath, pathMatch);
+ updateFormAction('.js-create-dir-form', newDirPath, pathMatch);
+ });
+
// eslint-disable-next-line no-new
new Vue({
el: breadcrumbEl,
diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js
index 5bf30e625a0..6498725adb6 100644
--- a/app/assets/javascripts/repository/log_tree.js
+++ b/app/assets/javascripts/repository/log_tree.js
@@ -1,5 +1,5 @@
-import axios from '~/lib/utils/axios_utils';
import { normalizeData } from 'ee_else_ce/repository/utils/commit';
+import axios from '~/lib/utils/axios_utils';
import getCommits from './queries/getCommits.query.graphql';
import getProjectPath from './queries/getProjectPath.query.graphql';
import getRef from './queries/getRef.query.graphql';
@@ -7,8 +7,8 @@ import getRef from './queries/getRef.query.graphql';
let fetchpromise;
let resolvers = [];
-export function resolveCommit(commits, { resolve, entry }) {
- const commit = commits.find(c => c.fileName === entry.name && c.type === entry.type);
+export function resolveCommit(commits, path, { resolve, entry }) {
+ const commit = commits.find(c => c.filePath === `${path}/${entry.name}` && c.type === entry.type);
if (commit) {
resolve(commit);
@@ -35,13 +35,13 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
.then(({ data, headers }) => {
const headerLogsOffset = headers['more-logs-offset'];
const { commits } = client.readQuery({ query: getCommits });
- const newCommitData = [...commits, ...normalizeData(data)];
+ const newCommitData = [...commits, ...normalizeData(data, path)];
client.writeQuery({
query: getCommits,
data: { commits: newCommitData },
});
- resolvers.forEach(r => resolveCommit(newCommitData, r));
+ resolvers.forEach(r => resolveCommit(newCommitData, path, r));
fetchpromise = null;
diff --git a/app/assets/javascripts/repository/utils/commit.js b/app/assets/javascripts/repository/utils/commit.js
index 6c204b57b37..3973798605d 100644
--- a/app/assets/javascripts/repository/utils/commit.js
+++ b/app/assets/javascripts/repository/utils/commit.js
@@ -1,11 +1,12 @@
// eslint-disable-next-line import/prefer-default-export
-export function normalizeData(data, extra = () => {}) {
+export function normalizeData(data, path, extra = () => {}) {
return data.map(d => ({
sha: d.commit.id,
message: d.commit.message,
committedDate: d.commit.committed_date,
commitPath: d.commit_path,
fileName: d.file_name,
+ filePath: `${path}/${d.file_name}`,
type: d.type,
__typename: 'LogTreeCommit',
...extra(d),
diff --git a/app/assets/javascripts/repository/utils/dom.js b/app/assets/javascripts/repository/utils/dom.js
index 963e6fc0bc4..81565a00d82 100644
--- a/app/assets/javascripts/repository/utils/dom.js
+++ b/app/assets/javascripts/repository/utils/dom.js
@@ -1,4 +1,11 @@
-// eslint-disable-next-line import/prefer-default-export
export const updateElementsVisibility = (selector, isVisible) => {
document.querySelectorAll(selector).forEach(elem => elem.classList.toggle('hidden', !isVisible));
};
+
+export const updateFormAction = (selector, basePath, path) => {
+ const form = document.querySelector(selector);
+
+ if (form) {
+ form.action = `${basePath}${path}`;
+ }
+};
diff --git a/app/assets/javascripts/serverless/components/area.vue b/app/assets/javascripts/serverless/components/area.vue
index a1a8cd3acbd..272c0bd5614 100644
--- a/app/assets/javascripts/serverless/components/area.vue
+++ b/app/assets/javascripts/serverless/components/area.vue
@@ -1,7 +1,7 @@
<script>
import { GlAreaChart } from '@gitlab/ui/dist/charts';
-import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
import dateFormat from 'dateformat';
+import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
import { X_INTERVAL } from '../constants';
import { validateGraphData } from '../utils';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index 9e66869515c..308bc4a2ddd 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -1,7 +1,7 @@
<script>
-import { sprintf, s__ } from '~/locale';
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
import FunctionRow from './function_row.vue';
import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
@@ -44,12 +44,14 @@ export default {
'Serverless|Your repository does not have a corresponding %{startTag}serverless.yml%{endTag} file.',
),
{ startTag: '<code>', endTag: '</code>' },
+ false,
);
},
noGitlabYamlConfigured() {
return sprintf(
s__('Serverless|Your %{startTag}.gitlab-ci.yml%{endTag} file is not properly configured.'),
{ startTag: '<code>', endTag: '</code>' },
+ false,
);
},
mismatchedServerlessFunctions() {
@@ -58,6 +60,7 @@ export default {
"Serverless|The functions listed in the %{startTag}serverless.yml%{endTag} file don't match the namespace of your cluster.",
),
{ startTag: '<code>', endTag: '</code>' },
+ false,
);
},
},
@@ -111,15 +114,9 @@ export default {
}}
</p>
<ul>
- <li>
- {{ noServerlessConfigFile }}
- </li>
- <li>
- {{ noGitlabYamlConfigured }}
- </li>
- <li>
- {{ mismatchedServerlessFunctions }}
- </li>
+ <li v-html="noServerlessConfigFile"></li>
+ <li v-html="noGitlabYamlConfigured"></li>
+ <li v-html="mismatchedServerlessFunctions"></li>
<li>{{ s__('Serverless|The deploy job has not finished.') }}</li>
</ul>
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index df950e79690..2757d64bd7d 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -1,11 +1,11 @@
<script>
import $ from 'jquery';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
+import { GlModal, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
-import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { __, s__ } from '~/locale';
import Api from '~/api';
-import { GlModal, GlTooltipDirective } from '@gitlab/ui';
import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
index 6633a63d046..9a60172db2e 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -1,7 +1,6 @@
<script>
-import { __, sprintf } from '~/locale';
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import { joinPaths } from '~/lib/utils/url_utility';
+import { __, sprintf } from '~/locale';
import AssigneeAvatar from './assignee_avatar.vue';
export default {
@@ -60,7 +59,7 @@ export default {
};
},
assigneeUrl() {
- return joinPaths(`${this.rootPath}`, `${this.user.username}`);
+ return this.user.web_url;
},
},
};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index b107e9789a7..f4dac38b9e1 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -32,14 +32,13 @@ export default {
};
</script>
<template>
- <div class="title hide-collapsed" data-qa-selector="assignee_title">
+ <div class="title hide-collapsed">
{{ assigneeTitle }}
<i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"></i>
<a
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
- data-qa-selector="assignee_edit_link"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
index 5b4a43399ca..7375855f899 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -1,6 +1,6 @@
<script>
-import { __, sprintf } from '~/locale';
import { GlTooltipDirective } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import CollapsedAssignee from './collapsed_assignee.vue';
const DEFAULT_MAX_COUNTER = 99;
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index 902aeb9b8e4..f88bde624b4 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import $ from 'jquery';
+import { __ } from '~/locale';
import eventHub from '../../event_hub';
export default {
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 4bfc8fa7eec..38b19d66163 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,8 +1,8 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
export default {
directives: {
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 0e489b28593..3b92ead8859 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -48,6 +48,12 @@ export default {
},
},
computed: {
+ tracking() {
+ return {
+ // eslint-disable-next-line no-underscore-dangle
+ category: this.$options._componentTag,
+ };
+ },
showLoadingState() {
return this.subscribed === null;
},
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index 06aca547183..4cb8d9ebd62 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -1,7 +1,7 @@
<script>
+import { GlProgressBar } from '@gitlab/ui';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip';
-import { GlProgressBar } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index 3d96405896d..3b7df369237 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -1,7 +1,7 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
-import { GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
@@ -59,6 +59,9 @@ export default {
collapsedButtonIcon() {
return this.isTodo ? 'todo-done' : 'todo-add';
},
+ collapsedButtonIconVisible() {
+ return this.collapsed && !this.isActionActive;
+ },
},
methods: {
handleButtonClick() {
@@ -82,8 +85,12 @@ export default {
data-boundary="viewport"
@click="handleButtonClick"
>
- <icon v-show="collapsed" :class="collapsedButtonIconClasses" :name="collapsedButtonIcon" />
- <span v-show="!collapsed" class="issuable-todo-inner"> {{ buttonLabel }} </span>
+ <icon
+ v-show="collapsedButtonIconVisible"
+ :class="collapsedButtonIconClasses"
+ :name="collapsedButtonIcon"
+ />
+ <span v-show="!collapsed" class="issuable-todo-inner">{{ buttonLabel }}</span>
<gl-loading-icon v-show="isActionActive" :inline="true" />
</button>
</template>
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 4a7000cbbda..ce869a625bf 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,7 +1,7 @@
+import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import Service from './services/sidebar_service';
-import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import { __ } from '~/locale';
export default class SidebarMediator {
diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js
index 6606271c4fa..65dd62f6af9 100644
--- a/app/assets/javascripts/snippet/snippet_embed.js
+++ b/app/assets/javascripts/snippet/snippet_embed.js
@@ -1,28 +1,35 @@
import { __ } from '~/locale';
+import { parseUrlPathname, parseUrl } from '../lib/utils/common_utils';
+
+function swapActiveState(activateBtn, deactivateBtn) {
+ activateBtn.classList.add('is-active');
+ deactivateBtn.classList.remove('is-active');
+}
export default () => {
const shareBtn = document.querySelector('.js-share-btn');
if (shareBtn) {
- const { protocol, host, pathname } = window.location;
-
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}`;
+ const dataUrl = snippetUrlArea.getAttribute('data-url');
+
+ snippetUrlArea.addEventListener('click', () => snippetUrlArea.select());
shareBtn.addEventListener('click', () => {
- shareBtn.classList.add('is-active');
- embedBtn.classList.remove('is-active');
- snippetUrlArea.value = url;
+ swapActiveState(shareBtn, embedBtn);
+ snippetUrlArea.value = dataUrl;
embedAction.innerText = __('Share');
});
embedBtn.addEventListener('click', () => {
- embedBtn.classList.add('is-active');
- shareBtn.classList.remove('is-active');
- const scriptTag = `<script src="${url}.js"></script>`;
+ const parser = parseUrl(dataUrl);
+ const url = `${parser.origin + parseUrlPathname(dataUrl)}`;
+ const params = parser.search;
+ const scriptTag = `<script src="${url}.js${params}"></script>`;
+
+ swapActiveState(embedBtn, shareBtn);
snippetUrlArea.value = scriptTag;
embedAction.innerText = __('Embed');
});
diff --git a/app/assets/javascripts/snippets/components/app.vue b/app/assets/javascripts/snippets/components/app.vue
new file mode 100644
index 00000000000..bd2cb8e4595
--- /dev/null
+++ b/app/assets/javascripts/snippets/components/app.vue
@@ -0,0 +1,50 @@
+<script>
+import GetSnippetQuery from '../queries/snippet.query.graphql';
+import SnippetHeader from './snippet_header.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ SnippetHeader,
+ GlLoadingIcon,
+ },
+ apollo: {
+ snippet: {
+ query: GetSnippetQuery,
+ variables() {
+ return {
+ ids: this.snippetGid,
+ };
+ },
+ update: data => data.snippets.edges[0].node,
+ },
+ },
+ props: {
+ snippetGid: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ snippet: {},
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.snippet.loading;
+ },
+ },
+};
+</script>
+<template>
+ <div class="js-snippet-view">
+ <gl-loading-icon
+ v-if="isLoading"
+ :label="__('Loading snippet')"
+ :size="2"
+ class="loading-animation prepend-top-20 append-bottom-20"
+ />
+ <snippet-header v-else :snippet="snippet" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
new file mode 100644
index 00000000000..e8f1bfeaf43
--- /dev/null
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -0,0 +1,241 @@
+<script>
+import { __ } from '~/locale';
+import {
+ GlAvatar,
+ GlIcon,
+ GlSprintf,
+ GlButton,
+ GlModal,
+ GlAlert,
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownItem,
+} from '@gitlab/ui';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
+import CanCreatePersonalSnippet from '../queries/userPermissions.query.graphql';
+import CanCreateProjectSnippet from '../queries/projectPermissions.query.graphql';
+
+export default {
+ components: {
+ GlAvatar,
+ GlIcon,
+ GlSprintf,
+ GlButton,
+ GlModal,
+ GlAlert,
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownItem,
+ TimeAgoTooltip,
+ },
+ apollo: {
+ canCreateSnippet: {
+ query() {
+ return this.snippet.project ? CanCreateProjectSnippet : CanCreatePersonalSnippet;
+ },
+ variables() {
+ return {
+ fullPath: this.snippet.project ? this.snippet.project.fullPath : undefined,
+ };
+ },
+ update(data) {
+ return this.snippet.project
+ ? data.project.userPermissions.createSnippet
+ : data.currentUser.userPermissions.createSnippet;
+ },
+ },
+ },
+ props: {
+ snippet: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isDeleting: false,
+ errorMessage: '',
+ canCreateSnippet: false,
+ };
+ },
+ computed: {
+ personalSnippetActions() {
+ return [
+ {
+ condition: this.snippet.userPermissions.updateSnippet,
+ text: __('Edit'),
+ href: this.editLink,
+ click: undefined,
+ variant: 'outline-info',
+ cssClass: undefined,
+ },
+ {
+ condition: this.snippet.userPermissions.adminSnippet,
+ text: __('Delete'),
+ href: undefined,
+ click: this.showDeleteModal,
+ variant: 'outline-danger',
+ cssClass: 'btn-inverted btn-danger ml-2',
+ },
+ {
+ condition: this.canCreateSnippet,
+ text: __('New snippet'),
+ href: this.snippet.project
+ ? `${this.snippet.project.webUrl}/snippets/new`
+ : '/snippets/new',
+ click: undefined,
+ variant: 'outline-success',
+ cssClass: 'btn-inverted btn-success ml-2',
+ },
+ ];
+ },
+ editLink() {
+ return `${this.snippet.webUrl}/edit`;
+ },
+ visibility() {
+ return this.snippet.visibilityLevel;
+ },
+ snippetVisibilityLevelDescription() {
+ switch (this.visibility) {
+ case 'private':
+ return this.snippet.project !== null
+ ? __('The snippet is visible only to project members.')
+ : __('The snippet is visible only to me.');
+ case 'internal':
+ return __('The snippet is visible to any logged in user.');
+ default:
+ return __('The snippet can be accessed without any authentication.');
+ }
+ },
+ visibilityLevelIcon() {
+ switch (this.visibility) {
+ case 'private':
+ return 'lock';
+ case 'internal':
+ return 'shield';
+ default:
+ return 'earth';
+ }
+ },
+ },
+ methods: {
+ redirectToSnippets() {
+ window.location.pathname = 'dashboard/snippets';
+ },
+ closeDeleteModal() {
+ this.$refs.deleteModal.hide();
+ },
+ showDeleteModal() {
+ this.$refs.deleteModal.show();
+ },
+ deleteSnippet() {
+ this.isDeleting = true;
+ this.$apollo
+ .mutate({
+ mutation: DeleteSnippetMutation,
+ variables: { id: this.snippet.id },
+ })
+ .then(() => {
+ this.isDeleting = false;
+ this.errorMessage = undefined;
+ this.closeDeleteModal();
+ this.redirectToSnippets();
+ })
+ .catch(err => {
+ this.isDeleting = false;
+ this.errorMessage = err.message;
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div class="detail-page-header">
+ <div class="detail-page-header-body">
+ <div
+ class="snippet-box qa-snippet-box has-tooltip d-flex align-items-center append-right-5 mb-1"
+ :title="snippetVisibilityLevelDescription"
+ data-container="body"
+ >
+ <span class="sr-only">
+ {{ s__(`VisibilityLevel|${visibility}`) }}
+ </span>
+ <gl-icon :name="visibilityLevelIcon" :size="14" />
+ </div>
+ <div class="creator">
+ <gl-sprintf message="Authored %{timeago} by %{author}">
+ <template #timeago>
+ <time-ago-tooltip
+ :time="snippet.createdAt"
+ tooltip-placement="bottom"
+ css-class="snippet_updated_ago"
+ />
+ </template>
+ <template #author>
+ <a :href="snippet.author.webUrl" class="d-inline">
+ <gl-avatar :size="24" :src="snippet.author.avatarUrl" />
+ <span class="bold">{{ snippet.author.name }}</span>
+ </a>
+ </template>
+ </gl-sprintf>
+ </div>
+ </div>
+
+ <div class="detail-page-header-actions">
+ <div class="d-none d-sm-block">
+ <template v-for="(action, index) in personalSnippetActions">
+ <gl-button
+ v-if="action.condition"
+ :key="index"
+ :variant="action.variant"
+ :class="action.cssClass"
+ :href="action.href || undefined"
+ @click="action.click ? action.click() : undefined"
+ >
+ {{ action.text }}
+ </gl-button>
+ </template>
+ </div>
+ <div class="d-block d-sm-none dropdown">
+ <gl-dropdown :text="__('Options')" class="w-100" toggle-class="text-center">
+ <gl-dropdown-item
+ v-for="(action, index) in personalSnippetActions"
+ :key="index"
+ :href="action.href || undefined"
+ @click="action.click ? action.click() : undefined"
+ >{{ action.text }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+ </div>
+ </div>
+
+ <gl-modal ref="deleteModal" modal-id="delete-modal" title="Example title">
+ <template #modal-title>{{ __('Delete snippet?') }}</template>
+
+ <gl-alert v-if="errorMessage" variant="danger" class="mb-2" @dismiss="errorMessage = ''">{{
+ errorMessage
+ }}</gl-alert>
+
+ <gl-sprintf message="Are you sure you want to delete %{name}?">
+ <template #name
+ ><strong>{{ snippet.title }}</strong></template
+ >
+ </gl-sprintf>
+
+ <template #modal-footer>
+ <gl-button @click="closeDeleteModal">{{ __('Cancel') }}</gl-button>
+ <gl-button
+ variant="danger"
+ :disabled="isDeleting"
+ data-qa-selector="delete_snippet_button"
+ @click="deleteSnippet"
+ >
+ <gl-loading-icon v-if="isDeleting" inline />
+ {{ __('Delete snippet') }}
+ </gl-button>
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/snippets/fragments/author.fragment.graphql b/app/assets/javascripts/snippets/fragments/author.fragment.graphql
new file mode 100644
index 00000000000..2684bd0fa37
--- /dev/null
+++ b/app/assets/javascripts/snippets/fragments/author.fragment.graphql
@@ -0,0 +1,8 @@
+fragment Author on Snippet {
+ author {
+ name,
+ avatarUrl,
+ username,
+ webUrl
+ }
+} \ No newline at end of file
diff --git a/app/assets/javascripts/snippets/fragments/project.fragment.graphql b/app/assets/javascripts/snippets/fragments/project.fragment.graphql
new file mode 100644
index 00000000000..7d65789c67b
--- /dev/null
+++ b/app/assets/javascripts/snippets/fragments/project.fragment.graphql
@@ -0,0 +1,6 @@
+fragment Project on Snippet {
+ project {
+ fullPath
+ webUrl
+ }
+} \ No newline at end of file
diff --git a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
new file mode 100644
index 00000000000..57348a422ec
--- /dev/null
+++ b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
@@ -0,0 +1,13 @@
+fragment SnippetBase on Snippet {
+ id
+ title
+ description
+ createdAt
+ updatedAt
+ visibilityLevel
+ webUrl
+ userPermissions {
+ adminSnippet
+ updateSnippet
+ }
+} \ No newline at end of file
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
new file mode 100644
index 00000000000..654856f8d14
--- /dev/null
+++ b/app/assets/javascripts/snippets/index.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+import SnippetsApp from './components/app.vue';
+
+Vue.use(VueApollo);
+Vue.use(Translate);
+
+export default () => {
+ const el = document.getElementById('js-snippet-view');
+
+ if (!el) {
+ return false;
+ }
+
+ const { snippetGid } = el.dataset;
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ render(createElement) {
+ return createElement(SnippetsApp, {
+ props: {
+ snippetGid,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql b/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql
new file mode 100644
index 00000000000..0c829cbdee6
--- /dev/null
+++ b/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql
@@ -0,0 +1,5 @@
+mutation DeleteSnippet($id: ID!) {
+ destroySnippet(input: {id: $id}) {
+ errors
+ }
+} \ No newline at end of file
diff --git a/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql b/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql
new file mode 100644
index 00000000000..288bd0889bf
--- /dev/null
+++ b/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql
@@ -0,0 +1,7 @@
+query CanCreateProjectSnippet($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ userPermissions {
+ createSnippet
+ }
+ }
+} \ No newline at end of file
diff --git a/app/assets/javascripts/snippets/queries/snippet.query.graphql b/app/assets/javascripts/snippets/queries/snippet.query.graphql
new file mode 100644
index 00000000000..1cb2c86c4d8
--- /dev/null
+++ b/app/assets/javascripts/snippets/queries/snippet.query.graphql
@@ -0,0 +1,15 @@
+#import '../fragments/snippetBase.fragment.graphql'
+#import '../fragments/project.fragment.graphql'
+#import '../fragments/author.fragment.graphql'
+
+query GetSnippetQuery($ids: [ID!]) {
+ snippets(ids: $ids) {
+ edges {
+ node {
+ ...SnippetBase
+ ...Project
+ ...Author
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/snippets/queries/userPermissions.query.graphql b/app/assets/javascripts/snippets/queries/userPermissions.query.graphql
new file mode 100644
index 00000000000..f5b97b3d0f0
--- /dev/null
+++ b/app/assets/javascripts/snippets/queries/userPermissions.query.graphql
@@ -0,0 +1,7 @@
+query CanCreatePersonalSnippet {
+ currentUser {
+ userPermissions {
+ createSnippet
+ }
+ }
+} \ No newline at end of file
diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js
index 7c0097fbe37..a17b8a047c0 100644
--- a/app/assets/javascripts/tracking.js
+++ b/app/assets/javascripts/tracking.js
@@ -73,20 +73,25 @@ export default class Tracking {
return handlers;
}
- static mixin(opts) {
+ static mixin(opts = {}) {
return {
- data() {
- return {
- tracking: {
- // eslint-disable-next-line no-underscore-dangle
- category: this.$options.name || this.$options._componentTag,
- },
- };
+ computed: {
+ trackingCategory() {
+ const localCategory = this.tracking ? this.tracking.category : null;
+ return localCategory || opts.category;
+ },
+ trackingOptions() {
+ return { ...opts, ...this.tracking };
+ },
},
methods: {
- track(action, data) {
- const category = opts.category || data.category || this.tracking.category;
- Tracking.event(category || 'unspecified', action, { ...opts, ...this.tracking, ...data });
+ track(action, data = {}) {
+ const category = data.category || this.trackingCategory;
+ const options = {
+ ...this.trackingOptions,
+ ...data,
+ };
+ Tracking.event(category, action, options);
},
},
};
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 7d6a725b30f..157d89a3a40 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -17,6 +17,7 @@ const handleUserPopoverMouseOut = event => {
renderedPopover.$destroy();
renderedPopover = null;
}
+ target.removeAttribute('aria-describedby');
};
/**
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index da1a7c290f8..57fbb88ca2e 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, consistent-return, no-shadow, no-else-return, no-self-compare, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
+/* eslint-disable func-names, prefer-rest-params, consistent-return, no-shadow, no-else-return, no-self-compare, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
/* global Issuable */
/* global emitSidebarEvent */
@@ -13,7 +13,7 @@ import { parseBoolean } from './lib/utils/common_utils';
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els, options = {}) {
- var $els;
+ const $els = $(els || '.js-user-search');
this.users = this.users.bind(this);
this.user = this.user.bind(this);
this.usersPath = '/autocomplete/users.json';
@@ -28,36 +28,11 @@ function UsersSelect(currentUser, els, options = {}) {
const { handleClick } = options;
- $els = $(els);
-
- if (!els) {
- $els = $('.js-user-search');
- }
-
$els.each(
(function(_this) {
return function(i, dropdown) {
- var options = {};
- var $block,
- $collapsedSidebar,
- $dropdown,
- $loading,
- $selectbox,
- $value,
- abilityName,
- assignTo,
- assigneeTemplate,
- collapsedAssigneeTemplate,
- defaultLabel,
- defaultNullUser,
- firstUser,
- issueURL,
- selectedId,
- selectedIdDefault,
- showAnyUser,
- showNullUser,
- showMenuAbove;
- $dropdown = $(dropdown);
+ const options = {};
+ const $dropdown = $(dropdown);
options.projectId = $dropdown.data('projectId');
options.groupId = $dropdown.data('groupId');
options.showCurrentUser = $dropdown.data('currentUser');
@@ -65,22 +40,25 @@ function UsersSelect(currentUser, els, options = {}) {
options.todoStateFilter = $dropdown.data('todoStateFilter');
options.iid = $dropdown.data('iid');
options.issuableType = $dropdown.data('issuableType');
- showNullUser = $dropdown.data('nullUser');
- defaultNullUser = $dropdown.data('nullUserDefault');
- showMenuAbove = $dropdown.data('showMenuAbove');
- showAnyUser = $dropdown.data('anyUser');
- firstUser = $dropdown.data('firstUser');
+ const showNullUser = $dropdown.data('nullUser');
+ const defaultNullUser = $dropdown.data('nullUserDefault');
+ const showMenuAbove = $dropdown.data('showMenuAbove');
+ const showAnyUser = $dropdown.data('anyUser');
+ const firstUser = $dropdown.data('firstUser');
options.authorId = $dropdown.data('authorId');
- defaultLabel = $dropdown.data('defaultLabel');
- issueURL = $dropdown.data('issueUpdate');
- $selectbox = $dropdown.closest('.selectbox');
- $block = $selectbox.closest('.block');
- abilityName = $dropdown.data('abilityName');
- $value = $block.find('.value');
- $collapsedSidebar = $block.find('.sidebar-collapsed-user');
- $loading = $block.find('.block-loading').fadeOut();
- selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
- selectedId = $dropdown.data('selected');
+ const defaultLabel = $dropdown.data('defaultLabel');
+ const issueURL = $dropdown.data('issueUpdate');
+ const $selectbox = $dropdown.closest('.selectbox');
+ let $block = $selectbox.closest('.block');
+ const abilityName = $dropdown.data('abilityName');
+ let $value = $block.find('.value');
+ const $collapsedSidebar = $block.find('.sidebar-collapsed-user');
+ const $loading = $block.find('.block-loading').fadeOut();
+ const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
+ let selectedId = $dropdown.data('selected');
+ let assignTo;
+ let assigneeTemplate;
+ let collapsedAssigneeTemplate;
if (selectedId === undefined) {
selectedId = selectedIdDefault;
@@ -207,15 +185,15 @@ function UsersSelect(currentUser, els, options = {}) {
});
assignTo = function(selected) {
- var data;
- data = {};
+ const data = {};
data[abilityName] = {};
data[abilityName].assignee_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return axios.put(issueURL, data).then(({ data }) => {
- var user, tooltipTitle;
+ let user = {};
+ let tooltipTitle = user.name;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
@@ -471,10 +449,9 @@ function UsersSelect(currentUser, els, options = {}) {
}
}
- var isIssueIndex, isMRIndex, page, selected;
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === page && page === 'projects:merge_requests:index';
+ const page = $('body').attr('data-page');
+ const isIssueIndex = page === 'projects:issues:index';
+ const isMRIndex = page === page && page === 'projects:merge_requests:index';
if (
$dropdown.hasClass('js-filter-bulk-update') ||
$dropdown.hasClass('js-issuable-form-dropdown')
@@ -501,7 +478,7 @@ function UsersSelect(currentUser, els, options = {}) {
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if (!$dropdown.hasClass('js-multiselect')) {
- selected = $dropdown
+ const selected = $dropdown
.closest('.selectbox')
.find(`input[name='${$dropdown.data('fieldName')}']`)
.val();
@@ -544,9 +521,8 @@ function UsersSelect(currentUser, els, options = {}) {
},
updateLabel: $dropdown.data('dropdownTitle'),
renderRow(user) {
- var avatar, img, username;
- username = user.username ? `@${user.username}` : '';
- avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
+ const username = user.username ? `@${user.username}` : '';
+ const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
let selected = false;
@@ -565,7 +541,7 @@ function UsersSelect(currentUser, els, options = {}) {
selected = user.id === selectedId;
}
- img = '';
+ let img = '';
if (user.beforeDivider != null) {
`<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(
user.name,
@@ -586,35 +562,34 @@ function UsersSelect(currentUser, els, options = {}) {
$('.ajax-users-select').each(
(function(_this) {
return function(i, select) {
- var firstUser, showAnyUser, showEmailUser, showNullUser;
- var options = {};
+ const options = {};
options.skipLdap = $(select).hasClass('skip_ldap');
options.projectId = $(select).data('projectId');
options.groupId = $(select).data('groupId');
options.showCurrentUser = $(select).data('currentUser');
options.authorId = $(select).data('authorId');
options.skipUsers = $(select).data('skipUsers');
- showNullUser = $(select).data('nullUser');
- showAnyUser = $(select).data('anyUser');
- showEmailUser = $(select).data('emailUser');
- firstUser = $(select).data('firstUser');
+ const showNullUser = $(select).data('nullUser');
+ const showAnyUser = $(select).data('anyUser');
+ const showEmailUser = $(select).data('emailUser');
+ const firstUser = $(select).data('firstUser');
return $(select).select2({
placeholder: __('Search for a user'),
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query(query) {
return _this.users(query.term, options, users => {
- var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
- data = {
+ let name;
+ const data = {
results: users,
};
if (query.term.length === 0) {
if (firstUser) {
// Move current user to the front of the list
- ref = data.results;
+ const ref = data.results;
- for (index = 0, len = ref.length; index < len; index += 1) {
- obj = ref[index];
+ for (let index = 0, len = ref.length; index < len; index += 1) {
+ const obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
data.results.unshift(obj);
@@ -623,7 +598,7 @@ function UsersSelect(currentUser, els, options = {}) {
}
}
if (showNullUser) {
- nullUser = {
+ const nullUser = {
name: s__('UsersSelect|Unassigned'),
id: 0,
};
@@ -634,7 +609,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (name === true) {
name = s__('UsersSelect|Any User');
}
- anyUser = {
+ const anyUser = {
name,
id: null,
};
@@ -646,8 +621,8 @@ function UsersSelect(currentUser, els, options = {}) {
data.results.length === 0 &&
query.term.match(/^[^@]+@[^@]+$/)
) {
- var trimmed = query.term.trim();
- emailUser = {
+ const trimmed = query.term.trim();
+ const emailUser = {
name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
username: trimmed,
id: trimmed,
@@ -659,18 +634,15 @@ function UsersSelect(currentUser, els, options = {}) {
});
},
initSelection() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return _this.initSelection.apply(_this, args);
},
formatResult() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return _this.formatResult.apply(_this, args);
},
formatSelection() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return _this.formatSelection.apply(_this, args);
},
dropdownCssClass: 'ajax-users-dropdown',
@@ -687,10 +659,9 @@ function UsersSelect(currentUser, els, options = {}) {
}
UsersSelect.prototype.initSelection = function(element, callback) {
- var id, nullUser;
- id = $(element).val();
+ const id = $(element).val();
if (id === '0') {
- nullUser = {
+ const nullUser = {
name: s__('UsersSelect|Unassigned'),
};
return callback(nullUser);
@@ -700,11 +671,9 @@ UsersSelect.prototype.initSelection = function(element, callback) {
};
UsersSelect.prototype.formatResult = function(user) {
- var avatar;
+ let avatar = gon.default_avatar_url;
if (user.avatar_url) {
avatar = user.avatar_url;
- } else {
- avatar = gon.default_avatar_url;
}
return `
<div class='user-result'>
@@ -732,8 +701,7 @@ UsersSelect.prototype.user = function(user_id, callback) {
return false;
}
- var url;
- url = this.buildUrl(this.userPath);
+ let url = this.buildUrl(this.userPath);
url = url.replace(':id', user_id);
return axios.get(url).then(({ data }) => {
callback(data);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
deleted file mode 100644
index 1873e09c370..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ /dev/null
@@ -1,245 +0,0 @@
-<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
-import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
-import { __ } from '~/locale';
-import timeagoMixin from '../../vue_shared/mixins/timeago';
-import LoadingButton from '../../vue_shared/components/loading_button.vue';
-import { visitUrl } from '../../lib/utils/url_utility';
-import createFlash from '../../flash';
-import MemoryUsage from './memory_usage.vue';
-import StatusIcon from './mr_widget_status_icon.vue';
-import ReviewAppLink from './review_app_link.vue';
-import MRWidgetService from '../services/mr_widget_service';
-
-export default {
- // name: 'Deployment' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
- // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
- name: 'Deployment',
- components: {
- LoadingButton,
- MemoryUsage,
- StatusIcon,
- Icon,
- TooltipOnTruncate,
- FilteredSearchDropdown,
- ReviewAppLink,
- VisualReviewAppLink: () =>
- import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [timeagoMixin],
- props: {
- deployment: {
- type: Object,
- required: true,
- },
- showMetrics: {
- type: Boolean,
- required: true,
- },
- showVisualReviewApp: {
- type: Boolean,
- required: false,
- default: false,
- },
- visualReviewAppMeta: {
- type: Object,
- required: false,
- default: () => ({
- sourceProjectId: '',
- sourceProjectPath: '',
- mergeRequestId: '',
- appUrl: '',
- }),
- },
- },
- deployedTextMap: {
- running: __('Deploying to'),
- success: __('Deployed to'),
- failed: __('Failed to deploy to'),
- created: __('Will deploy to'),
- canceled: __('Failed to deploy to'),
- },
- data() {
- return {
- isStopping: false,
- };
- },
- computed: {
- deployTimeago() {
- return this.timeFormated(this.deployment.deployed_at);
- },
- deploymentExternalUrl() {
- if (this.deployment.changes && this.deployment.changes.length === 1) {
- return this.deployment.changes[0].external_url;
- }
- return this.deployment.external_url;
- },
- hasExternalUrls() {
- return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
- },
- hasDeploymentTime() {
- return Boolean(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
- },
- hasDeploymentMeta() {
- return Boolean(this.deployment.url && this.deployment.name);
- },
- hasMetrics() {
- return Boolean(this.deployment.metrics_url);
- },
- deployedText() {
- return this.$options.deployedTextMap[this.deployment.status];
- },
- isDeployInProgress() {
- return this.deployment.status === 'running';
- },
- deployInProgressTooltip() {
- return this.isDeployInProgress
- ? __('Stopping this environment is currently not possible as a deployment is in progress')
- : '';
- },
- shouldRenderDropdown() {
- return this.deployment.changes && this.deployment.changes.length > 1;
- },
- showMemoryUsage() {
- return this.hasMetrics && this.showMetrics;
- },
- },
- methods: {
- stopEnvironment() {
- const msg = __('Are you sure you want to stop this environment?');
- const isConfirmed = confirm(msg); // eslint-disable-line
-
- if (isConfirmed) {
- this.isStopping = true;
-
- MRWidgetService.stopEnvironment(this.deployment.stop_url)
- .then(res => res.data)
- .then(data => {
- if (data.redirect_url) {
- visitUrl(data.redirect_url);
- }
-
- this.isStopping = false;
- })
- .catch(() => {
- createFlash(
- __('Something went wrong while stopping this environment. Please try again.'),
- );
- this.isStopping = false;
- });
- }
- },
- },
-};
-</script>
-
-<template>
- <div class="deploy-heading">
- <div class="ci-widget media">
- <div class="media-body">
- <div class="deploy-body">
- <div class="js-deployment-info deployment-info">
- <template v-if="hasDeploymentMeta">
- <span> {{ deployedText }} </span>
- <tooltip-on-truncate
- :title="deployment.name"
- truncate-target="child"
- class="deploy-link label-truncate"
- >
- <a
- :href="deployment.url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-meta"
- >
- {{ deployment.name }}
- </a>
- </tooltip-on-truncate>
- </template>
- <span
- v-if="hasDeploymentTime"
- v-gl-tooltip
- :title="deployment.deployed_at_formatted"
- class="js-deploy-time"
- >
- {{ deployTimeago }}
- </span>
- <memory-usage
- v-if="showMemoryUsage"
- :metrics-url="deployment.metrics_url"
- :metrics-monitoring-url="deployment.metrics_monitoring_url"
- />
- </div>
- <div>
- <template v-if="hasExternalUrls">
- <filtered-search-dropdown
- v-if="shouldRenderDropdown"
- class="js-mr-wigdet-deployment-dropdown inline"
- :items="deployment.changes"
- :main-action-link="deploymentExternalUrl"
- filter-key="path"
- >
- <template slot="mainAction" slot-scope="slotProps">
- <review-app-link
- :link="deploymentExternalUrl"
- :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
- />
- </template>
-
- <template slot="result" slot-scope="slotProps">
- <a
- :href="slotProps.result.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="menu-item"
- >
- <strong class="str-truncated-100 append-bottom-0 d-block">
- {{ slotProps.result.path }}
- </strong>
-
- <p class="text-secondary str-truncated-100 append-bottom-0 d-block">
- {{ slotProps.result.external_url }}
- </p>
- </a>
- </template>
- </filtered-search-dropdown>
- <template v-else>
- <review-app-link
- :link="deploymentExternalUrl"
- css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
- />
- </template>
- <visual-review-app-link
- v-if="showVisualReviewApp"
- :link="deploymentExternalUrl"
- :app-metadata="visualReviewAppMeta"
- />
- </template>
- <span
- v-if="deployment.stop_url"
- v-gl-tooltip
- :title="deployInProgressTooltip"
- class="d-inline-block"
- tabindex="0"
- >
- <loading-button
- :loading="isStopping"
- :disabled="isDeployInProgress"
- :title="__('Stop environment')"
- container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4"
- @click="stopEnvironment"
- >
- <icon name="stop" />
- </loading-button>
- </span>
- </div>
- </div>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
new file mode 100644
index 00000000000..90741e3aa44
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
@@ -0,0 +1,8 @@
+// DEPLOYMENT STATUSES
+export const CREATED = 'created';
+export const MANUAL_DEPLOY = 'manual_deploy';
+export const WILL_DEPLOY = 'will_deploy';
+export const RUNNING = 'running';
+export const SUCCESS = 'success';
+export const FAILED = 'failed';
+export const CANCELED = 'canceled';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
new file mode 100644
index 00000000000..e03b1e6d6a6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
@@ -0,0 +1,108 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import DeploymentInfo from './deployment_info.vue';
+import DeploymentViewButton from './deployment_view_button.vue';
+import DeploymentStopButton from './deployment_stop_button.vue';
+import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED, RUNNING, SUCCESS } from './constants';
+
+export default {
+ // name: 'Deployment' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ name: 'Deployment',
+ components: {
+ DeploymentInfo,
+ DeploymentStopButton,
+ DeploymentViewButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ deployment: {
+ type: Object,
+ required: true,
+ },
+ showMetrics: {
+ type: Boolean,
+ required: true,
+ },
+ showVisualReviewApp: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ visualReviewAppMeta: {
+ type: Object,
+ required: false,
+ default: () => ({
+ sourceProjectId: '',
+ sourceProjectPath: '',
+ mergeRequestId: '',
+ appUrl: '',
+ }),
+ },
+ },
+ computed: {
+ canBeManuallyDeployed() {
+ return this.computedDeploymentStatus === MANUAL_DEPLOY;
+ },
+ computedDeploymentStatus() {
+ if (this.deployment.status === CREATED) {
+ return this.isManual ? MANUAL_DEPLOY : WILL_DEPLOY;
+ }
+ return this.deployment.status;
+ },
+ hasExternalUrls() {
+ return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
+ },
+ hasPreviousDeployment() {
+ return Boolean(!this.isCurrent && this.deployment.deployed_at);
+ },
+ isCurrent() {
+ return this.computedDeploymentStatus === SUCCESS;
+ },
+ isManual() {
+ return Boolean(
+ this.deployment.details &&
+ this.deployment.details.playable_build &&
+ this.deployment.details.playable_build.play_path,
+ );
+ },
+ isDeployInProgress() {
+ return this.deployment.status === RUNNING;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="deploy-heading">
+ <div class="ci-widget media">
+ <div class="media-body">
+ <div class="deploy-body">
+ <deployment-info
+ :computed-deployment-status="computedDeploymentStatus"
+ :deployment="deployment"
+ :show-metrics="showMetrics"
+ />
+ <div>
+ <!-- show appropriate version of review app button -->
+ <deployment-view-button
+ v-if="hasExternalUrls"
+ :is-current="isCurrent"
+ :deployment="deployment"
+ :show-visual-review-app="showVisualReviewApp"
+ :visual-review-app-metadata="visualReviewAppMeta"
+ />
+ <!-- if it is stoppable, show stop -->
+ <deployment-stop-button
+ v-if="deployment.stop_url"
+ :is-deploy-in-progress="isDeployInProgress"
+ :stop-url="deployment.stop_url"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
new file mode 100644
index 00000000000..db4a4ece002
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import MemoryUsage from './memory_usage.vue';
+import { MANUAL_DEPLOY, WILL_DEPLOY, RUNNING, SUCCESS, FAILED, CANCELED } from './constants';
+
+export default {
+ name: 'DeploymentInfo',
+ components: {
+ GlLink,
+ MemoryUsage,
+ TooltipOnTruncate,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ computedDeploymentStatus: {
+ type: String,
+ required: true,
+ },
+ deployment: {
+ type: Object,
+ required: true,
+ },
+ showMetrics: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ deployedTextMap: {
+ [MANUAL_DEPLOY]: __('Can deploy manually to'),
+ [WILL_DEPLOY]: __('Will deploy to'),
+ [RUNNING]: __('Deploying to'),
+ [SUCCESS]: __('Deployed to'),
+ [FAILED]: __('Failed to deploy to'),
+ [CANCELED]: __('Canceled deploy to'),
+ },
+ computed: {
+ deployTimeago() {
+ return this.timeFormatted(this.deployment.deployed_at);
+ },
+ deployedText() {
+ return this.$options.deployedTextMap[this.computedDeploymentStatus];
+ },
+ hasDeploymentTime() {
+ return Boolean(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
+ },
+ hasDeploymentMeta() {
+ return Boolean(this.deployment.url && this.deployment.name);
+ },
+ hasMetrics() {
+ return Boolean(this.deployment.metrics_url);
+ },
+ showMemoryUsage() {
+ return this.hasMetrics && this.showMetrics;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="js-deployment-info deployment-info">
+ <template v-if="hasDeploymentMeta">
+ <span>{{ deployedText }}</span>
+ <tooltip-on-truncate
+ :title="deployment.name"
+ truncate-target="child"
+ class="deploy-link label-truncate"
+ >
+ <gl-link
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-meta gl-font-size-12"
+ >
+ {{ deployment.name }}
+ </gl-link>
+ </tooltip-on-truncate>
+ </template>
+ <span
+ v-if="hasDeploymentTime"
+ v-gl-tooltip
+ :title="deployment.deployed_at_formatted"
+ class="js-deploy-time"
+ >
+ {{ deployTimeago }}
+ </span>
+ <memory-usage
+ v-if="showMemoryUsage"
+ :metrics-url="deployment.metrics_url"
+ :metrics-monitoring-url="deployment.metrics_monitoring_url"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_stop_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_stop_button.vue
new file mode 100644
index 00000000000..e20296c41a2
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_stop_button.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { visitUrl } from '~/lib/utils/url_utility';
+import createFlash from '~/flash';
+import MRWidgetService from '../../services/mr_widget_service';
+
+export default {
+ name: 'DeploymentStopButton',
+ components: {
+ LoadingButton,
+ Icon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ isDeployInProgress: {
+ type: Boolean,
+ required: true,
+ },
+ stopUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isStopping: false,
+ };
+ },
+ computed: {
+ deployInProgressTooltip() {
+ return this.isDeployInProgress
+ ? __('Stopping this environment is currently not possible as a deployment is in progress')
+ : '';
+ },
+ },
+ methods: {
+ stopEnvironment() {
+ const msg = __('Are you sure you want to stop this environment?');
+ const isConfirmed = confirm(msg); // eslint-disable-line
+
+ if (isConfirmed) {
+ this.isStopping = true;
+
+ MRWidgetService.stopEnvironment(this.stopUrl)
+ .then(res => res.data)
+ .then(data => {
+ if (data.redirect_url) {
+ visitUrl(data.redirect_url);
+ }
+
+ this.isStopping = false;
+ })
+ .catch(() => {
+ createFlash(
+ __('Something went wrong while stopping this environment. Please try again.'),
+ );
+ this.isStopping = false;
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <span v-gl-tooltip :title="deployInProgressTooltip" class="d-inline-block" tabindex="0">
+ <loading-button
+ v-gl-tooltip
+ :loading="isStopping"
+ :disabled="isDeployInProgress"
+ :title="__('Stop environment')"
+ container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4"
+ @click="stopEnvironment"
+ >
+ <icon name="stop" />
+ </loading-button>
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
new file mode 100644
index 00000000000..9965e3d5203
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -0,0 +1,99 @@
+<script>
+import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
+import ReviewAppLink from '../review_app_link.vue';
+
+export default {
+ name: 'DeploymentViewButton',
+ components: {
+ FilteredSearchDropdown,
+ ReviewAppLink,
+ VisualReviewAppLink: () =>
+ import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
+ },
+ props: {
+ deployment: {
+ type: Object,
+ required: true,
+ },
+ isCurrent: {
+ type: Boolean,
+ required: true,
+ },
+ showVisualReviewApp: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ visualReviewAppMeta: {
+ type: Object,
+ required: false,
+ default: () => ({
+ sourceProjectId: '',
+ sourceProjectPath: '',
+ mergeRequestId: '',
+ appUrl: '',
+ }),
+ },
+ },
+ computed: {
+ deploymentExternalUrl() {
+ if (this.deployment.changes && this.deployment.changes.length === 1) {
+ return this.deployment.changes[0].external_url;
+ }
+ return this.deployment.external_url;
+ },
+ shouldRenderDropdown() {
+ return this.deployment.changes && this.deployment.changes.length > 1;
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <filtered-search-dropdown
+ v-if="shouldRenderDropdown"
+ class="js-mr-wigdet-deployment-dropdown inline"
+ :items="deployment.changes"
+ :main-action-link="deploymentExternalUrl"
+ filter-key="path"
+ >
+ <template slot="mainAction" slot-scope="slotProps">
+ <review-app-link
+ :is-current="isCurrent"
+ :link="deploymentExternalUrl"
+ :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
+ />
+ </template>
+
+ <template slot="result" slot-scope="slotProps">
+ <a
+ :href="slotProps.result.external_url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-url-menu-item menu-item"
+ >
+ <strong class="str-truncated-100 append-bottom-0 d-block">
+ {{ slotProps.result.path }}
+ </strong>
+
+ <p class="text-secondary str-truncated-100 append-bottom-0 d-block">
+ {{ slotProps.result.external_url }}
+ </p>
+ </a>
+ </template>
+ </filtered-search-dropdown>
+ <template v-else>
+ <review-app-link
+ :is-current="isCurrent"
+ :link="deploymentExternalUrl"
+ css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
+ />
+ </template>
+ <visual-review-app-link
+ v-if="showVisualReviewApp"
+ :link="deploymentExternalUrl"
+ :app-metadata="visualReviewAppMeta"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
index 7ce454b7338..fe41a15979e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
@@ -1,10 +1,10 @@
<script>
import { sprintf, s__ } from '~/locale';
-import statusCodes from '../../lib/utils/http_status';
-import { bytesToMiB } from '../../lib/utils/number_utils';
-import { backOff } from '../../lib/utils/common_utils';
-import MemoryGraph from '../../vue_shared/components/memory_graph.vue';
-import MRWidgetService from '../services/mr_widget_service';
+import statusCodes from '~/lib/utils/http_status';
+import { bytesToMiB } from '~/lib/utils/number_utils';
+import { backOff } from '~/lib/utils/common_utils';
+import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
+import MRWidgetService from '../../services/mr_widget_service';
export default {
name: 'MemoryUsage',
@@ -169,12 +169,6 @@ export default {
<p v-if="shouldShowMetricsUnavailable" class="usage-info js-usage-info usage-info-unavailable">
{{ s__('mrWidget|Deployment statistics are not available currently') }}
</p>
- <memory-graph
- v-if="shouldShowMemoryGraph"
- :metrics="memoryMetrics"
- :deployment-time="deploymentTime"
- height="25"
- width="100"
- />
+ <memory-graph v-if="shouldShowMemoryGraph" :metrics="memoryMetrics" :height="25" :width="110" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/loading.vue b/app/assets/javascripts/vue_merge_request_widget/components/loading.vue
new file mode 100644
index 00000000000..78dc28ee92b
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/loading.vue
@@ -0,0 +1,29 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+
+<template>
+ <div class="prepend-top-default">
+ <div class="mr-widget-heading p-3">
+ <gl-skeleton-loader :width="577" :height="12">
+ <rect width="86" height="12" rx="2" />
+ <rect x="96" width="300" height="12" rx="2" />
+ </gl-skeleton-loader>
+ </div>
+ <div class="mr-widget-heading mr-widget-workflow p-3">
+ <gl-skeleton-loader :width="577" :height="72">
+ <rect width="120" height="12" rx="2" />
+ <rect y="20" width="300" height="12" rx="2" />
+ <rect y="40" width="60" height="12" rx="2" />
+ <rect y="40" x="68" width="100" height="12" rx="2" />
+ <rect y="60" width="40" height="12" rx="2" />
+ </gl-skeleton-loader>
+ </div>
+ </div>
+</template>
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 7c5f35579b8..42db1935123 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,12 +1,12 @@
<script>
/* eslint-disable vue/require-default-prop */
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
import { sprintf, s__ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
-import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
export default {
name: 'MRWidgetPipeline',
@@ -28,6 +28,10 @@ export default {
type: Object,
required: true,
},
+ pipelineCoverageDelta: {
+ 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: {
@@ -92,6 +96,16 @@ export default {
showSourceBranch() {
return Boolean(this.pipeline.ref.branch);
},
+ coverageDeltaClass() {
+ const delta = this.pipelineCoverageDelta;
+ if (delta && parseFloat(delta) > 0) {
+ return 'text-success';
+ }
+ if (delta && parseFloat(delta) < 0) {
+ return 'text-danger';
+ }
+ return '';
+ },
},
};
</script>
@@ -142,6 +156,14 @@ export default {
</div>
<div v-if="pipeline.coverage" class="coverage">
{{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}%
+
+ <span
+ v-if="pipelineCoverageDelta"
+ class="js-pipeline-coverage-delta"
+ :class="coverageDeltaClass"
+ >
+ ({{ pipelineCoverageDelta }}%)
+ </span>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index ffc3e0967d4..90fb254ecca 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -1,9 +1,10 @@
<script>
import _ from 'underscore';
import ArtifactsApp from './artifacts_list_app.vue';
-import Deployment from './deployment.vue';
+import Deployment from './deployment/deployment.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
/**
* Renders the pipeline and related deployments from the store.
@@ -23,6 +24,7 @@ export default {
MergeTrainPositionIndicator: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_position_indicator.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
props: {
mr: {
type: Object,
@@ -62,7 +64,7 @@ export default {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
},
showVisualReviewAppLink() {
- return this.mr.visualReviewAppAvailable;
+ return this.mr.visualReviewAppAvailable && this.glFeatures.anonymousVisualReviewFeedback;
},
showMergeTrainPositionIndicator() {
return _.isNumber(this.mr.mergeTrainIndex);
@@ -74,6 +76,7 @@ export default {
<mr-widget-container>
<mr-widget-pipeline
:pipeline="pipeline"
+ :pipeline-coverage-delta="mr.pipelineCoverageDelta"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
:source-branch="branch"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
index 75f557d05dd..1550ec0f21e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
@@ -6,19 +7,35 @@ export default {
Icon,
},
props: {
- link: {
+ cssClass: {
type: String,
required: true,
},
- cssClass: {
+ isCurrent: {
+ type: Boolean,
+ required: true,
+ },
+ link: {
type: String,
required: true,
},
},
+ computed: {
+ linkText() {
+ return this.isCurrent ? __('View app') : __('View previous app');
+ },
+ },
};
</script>
<template>
- <a :href="link" target="_blank" rel="noopener noreferrer nofollow" :class="cssClass">
- {{ __('View app') }} <icon class="fgray" name="external-link" />
+ <a
+ :href="link"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ :class="cssClass"
+ data-track-event="open_review_app"
+ data-track-label="review_app"
+ >
+ {{ linkText }} <icon class="fgray" name="external-link" />
</a>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index a2b5a79af36..c8e652a1305 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -1,5 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
+import { GlLoadingIcon } from '@gitlab/ui';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import { s__, __ } from '~/locale';
@@ -7,7 +8,6 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
-import { GlLoadingIcon } from '@gitlab/ui';
export default {
name: 'MRWidgetMerged',
@@ -155,7 +155,7 @@ export default {
{{ cherryPickLabel }}
</a>
</div>
- <section class="mr-info-list">
+ <section class="mr-info-list" data-qa-selector="merged_status_content">
<p>
{{ s__('mrWidget|The changes were merged into') }}
<span class="label-branch">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 2c113770d8b..d230ac566de 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -1,10 +1,11 @@
<script>
import _ from 'underscore';
+import { GlIcon } from '@gitlab/ui';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
-import simplePoll from '~/lib/utils/simple_poll';
-import { __ } from '~/locale';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
+import simplePoll from '~/lib/utils/simple_poll';
+import { __, sprintf } from '~/locale';
import MergeRequest from '../../../merge_request';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import Flash from '../../../flash';
@@ -24,6 +25,11 @@ export default {
CommitsHeader,
CommitEdit,
CommitMessageDropdown,
+ GlIcon,
+ MergeImmediatelyConfirmationDialog: () =>
+ import(
+ 'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
+ ),
},
mixins: [readyToMergeMixin],
props: {
@@ -111,6 +117,18 @@ export default {
shouldShowMergeEdit() {
return !this.mr.ffOnlyEnabled;
},
+ shaMismatchLink() {
+ const href = this.mr.mergeRequestDiffsPath;
+
+ return sprintf(
+ __('New changes were added. %{linkStart}Reload the page to review them%{linkEnd}'),
+ {
+ linkStart: `<a href="${href}">`,
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
},
methods: {
updateMergeCommitMessage(includeDescription) {
@@ -123,7 +141,7 @@ export default {
}
const options = {
- sha: this.mr.sha,
+ sha: this.mr.latestSHA || this.mr.sha,
commit_message: this.commitMessage,
auto_merge_strategy: useAutoMerge ? this.mr.preferredAutoMergeStrategy : undefined,
should_remove_source_branch: this.removeSourceBranch === true,
@@ -151,6 +169,16 @@ export default {
new Flash(__('Something went wrong. Please try again.')); // eslint-disable-line
});
},
+ handleMergeImmediatelyButtonClick() {
+ if (this.isMergeImmediatelyDangerous) {
+ this.$refs.confirmationDialog.show();
+ } else {
+ this.handleMergeButtonClick(false, true);
+ }
+ },
+ onMergeImmediatelyConfirmation() {
+ this.handleMergeButtonClick(false, true);
+ },
initiateMergePolling() {
simplePoll(
(continuePolling, stopPolling) => {
@@ -249,9 +277,10 @@ export default {
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
+ data-qa-selector="merge_moment_dropdown"
:aria-label="__('Select merge moment')"
>
- <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
+ <i class="fa fa-chevron-down" aria-hidden="true"></i>
</button>
<ul
v-if="shouldShowMergeImmediatelyDropdown"
@@ -271,10 +300,16 @@ export default {
</a>
</li>
<li>
+ <merge-immediately-confirmation-dialog
+ ref="confirmationDialog"
+ :docs-url="mr.mergeImmediatelyDocsPath"
+ @mergeImmediately="onMergeImmediatelyConfirmation"
+ />
<a
- class="accept-merge-request qa-merge-immediately-option"
+ class="accept-merge-request js-merge-immediately-button"
+ data-qa-selector="merge_immediately_option"
href="#"
- @click.prevent="handleMergeButtonClick(false, true)"
+ @click.prevent="handleMergeImmediatelyButtonClick"
>
<span class="media">
<span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span>
@@ -312,6 +347,10 @@ export default {
</template>
</div>
</div>
+ <div v-if="mr.isSHAMismatch" class="d-flex align-items-center mt-2 js-sha-mismatch">
+ <gl-icon name="warning-solid" class="text-warning mr-1" />
+ <span class="text-warning" v-html="shaMismatchLink"></span>
+ </div>
</div>
</div>
<template v-if="shouldShowMergeControls">
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index 40e6203599f..32a2b7b83f4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -23,5 +23,8 @@ export default {
shouldShowMergeImmediatelyDropdown() {
return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds;
},
+ isMergeImmediatelyDangerous() {
+ return false;
+ },
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index edd21a81f8b..38a7c262b3e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,16 +1,17 @@
<script>
import _ from 'underscore';
-import { sprintf, s__, __ } from '~/locale';
-import Project from '~/pages/projects/project';
-import SmartInterval from '~/smart_interval';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
+import { sprintf, s__, __ } from '~/locale';
+import Project from '~/pages/projects/project';
+import SmartInterval from '~/smart_interval';
import createFlash from '../flash';
+import Loading from './components/loading.vue';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
-import Deployment from './components/deployment.vue';
+import Deployment from './components/deployment/deployment.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
import MergedState from './components/states/mr_widget_merged.vue';
@@ -24,7 +25,6 @@ import NothingToMergeState from './components/states/nothing_to_merge.vue';
import MissingBranchState from './components/states/mr_widget_missing_branch.vue';
import NotAllowedState from './components/states/mr_widget_not_allowed.vue';
import ReadyToMergeState from './components/states/ready_to_merge.vue';
-import ShaMismatchState from './components/states/sha_mismatch.vue';
import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue';
import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue';
import PipelineFailedState from './components/states/pipeline_failed.vue';
@@ -44,6 +44,7 @@ export default {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
name: 'MRWidget',
components: {
+ Loading,
'mr-widget-header': WidgetHeader,
'mr-widget-merge-help': WidgetMergeHelp,
MrWidgetPipelineContainer,
@@ -61,7 +62,7 @@ export default {
'mr-widget-not-allowed': NotAllowedState,
'mr-widget-missing-branch': MissingBranchState,
'mr-widget-ready-to-merge': ReadyToMergeState,
- 'sha-mismatch': ShaMismatchState,
+ 'sha-mismatch': ReadyToMergeState,
'mr-widget-checking': CheckingState,
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
'mr-widget-pipeline-blocked': PipelineBlockedState,
@@ -80,12 +81,12 @@ export default {
},
},
data() {
- const store = new MRWidgetStore(this.mrData || window.gl.mrWidgetData);
- const service = this.createService(store);
+ const store = this.mrData && new MRWidgetStore(this.mrData);
+
return {
mr: store,
- state: store.state,
- service,
+ state: store && store.state,
+ service: store && this.createService(store),
};
},
computed: {
@@ -133,29 +134,58 @@ export default {
}
},
},
- created() {
- this.initPolling();
- this.bindEventHubListeners();
- eventHub.$on('mr.discussion.updated', this.checkStatus);
- },
mounted() {
- this.setFaviconHelper();
- this.initDeploymentsPolling();
-
- if (this.shouldRenderMergedPipeline) {
- this.initPostMergeDeploymentsPolling();
+ if (gon && gon.features && gon.features.asyncMrWidget) {
+ MRWidgetService.fetchInitialData()
+ .then(({ data }) => this.initWidget(data))
+ .catch(() =>
+ createFlash(__('Unable to load the merge request widget. Try reloading the page.')),
+ );
+ } else {
+ this.initWidget();
}
},
beforeDestroy() {
eventHub.$off('mr.discussion.updated', this.checkStatus);
- this.pollingInterval.destroy();
- this.deploymentsInterval.destroy();
+ if (this.pollingInterval) {
+ this.pollingInterval.destroy();
+ }
+
+ if (this.deploymentsInterval) {
+ this.deploymentsInterval.destroy();
+ }
if (this.postMergeDeploymentsInterval) {
this.postMergeDeploymentsInterval.destroy();
}
},
methods: {
+ initWidget(data = {}) {
+ if (this.mr) {
+ this.mr.setData({ ...window.gl.mrWidgetData, ...data });
+ } else {
+ this.mr = new MRWidgetStore({ ...window.gl.mrWidgetData, ...data });
+ }
+
+ if (!this.state) {
+ this.state = this.mr.state;
+ }
+
+ if (!this.service) {
+ this.service = this.createService(this.mr);
+ }
+
+ this.setFaviconHelper();
+ this.initDeploymentsPolling();
+
+ if (this.shouldRenderMergedPipeline) {
+ this.initPostMergeDeploymentsPolling();
+ }
+
+ this.initPolling();
+ this.bindEventHubListeners();
+ eventHub.$on('mr.discussion.updated', this.checkStatus);
+ },
getServiceEndpoints(store) {
return {
mergePath: store.mergePath,
@@ -319,7 +349,7 @@ export default {
};
</script>
<template>
- <div class="mr-state-widget prepend-top-default">
+ <div v-if="mr" class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" />
<mr-widget-pipeline-container
v-if="shouldRenderPipelines"
@@ -377,4 +407,5 @@ export default {
:is-post-merge="true"
/>
</div>
+ <loading v-else />
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index 8a229d80954..d22cb4ced80 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -61,4 +61,11 @@ export default class MRWidgetService {
static fetchMetrics(metricsUrl) {
return axios.get(`${metricsUrl}.json`);
}
+
+ static fetchInitialData() {
+ return Promise.all([
+ axios.get(window.gl.mrWidgetData.merge_request_cached_widget_path),
+ axios.get(window.gl.mrWidgetData.merge_request_widget_path),
+ ]).then(axios.spread((res, cachedRes) => ({ data: Object.assign(res.data, cachedRes.data) })));
+ }
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index f51d0fa4f52..c7949fa264e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -1,4 +1,4 @@
-import Timeago from 'timeago.js';
+import { format } from 'timeago.js';
import _ from 'underscore';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps';
@@ -42,12 +42,14 @@ export default class MergeRequestStore {
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
+ this.pipelineCoverageDelta = data.pipeline_coverage_delta;
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
this.commits = data.commits_without_merge_commits || [];
this.squashCommitMessage = data.default_squash_commit_message;
this.rebaseInProgress = data.rebase_in_progress;
+ this.mergeRequestDiffsPath = data.diffs_path;
if (data.issues_links) {
const links = data.issues_links;
@@ -81,6 +83,7 @@ export default class MergeRequestStore {
this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.isSHAMismatch = this.sha !== data.diff_head_sha;
+ this.latestSHA = data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
@@ -170,6 +173,8 @@ export default class MergeRequestStore {
this.conflictsDocsPath = data.conflicts_docs_path;
this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
this.securityApprovalsHelpPagePath = data.security_approvals_help_page_path;
+ this.eligibleApproversDocsPath = data.eligible_approvers_docs_path;
+ this.mergeImmediatelyDocsPath = data.merge_immediately_docs_path;
}
get isNothingToMergeState() {
@@ -213,9 +218,7 @@ export default class MergeRequestStore {
return '';
}
- const timeagoInstance = new Timeago();
-
- return timeagoInstance.format(date);
+ return format(date);
}
static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) {
diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue
index eabf5d4bf60..25d7bfe515c 100644
--- a/app/assets/javascripts/vue_shared/components/bar_chart.vue
+++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue
@@ -55,13 +55,13 @@ export default {
vbWidth: 0,
vbHeight: 0,
vpWidth: 0,
- vpHeight: 350,
- preserveAspectRatioType: 'xMidYMid meet',
+ vpHeight: 200,
+ preserveAspectRatioType: 'xMidYMin meet',
containerMargin: {
leftRight: 30,
},
viewBoxMargin: {
- topBottom: 150,
+ topBottom: 100,
},
panX: 0,
xScale: {},
@@ -274,6 +274,7 @@ export default {
<div ref="svgContainer" :class="activateGrabCursor" class="svg-graph-container">
<svg
ref="baseSvg"
+ class="svg-graph overflow-visible pt-5"
:width="vpWidth"
:height="vpHeight"
:viewBox="svgViewBox"
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index c50304f057d..eb3e489fb8c 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,8 +1,8 @@
<script>
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
import $ from 'jquery';
import { GlSkeletonLoading } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
const { CancelToken } = axios;
let axiosSource;
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index 22f370c4bca..494df2d7a37 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { GlLoadingIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
index c01c7cc4ccc..610bce9a705 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
@@ -8,6 +8,11 @@ export default {
required: true,
default: __('Search'),
},
+ focused: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return { searchQuery: this.value };
@@ -16,6 +21,11 @@ export default {
searchQuery(query) {
this.$emit('input', query);
},
+ focused(val) {
+ if (val) {
+ this.$refs.searchInput.focus();
+ }
+ },
},
};
</script>
@@ -23,6 +33,7 @@ export default {
<template>
<div class="dropdown-input">
<input
+ ref="searchInput"
v-model="searchQuery"
:placeholder="placeholderText"
class="dropdown-input-field"
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index d64ab774431..e2a6e92081f 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -1,4 +1,5 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
@@ -15,6 +16,7 @@ import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'ExpandButton',
components: {
+ GlButton,
Icon,
},
data() {
@@ -39,15 +41,25 @@ export default {
</script>
<template>
<span>
- <button
+ <gl-button
v-show="isCollapsed"
:aria-label="ariaLabel"
type="button"
- class="text-expander btn-blank"
+ class="js-text-expander-prepend text-expander btn-blank"
@click="onClick"
>
<icon :size="12" name="ellipsis_h" />
- </button>
+ </gl-button>
+ <span v-if="isCollapsed"> <slot name="short"></slot> </span>
<span v-if="!isCollapsed"> <slot name="expanded"></slot> </span>
+ <gl-button
+ v-show="!isCollapsed"
+ :aria-label="ariaLabel"
+ type="button"
+ class="js-text-expander-append text-expander btn-blank"
+ @click="onClick"
+ >
+ <icon :size="12" name="ellipsis_h" />
+ </gl-button>
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
index 4aae3549601..1769a283d8c 100644
--- a/app/assets/javascripts/vue_shared/components/gl_countdown.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
@@ -1,6 +1,6 @@
<script>
-import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility';
import { GlTooltipDirective } from '@gitlab/ui';
+import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility';
/**
* Counts down to a given end date.
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
index 9b2ee5062b1..cfbc5b0df3c 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -54,7 +54,7 @@ export default {
return timeFor(
this.milestoneDue,
sprintf(__('Expired %{expiredOn}'), {
- expiredOn: this.timeFormated(this.milestoneDue),
+ expiredOn: this.timeFormatted(this.milestoneDue),
}),
);
}
@@ -62,7 +62,7 @@ export default {
return sprintf(
this.isMilestoneStarted ? __('Started %{startsIn}') : __('Starts %{startsIn}'),
{
- startsIn: this.timeFormated(this.milestoneStart),
+ startsIn: this.timeFormatted(this.milestoneStart),
},
);
}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index cc700440a23..8a8cf09194c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -1,6 +1,6 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
export default {
components: { Icon, GlButton, GlLoadingIcon },
diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.vue b/app/assets/javascripts/vue_shared/components/memory_graph.vue
index 26d7d8e8866..af0b85cc6e4 100644
--- a/app/assets/javascripts/vue_shared/components/memory_graph.vue
+++ b/app/assets/javascripts/vue_shared/components/memory_graph.vue
@@ -1,128 +1,43 @@
<script>
-import { __, sprintf } from '~/locale';
-import { getTimeago } from '../../lib/utils/datetime_utility';
+import { GlSparklineChart } from '@gitlab/ui/dist/charts';
+import { formatDate, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
export default {
name: 'MemoryGraph',
+ components: {
+ GlSparklineChart,
+ },
props: {
metrics: { type: Array, required: true },
- deploymentTime: { type: Number, required: true },
- width: { type: String, required: true },
- height: { type: String, required: true },
- },
- data() {
- return {
- pathD: '',
- pathViewBox: '',
- dotX: '',
- dotY: '',
- };
+ width: { type: Number, required: true },
+ height: { type: Number, required: true },
},
computed: {
- getFormattedMedian() {
- const deployedSince = getTimeago().format(this.deploymentTime * 1000);
- return sprintf(__('Deployed %{deployedSince}'), { deployedSince });
+ chartData() {
+ return this.metrics.map(([x, y]) => [
+ this.getFormattedDeploymentTime(x),
+ this.getMemoryUsage(y),
+ ]);
},
},
- mounted() {
- this.renderGraph(this.deploymentTime, this.metrics);
- },
methods: {
- /**
- * Returns metric value index in metrics array
- * with timestamp closest to matching median
- */
- getMedianMetricIndex(median, metrics) {
- let matchIndex = 0;
- let timestampDiff = 0;
- let smallestDiff = 0;
-
- const metricTimestamps = metrics.map(v => v[0]);
-
- // Find metric timestamp which is closest to deploymentTime
- timestampDiff = Math.abs(metricTimestamps[0] - median);
- metricTimestamps.forEach((timestamp, index) => {
- if (index === 0) {
- // Skip first element
- return;
- }
-
- smallestDiff = Math.abs(timestamp - median);
- if (smallestDiff < timestampDiff) {
- matchIndex = index;
- timestampDiff = smallestDiff;
- }
- });
-
- return matchIndex;
+ getFormattedDeploymentTime(timestamp) {
+ return formatDate(new Date(secondsToMilliseconds(timestamp)), 'mmm dd yyyy HH:MM:s');
},
-
- /**
- * Get Graph Plotting values to render Line and Dot
- */
- getGraphPlotValues(median, metrics) {
- const renderData = metrics.map(v => v[1]);
- const medianMetricIndex = this.getMedianMetricIndex(median, metrics);
- let cx = 0;
- let cy = 0;
-
- // Find Maximum and Minimum values from `renderData` array
- const maxMemory = Math.max.apply(null, renderData);
- const minMemory = Math.min.apply(null, renderData);
-
- // Find difference between extreme ends
- const diff = maxMemory - minMemory;
- const lineWidth = renderData.length;
-
- // Iterate over metrics values and perform following
- // 1. Find x & y co-ords for deploymentTime's memory value
- // 2. Return line path against maxMemory
- const linePath = renderData.map((y, x) => {
- if (medianMetricIndex === x) {
- cx = x;
- cy = maxMemory - y;
- }
- return `${x} ${maxMemory - y}`;
- });
-
- return {
- pathD: linePath,
- pathViewBox: {
- lineWidth,
- diff,
- },
- dotX: cx,
- dotY: cy,
- };
- },
-
- /**
- * Render Graph based on provided median and metrics values
- */
- renderGraph(median, metrics) {
- const { pathD, pathViewBox, dotX, dotY } = this.getGraphPlotValues(median, metrics);
-
- // Set props and update graph on UI.
- this.pathD = `M ${pathD}`;
- this.pathViewBox = `0 0 ${pathViewBox.lineWidth} ${pathViewBox.diff}`;
- this.dotX = dotX;
- this.dotY = dotY;
+ getMemoryUsage(MBs) {
+ return Number(MBs).toFixed(2);
},
},
};
</script>
<template>
- <div class="memory-graph-container">
- <svg
- :title="getFormattedMedian"
- :width="width"
+ <div class="memory-graph-container p-1" :style="{ width: `${width}px` }">
+ <gl-sparkline-chart
:height="height"
- class="has-tooltip"
- xmlns="http://www.w3.org/2000/svg"
- >
- <path :d="pathD" :viewBox="pathViewBox" />
- <circle :cx="dotX" :cy="dotY" r="1.5" transform="translate(0 -1)" />
- </svg>
+ :tooltip-label="__('MB')"
+ :show-last-y-value="false"
+ :data="chartData"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
index edbeab9c600..cdcfff42981 100644
--- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -1,9 +1,9 @@
<script>
import $ from 'jquery';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import Clipboard from 'clipboard';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
-import Clipboard from 'clipboard';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index f8e010c4f42..15ca64ba297 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -19,9 +19,9 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui';
+import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
import noteHeader from '~/notes/components/note_header.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
import initMRPopovers from '~/mr_popover/';
diff --git a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
index e89638130f5..29a4a90a59f 100644
--- a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
@@ -1,15 +1,18 @@
<script>
+import { GlPagination } from '@gitlab/ui';
import {
- PAGINATION_UI_BUTTON_LIMIT,
- UI_LIMIT,
- SPREAD,
PREV,
NEXT,
- FIRST,
- LAST,
+ LABEL_FIRST_PAGE,
+ LABEL_PREV_PAGE,
+ LABEL_NEXT_PAGE,
+ LABEL_LAST_PAGE,
} from '~/vue_shared/components/pagination/constants';
export default {
+ components: {
+ GlPagination,
+ },
props: {
/**
This function will take the information given by the pagination component
@@ -46,113 +49,34 @@ export default {
},
},
computed: {
- prev() {
- return this.pageInfo.previousPage;
- },
- next() {
- return this.pageInfo.nextPage;
- },
- getItems() {
- const { totalPages, nextPage, previousPage, page } = this.pageInfo;
- const items = [];
-
- if (page > 1) {
- items.push({ title: FIRST, first: true });
- }
-
- if (previousPage) {
- items.push({ title: PREV, prev: true });
- } else {
- items.push({ title: PREV, disabled: true, prev: true });
- }
-
- if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
-
- if (totalPages) {
- const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
- const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, totalPages);
-
- for (let i = start; i <= end; i += 1) {
- const isActive = i === page;
- items.push({ title: i, active: isActive, page: true });
- }
-
- if (totalPages - page > PAGINATION_UI_BUTTON_LIMIT) {
- items.push({ title: SPREAD, separator: true, page: true });
- }
- }
-
- if (nextPage) {
- items.push({ title: NEXT, next: true });
- } else {
- items.push({ title: NEXT, disabled: true, next: true });
- }
-
- if (totalPages && totalPages - page >= 1) {
- items.push({ title: LAST, last: true });
- }
-
- return items;
- },
showPagination() {
return this.pageInfo.nextPage || this.pageInfo.previousPage;
},
},
- methods: {
- changePage(text, isDisabled) {
- if (isDisabled) return;
-
- const { totalPages, nextPage, previousPage } = this.pageInfo;
-
- switch (text) {
- case SPREAD:
- break;
- case LAST:
- this.change(totalPages);
- break;
- case NEXT:
- this.change(nextPage);
- break;
- case PREV:
- this.change(previousPage);
- break;
- case FIRST:
- this.change(1);
- break;
- default:
- this.change(Number(text));
- break;
- }
- },
- hideOnSmallScreen(item) {
- return !item.first && !item.last && !item.next && !item.prev && !item.active;
- },
- },
+ prevText: PREV,
+ nextText: NEXT,
+ labelFirstPage: LABEL_FIRST_PAGE,
+ labelPrevPage: LABEL_PREV_PAGE,
+ labelNextPage: LABEL_NEXT_PAGE,
+ labelLastPage: LABEL_LAST_PAGE,
};
</script>
<template>
- <div v-if="showPagination" class="gl-pagination prepend-top-default">
- <ul class="pagination justify-content-center">
- <li
- v-for="(item, index) in getItems"
- :key="index"
- :class="{
- page: item.page,
- 'js-previous-button': item.prev,
- 'js-next-button': item.next,
- 'js-last-button': item.last,
- 'js-first-button': item.first,
- 'd-none d-md-block': hideOnSmallScreen(item),
- separator: item.separator,
- active: item.active,
- disabled: item.disabled || item.separator,
- }"
- class="page-item"
- >
- <button type="button" class="page-link" @click="changePage(item.title, item.disabled)">
- {{ item.title }}
- </button>
- </li>
- </ul>
- </div>
+ <gl-pagination
+ v-if="showPagination"
+ class="justify-content-center prepend-top-default"
+ v-bind="$attrs"
+ :value="pageInfo.page"
+ :per-page="pageInfo.perPage"
+ :total-items="pageInfo.total"
+ :prev-page="pageInfo.previousPage"
+ :prev-text="$options.prevText"
+ :next-page="pageInfo.nextPage"
+ :next-text="$options.nextText"
+ :label-first-page="$options.labelFirstPage"
+ :label-prev-page="$options.labelPrevPage"
+ :label-next-page="$options.labelNextPage"
+ :label-last-page="$options.labelLastPage"
+ @input="change"
+ />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
index 43bbb756805..269736c799c 100644
--- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
+++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
@@ -14,8 +14,8 @@
/>
*/
-import { __ } from '~/locale';
import defaultAvatarUrl from 'images/no_avatar.png';
+import { __ } from '~/locale';
import { placeholderImage } from '../../../lazy_loader';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
index 071bae7f665..c472e54efda 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
@@ -1,10 +1,10 @@
<script>
import { GlButton } from '@gitlab/ui';
+import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
-import _ from 'underscore';
export default {
name: 'ProjectListItem',
diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue b/app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue
index 1f3d248e991..02cb7785ef4 100644
--- a/app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue
+++ b/app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue
@@ -1,6 +1,6 @@
<script>
-import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
import $ from 'jquery';
+import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
export default {
data() {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
index c1f3d86335a..80c61627b8f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
@@ -64,7 +64,7 @@ export default {
tooltipText(dateType = 'min') {
const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
const date = this[`${dateType}Date`];
- const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date);
+ const timeAgo = dateType === 'min' ? this.timeFormatted(date) : timeFor(date);
const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : '';
if (date) {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 13795eff714..0e401a9f7aa 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -1,10 +1,10 @@
<script>
import $ from 'jquery';
+import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue
index f7dc00a345c..9aacde49264 100644
--- a/app/assets/javascripts/vue_shared/components/split_button.vue
+++ b/app/assets/javascripts/vue_shared/components/split_button.vue
@@ -26,6 +26,11 @@ export default {
required: false,
default: '',
},
+ variant: {
+ type: String,
+ required: false,
+ default: 'secondary',
+ },
},
data() {
@@ -53,6 +58,7 @@ export default {
:menu-class="`dropdown-menu-selectable ${menuClass}`"
split
:text="dropdownToggleText"
+ :variant="variant"
v-bind="$attrs"
@click="triggerEvent"
>
diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
index af4eb2de7f8..ea564d1b2f2 100644
--- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
@@ -71,6 +71,10 @@ export default {
},
methods: {
getPercent(count) {
+ if (!this.totalCount) {
+ return 0;
+ }
+
const percent = roundOffFloat((count / this.totalCount) * 100, 1);
if (percent > 0 && percent < 1) {
return '< 1';
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index 43935cf31d5..b1a4f3dccaf 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -35,7 +35,7 @@ export default {
v-gl-tooltip.viewport="{ placement: tooltipPlacement }"
:class="cssClass"
:title="tooltipTitle(time)"
- v-text="timeFormated(time)"
+ v-text="timeFormatted(time)"
>
</time>
</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
index 3c727cb7b3f..fbebd7c7945 100644
--- a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
@@ -159,7 +159,7 @@ const mixins = {
return this.displayReference.split(this.pathIdSeparator).pop();
},
createdAtInWords() {
- return this.createdAt ? this.timeFormated(this.createdAt) : '';
+ return this.createdAt ? this.timeFormatted(this.createdAt) : '';
},
createdAtTimestamp() {
return this.createdAt ? formatDate(new Date(this.createdAt)) : '';
@@ -168,10 +168,10 @@ const mixins = {
return this.mergedAt ? formatDate(new Date(this.mergedAt)) : '';
},
mergedAtInWords() {
- return this.mergedAt ? this.timeFormated(this.mergedAt) : '';
+ return this.mergedAt ? this.timeFormatted(this.mergedAt) : '';
},
closedAtInWords() {
- return this.closedAt ? this.timeFormated(this.closedAt) : '';
+ return this.closedAt ? this.timeFormatted(this.closedAt) : '';
},
closedAtTimestamp() {
return this.closedAt ? formatDate(new Date(this.closedAt)) : '';
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
index 4e3b9d7b767..af14c6d9486 100644
--- a/app/assets/javascripts/vue_shared/mixins/timeago.js
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -5,7 +5,7 @@ import { formatDate, getTimeago } from '../../lib/utils/datetime_utility';
*/
export default {
methods: {
- timeFormated(time) {
+ timeFormatted(time) {
const timeago = getTimeago();
return timeago.format(time);
diff --git a/app/assets/stylesheets/components/release_block_milestone_info.scss b/app/assets/stylesheets/components/release_block_milestone_info.scss
new file mode 100644
index 00000000000..b6a85ae965a
--- /dev/null
+++ b/app/assets/stylesheets/components/release_block_milestone_info.scss
@@ -0,0 +1,6 @@
+.release-block-milestone-info {
+ .milestone-progress-bar-container {
+ width: 300px;
+ min-height: 46px;
+ }
+}
diff --git a/app/assets/stylesheets/components/toast.scss b/app/assets/stylesheets/components/toast.scss
deleted file mode 100644
index e27bf282247..00000000000
--- a/app/assets/stylesheets/components/toast.scss
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
-* These styles are specific to the gl-toast component.
-* Documentation: https://design.gitlab.com/components/toasts
-* Note: Styles below are nested in order to override some of vue-toasted's default styling
-*/
-.toasted-container {
-
- max-width: $toast-max-width;
-
- @include media-breakpoint-down(xs) {
- width: 100%;
- padding-right: $toast-padding-right;
- }
-
- .toasted.gl-toast {
- border-radius: $border-radius-default;
- font-size: $gl-font-size;
- padding: $gl-padding-8 $gl-padding $gl-padding-8 $gl-padding-24;
- margin-top: $toast-default-margin;
- line-height: $gl-line-height;
- background-color: rgba($gray-900, $toast-background-opacity);
-
- span {
- padding-right: $gl-padding-8;
- }
-
- @include media-breakpoint-down(xs) {
- .action:first-of-type {
- // Ensures actions buttons are right aligned on mobile
- margin-left: auto;
- }
- }
-
- .action {
- color: $blue-300;
- margin: 0 0 0 $toast-default-margin;
- text-transform: none;
- font-size: $gl-font-size;
- }
-
- .toast-close {
- font-size: $default-icon-size;
- margin-left: $toast-default-margin;
- }
- }
-}
-
-// Overrides the default positioning of toasts
-body .toasted-container.bottom-left {
- bottom: $toast-offset;
- left: $toast-offset;
-}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 31ea59df4c5..4b7dda3a2ff 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -433,6 +433,7 @@ img.emoji {
.block { display: block; }
.flex { display: flex; }
.vertical-align-top { vertical-align: top; }
+.vertical-align-text-top { vertical-align: text-top; }
.vertical-align-middle { vertical-align: middle; }
.vertical-align-sub { vertical-align: sub; }
.flex-align-self-center { align-self: center; }
@@ -442,6 +443,7 @@ img.emoji {
.ws-normal { white-space: normal; }
.ws-pre-wrap { white-space: pre-wrap; }
.overflow-auto { overflow: auto; }
+.overflow-visible { overflow: visible; }
.d-flex-center {
display: flex;
@@ -514,6 +516,12 @@ img.emoji {
cursor: pointer;
}
+// this needs to use "!important" due to some very specific styles
+// around buttons
+.cursor-default {
+ cursor: default !important;
+}
+
// Make buttons/dropdowns full-width on mobile
.full-width-mobile {
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d53a4c1286c..21253e004ef 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -288,7 +288,7 @@
list-style: none;
padding: 0 1px;
- a,
+ > a,
button,
.menu-item {
@include dropdown-link;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 4938215b2e7..8e0314bc6da 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -334,10 +334,6 @@ span.idiff {
padding: $gl-padding-8 $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
-
- &.is-stuck {
- border-radius: 0;
- }
}
.file-header-content {
@@ -490,3 +486,8 @@ span.idiff {
overflow-y: auto;
max-height: 20rem;
}
+
+#js-openapi-viewer pre.version {
+ background-color: transparent;
+ border: transparent;
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 2d826064569..1c252584047 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -214,8 +214,8 @@
padding-left: 0;
height: $input-height - 2;
line-height: inherit;
- border-color: transparent;
+ &,
&:focus,
&:hover {
outline: none;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 1195e467192..5ae4f72de56 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -552,6 +552,11 @@
svg {
vertical-align: text-top;
}
+
+ a.trial-link gl-emoji {
+ font-size: $gl-font-size;
+ vertical-align: baseline;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index ecd32dcd0ce..4aba633e182 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -196,6 +196,11 @@ ul.content-list {
display: flex;
align-items: center;
white-space: nowrap;
+
+ // Override style that allows the flex-row text to wrap.
+ &.allow-wrap {
+ white-space: normal;
+ }
}
.row-main-content {
diff --git a/app/assets/stylesheets/framework/memory_graph.scss b/app/assets/stylesheets/framework/memory_graph.scss
index c84010c6f10..06e1ebe41be 100644
--- a/app/assets/stylesheets/framework/memory_graph.scss
+++ b/app/assets/stylesheets/framework/memory_graph.scss
@@ -1,18 +1,4 @@
.memory-graph-container {
- svg {
- background: $white-light;
- border: 1px solid $gray-200;
- }
-
- path {
- fill: none;
- stroke: $blue-500;
- stroke-width: 2px;
- }
-
- circle {
- stroke: $blue-700;
- fill: $blue-700;
- stroke-width: 4px;
- }
+ background: $white-light;
+ border: 1px solid $gray-200;
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 2289f0a7011..bd0134a82d3 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -118,7 +118,7 @@
background: none;
.select2-search-field input {
- padding: 5px $gl-padding / 2;
+ padding: 5px $gl-input-padding;
height: auto;
font-family: inherit;
font-size: inherit;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index b9cfcf6ce5c..bf1fd7fd29f 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -61,10 +61,6 @@
padding-right: 0;
z-index: 300;
- .btn-sidebar-action {
- display: inline-flex;
- }
-
@include media-breakpoint-only(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter-collapsed-width;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 3876d1c10d4..39e7e4bb7e5 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -645,6 +645,12 @@ h4 {
}
}
+.text-right-md {
+ @include media-breakpoint-up(md) {
+ text-align: right;
+ }
+}
+
.text-right-lg {
@include media-breakpoint-up(lg) {
text-align: right;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 0f77c451fac..90600ecf615 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -62,6 +62,9 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c4c4c4;
+$purple: #6d49cb;
+$purple-light: #ede8fb;
+
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
@@ -529,16 +532,6 @@ $pagination-line-height: 20px;
$pagination-disabled-color: #cdcdcd;
/*
-* Toasts
-*/
-$toast-offset: 24px;
-$toast-height: 48px;
-$toast-max-width: 586px;
-$toast-padding-right: 42px;
-$toast-default-margin: 8px;
-$toast-background-opacity: 0.95;
-
-/*
* Status icons
*/
$status-icon-size: 22px;
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 434cbd6d21c..3eff1807403 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -3,7 +3,7 @@
color: $gl-text-color;
border: 1px solid $border-color;
border-radius: $border-radius-default;
- margin-bottom: $gl-padding;
+ margin-bottom: $gl-padding-8;
.card.card-body-segment {
padding: $gl-padding;
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index ba126d59eef..977fc8329b6 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -883,6 +883,15 @@ $ide-commit-header-height: 48px;
margin-right: $ide-tree-padding;
border-bottom: 1px solid $white-dark;
+ svg {
+ color: $gray-700;
+
+ &:focus,
+ &:hover {
+ color: $blue-600;
+ }
+ }
+
.ide-new-btn {
margin-left: auto;
}
@@ -899,6 +908,11 @@ $ide-commit-header-height: 48px;
.dropdown-menu-toggle {
svg {
vertical-align: middle;
+ color: $gray-700;
+
+ &:hover {
+ color: $gray-700;
+ }
}
&:hover {
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index d26979bc174..90c2e369ccd 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -187,6 +187,10 @@
font-size: 1em;
border-bottom: 1px solid $border-color;
padding: $gl-padding-8 $gl-padding;
+
+ .js-max-issue-size::before {
+ content: '/';
+ }
}
.board-title-text {
diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/dev_ops_score.scss
index 52fcdf4a405..6b6dce43dba 100644
--- a/app/assets/stylesheets/pages/convdev_index.scss
+++ b/app/assets/stylesheets/pages/dev_ops_score.scss
@@ -1,24 +1,24 @@
$space-between-cards: 8px;
-.convdev-empty svg {
+.devops-empty svg {
margin: 64px auto 32px;
max-width: 420px;
}
-.convdev-header {
+.devops-header {
margin-top: $gl-padding;
margin-bottom: $gl-padding;
padding: 0 4px;
display: flex;
align-items: center;
- .convdev-header-title {
+ .devops-header-title {
font-size: 48px;
line-height: 1;
margin: 0;
}
- .convdev-header-subtitle {
+ .devops-header-subtitle {
font-size: 22px;
line-height: 1;
color: $gl-text-color-secondary;
@@ -36,13 +36,13 @@ $space-between-cards: 8px;
}
}
-.convdev-cards {
+.devops-cards {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
-.convdev-card-wrapper {
+.devops-card-wrapper {
display: flex;
flex-direction: column;
align-items: stretch;
@@ -70,7 +70,7 @@ $space-between-cards: 8px;
}
}
-.convdev-card {
+.devops-card {
border: solid 1px $border-color;
border-radius: 3px;
border-top-width: 3px;
@@ -79,7 +79,7 @@ $space-between-cards: 8px;
flex-grow: 1;
}
-.convdev-card-low {
+.devops-card-low {
border-top-color: $red-400;
.board-card-score-big {
@@ -87,7 +87,7 @@ $space-between-cards: 8px;
}
}
-.convdev-card-average {
+.devops-card-average {
border-top-color: $orange-400;
.board-card-score-big {
@@ -95,7 +95,7 @@ $space-between-cards: 8px;
}
}
-.convdev-card-high {
+.devops-card-high {
border-top-color: $green-400;
.board-card-score-big {
@@ -103,7 +103,7 @@ $space-between-cards: 8px;
}
}
-.convdev-card-title {
+.devops-card-title {
margin: $gl-padding auto auto;
max-width: 100px;
@@ -170,7 +170,7 @@ $space-between-cards: 8px;
}
}
-.convdev-steps {
+.devops-steps {
margin-top: $gl-padding;
height: 1px;
min-width: 100%;
@@ -179,7 +179,7 @@ $space-between-cards: 8px;
background: $border-color;
}
-.convdev-step {
+.devops-step {
$step-positions: 5% 10% 30% 42% 48% 55% 60% 70% 75% 90%;
@each $pos in $step-positions {
$i: index($step-positions, $pos);
@@ -212,7 +212,7 @@ $space-between-cards: 8px;
height: auto;
width: auto;
- .convdev-step-title {
+ .devops-step-title {
max-height: 2em;
opacity: 1;
transition: opacity 0.2s;
@@ -233,7 +233,7 @@ $space-between-cards: 8px;
}
}
-.convdev-step-title {
+.devops-step-title {
max-height: 0;
opacity: 0;
text-transform: uppercase;
@@ -242,14 +242,14 @@ $space-between-cards: 8px;
font-size: 12px;
}
-.convdev-high-score {
+.devops-high-score {
color: $green-400;
}
-.convdev-average-score {
+.devops-average-score {
color: $orange-400;
}
-.convdev-low-score {
+.devops-low-score {
color: $red-400;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index defa1a6c0d5..f394e4ab58a 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -10,6 +10,7 @@
.file-title-flex-parent {
border-top-left-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
+ box-shadow: 0 -2px 0 0 var(--white);
cursor: pointer;
@media (min-width: map-get($grid-breakpoints, md)) {
@@ -472,6 +473,7 @@ table.code {
text-align: right;
width: 50px;
position: relative;
+ white-space: nowrap;
a {
transition: none;
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 65d0ce8c52e..b716c6e14fe 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -172,7 +172,7 @@
}
.template-selector-dropdowns-wrap {
- display: inline-block;
+ display: flex;
vertical-align: top;
@media(max-width: map-get($grid-breakpoints, lg)-1) {
@@ -189,6 +189,7 @@
display: inline-block;
vertical-align: top;
font-family: $regular_font;
+ margin: 0 8px 0 0;
@media(max-width: map-get($grid-breakpoints, lg)-1) {
display: block;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 93dffb5ff09..3892d9dbd07 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -7,7 +7,6 @@
.environments-folder-name {
font-weight: $gl-font-weight-normal;
- padding-top: 20px;
}
.environments-container {
diff --git a/app/assets/stylesheets/pages/error_details.scss b/app/assets/stylesheets/pages/error_details.scss
index 0515db914e9..dcd25c126c4 100644
--- a/app/assets/stylesheets/pages/error_details.scss
+++ b/app/assets/stylesheets/pages/error_details.scss
@@ -12,6 +12,12 @@
}
}
+ .file-title-name {
+ &.limited-width {
+ max-width: 80%;
+ }
+ }
+
.line_content.old::before {
content: none !important;
}
diff --git a/app/assets/stylesheets/pages/error_tracking_list.scss b/app/assets/stylesheets/pages/error_tracking_list.scss
new file mode 100644
index 00000000000..cd1adb9a754
--- /dev/null
+++ b/app/assets/stylesheets/pages/error_tracking_list.scss
@@ -0,0 +1,5 @@
+.error-list {
+ .sort-dropdown {
+ min-width: auto;
+ }
+}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 1502cf18440..1cf72c51ca7 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -25,6 +25,7 @@
.description p {
margin-bottom: 0;
+ color: $gl-text-color-secondary;
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 5617ab0af41..09b335f9ba2 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -883,7 +883,7 @@
.time-tracking-help-state {
background: $white-light;
- margin: 16px -20px 0;
+ margin: 16px -20px -20px;
padding: 16px 20px;
border-top: 1px solid $border-gray-light;
border-bottom: 1px solid $border-gray-light;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index a37cbda8558..b03ad5c6b75 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -21,16 +21,11 @@
margin-bottom: 2px;
}
- .issue-labels {
+ .issue-labels,
+ .author-link {
display: inline-block;
}
- .issuable-meta {
- .author-link {
- display: inline-block;
- }
- }
-
.icon-merge-request-unmerged {
height: 13px;
margin-bottom: 3px;
@@ -53,16 +48,6 @@
margin-right: 15px;
}
-.issues_content {
- .title {
- height: 40px;
- }
-
- form {
- margin: 0;
- }
-}
-
form.edit-issue {
margin: 0;
}
@@ -79,10 +64,6 @@ ul.related-merge-requests > li {
margin-left: 5px;
}
- .row_title {
- vertical-align: bottom;
- }
-
gl-emoji {
font-size: 1em;
}
@@ -93,10 +74,6 @@ ul.related-merge-requests > li {
font-weight: $gl-font-weight-bold;
}
-.merge-request-id {
- display: inline-block;
-}
-
.merge-request-status {
&.merged {
color: $blue-500;
@@ -118,11 +95,7 @@ ul.related-merge-requests > li {
border-color: $issues-today-border;
}
- &.closed {
- background: $gray-light;
- border-color: $border-color;
- }
-
+ &.closed,
&.merged {
background: $gray-light;
border-color: $border-color;
@@ -160,9 +133,12 @@ ul.related-merge-requests > li {
padding-bottom: 37px;
}
-.issues-nav-controls {
+.issues-nav-controls,
+.new-branch-col {
font-size: 0;
+}
+.issues-nav-controls {
.btn-group:empty {
display: none;
}
@@ -198,8 +174,6 @@ ul.related-merge-requests > li {
}
.new-branch-col {
- font-size: 0;
-
.discussion-filter-container {
&:not(:only-child) {
margin-right: $gl-padding-8;
@@ -240,7 +214,6 @@ ul.related-merge-requests > li {
}
.create-merge-request-dropdown-menu {
- width: 300px;
opacity: 1;
visibility: visible;
transform: translateY(0);
@@ -297,11 +270,11 @@ ul.related-merge-requests > li {
padding-top: 0;
align-self: center;
}
+ }
- .create-mr-dropdown-wrap {
- .btn-group:not(.hidden) {
- display: inline-flex;
- }
+ .create-mr-dropdown-wrap {
+ .btn-group:not(.hidden) {
+ display: inline-flex;
}
}
}
diff --git a/app/assets/stylesheets/pages/issues/issue_count_badge.scss b/app/assets/stylesheets/pages/issues/issue_count_badge.scss
index 64ca61f7094..569f323abd8 100644
--- a/app/assets/stylesheets/pages/issues/issue_count_badge.scss
+++ b/app/assets/stylesheets/pages/issues/issue_count_badge.scss
@@ -2,7 +2,6 @@
.mr-count-badge {
display: inline-flex;
border-radius: $border-radius-base;
- border: 1px solid $border-color;
padding: 5px $gl-padding-8;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 971f3b2c308..c023c9e5cbd 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -51,6 +51,10 @@
position: relative;
border: 1px solid $border-color;
border-radius: $border-radius-default;
+
+ .gl-skeleton-loader {
+ display: block;
+ }
}
.mr-widget-extension {
@@ -949,7 +953,6 @@
.deployment-info {
flex: 1;
white-space: nowrap;
- overflow: hidden;
text-overflow: ellipsis;
min-width: 100px;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 87cef43b923..08796742f08 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -17,12 +17,6 @@
display: inline-block;
}
-.account-btn-link,
-.profile-settings-sidebar a,
-.settings-sidebar a {
- color: $blue-600;
-}
-
.private-tokens-reset div.reset-action:not(:first-child) {
padding-top: 15px;
}
@@ -122,24 +116,12 @@
float: left;
}
}
-
- .description {
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- }
}
.key-created-at {
line-height: 42px;
}
-.profile-settings-content {
- a {
- color: $blue-600;
- }
-}
-
.provider-btn-group {
display: inline-block;
margin-right: 10px;
@@ -169,10 +151,6 @@
margin-left: -3px;
line-height: 22px;
background-color: $gray-light;
-
- &.not-active {
- color: $blue-500;
- }
}
.oauth-applications {
@@ -292,10 +270,6 @@ table.u2f-registrations {
}
.oauth-application-show {
- .scope-name {
- font-weight: $gl-font-weight-bold;
- }
-
.scopes-list {
padding-left: 18px;
}
@@ -317,52 +291,52 @@ table.u2f-registrations {
.landing {
padding: 32px;
+ }
- .close {
- position: absolute;
- top: 20px;
- right: 20px;
- opacity: 1;
+ .close {
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ opacity: 1;
- .dismiss-icon {
- float: right;
- cursor: pointer;
- color: $blue-300;
- }
+ .dismiss-icon {
+ float: right;
+ cursor: pointer;
+ color: $blue-300;
+ }
- &:hover {
- background-color: transparent;
- border: 0;
+ &:hover {
+ background-color: transparent;
+ border: 0;
- .dismiss-icon {
- color: $blue-400;
- }
+ .dismiss-icon {
+ color: $blue-400;
}
}
+ }
- .svg-container {
- margin-right: 30px;
- display: inline-block;
+ .svg-container {
+ margin-right: 30px;
+ display: inline-block;
- svg {
- height: 110px;
- vertical-align: top;
- }
+ svg {
+ height: 110px;
+ vertical-align: top;
+ }
- &.convdev {
- margin: 0 0 0 30px;
+ &.convdev {
+ margin: 0 0 0 30px;
- svg {
- height: 127px;
- }
+ svg {
+ height: 127px;
}
}
+ }
- .user-callout-copy {
- display: inline-block;
- vertical-align: top;
- max-width: 570px;
- }
+ .user-callout-copy {
+ display: inline-block;
+ vertical-align: top;
+ max-width: 570px;
}
@include media-breakpoint-down(xs) {
@@ -372,43 +346,26 @@ table.u2f-registrations {
display: block;
}
- .landing {
- .svg-container,
- .user-callout-copy {
- margin: 0 auto;
- display: block;
+ .svg-container,
+ .user-callout-copy {
+ margin: 0 auto;
+ display: block;
- svg {
- height: 75px;
- }
+ svg {
+ height: 75px;
+ }
- &.convdev {
- margin: $gl-padding auto 0;
+ &.convdev {
+ margin: $gl-padding auto 0;
- svg {
- height: 120px;
- }
+ svg {
+ height: 120px;
}
}
}
}
}
-.nav-wip {
- border: 1px solid $blue-500;
- background: $blue-50;
- padding: $gl-padding;
- margin-bottom: $gl-padding;
-
- a {
- color: $blue-500;
- }
-
- p:last-child {
- margin-bottom: 0;
- }
-}
-
.email-badge {
display: inline;
margin-right: $gl-padding / 2;
@@ -433,10 +390,8 @@ table.u2f-registrations {
}
.edit-user {
- .clear-user-status {
- svg {
- fill: $gl-text-color-secondary;
- }
+ svg {
+ fill: $gl-text-color-secondary;
}
.form-group > label {
@@ -453,10 +408,6 @@ table.u2f-registrations {
.no-emoji-placeholder {
position: relative;
-
- svg {
- fill: $gl-text-color-secondary;
- }
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 17a446fca53..8b2c67378d9 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -792,7 +792,7 @@
}
.btn {
- margin-top: $gl-padding;
+ margin-top: $gl-padding-8;
padding: $gl-btn-vert-padding $gl-btn-padding;
line-height: $gl-btn-line-height;
@@ -812,6 +812,10 @@
@extend .btn;
@extend .btn-default;
}
+
+ .nav > li:not(:last-child) {
+ margin-right: $gl-padding-8;
+ }
}
.repository-languages-bar {
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index 154e505f7a4..e20e58e21cf 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -67,7 +67,6 @@
.prometheus-graph-group {
display: flex;
flex-wrap: wrap;
- margin-top: $gl-padding-8;
}
.prometheus-graph {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 5664f46484e..79ad0bd7735 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -1,6 +1,6 @@
.tree-holder {
.nav-block {
- margin: 10px 0;
+ margin: 16px 0;
.btn .fa,
.btn svg {
@@ -17,6 +17,10 @@
.tree-controls {
text-align: right;
+ .btn {
+ margin-left: 8px;
+ }
+
.btn-group {
margin-left: 10px;
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 3b3a2778b23..1f4bba5fc33 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -29,5 +29,25 @@
.border-color-default { border-color: $border-color; }
.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; }
+.mh-50vh { max-height: 50vh; }
+
.gl-w-64 { width: px-to-rem($grid-size * 8); }
.gl-h-64 { height: px-to-rem($grid-size * 8); }
+
+.gl-text-purple { color: $purple; }
+.gl-text-gray-800 { color: $gray-800; }
+.gl-bg-purple-light { background-color: $purple-light; }
+
+// Classes using mixins coming from @gitlab-ui
+// can be removed once https://gitlab.com/gitlab-org/gitlab/merge_requests/19021 has been merged
+.gl-bg-red-100 { @include gl-bg-red-100; }
+.gl-bg-orange-100 { @include gl-bg-orange-100; }
+.gl-bg-gray-100 { @include gl-bg-gray-100; }
+.gl-bg-green-100 { @include gl-bg-green-100;}
+
+.gl-text-blue-500 { @include gl-text-blue-500; }
+.gl-text-gray-900 { @include gl-text-gray-900; }
+.gl-text-red-700 { @include gl-text-red-700; }
+.gl-text-orange-700 { @include gl-text-orange-700; }
+.gl-text-green-700 { @include gl-text-green-700; }
+