summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/icons.json2
-rw-r--r--app/assets/images/icons.svg2
-rw-r--r--app/assets/images/illustrations/canceled-job_empty.svg1
-rw-r--r--app/assets/images/illustrations/epics.svg2
-rw-r--r--app/assets/images/illustrations/epics/list.svg1
-rw-r--r--app/assets/images/illustrations/epics/roadmap.svg1
-rw-r--r--app/assets/images/illustrations/issue-dashboard_results-without-filiter.svg1
-rw-r--r--app/assets/images/illustrations/lock_promotion.svg1
-rw-r--r--app/assets/images/illustrations/milestone_removing-page.svg1
-rw-r--r--app/assets/images/illustrations/profile-page/activity.svg1
-rw-r--r--app/assets/images/illustrations/profile-page/contributed-projects.svg1
-rw-r--r--app/assets/images/illustrations/profile-page/groups.svg1
-rw-r--r--app/assets/images/illustrations/profile-page/personal-project.svg1
-rw-r--r--app/assets/images/illustrations/profile-page/personal-projects.svg1
-rw-r--r--app/assets/images/illustrations/profile-page/snippets.svg1
-rw-r--r--app/assets/images/illustrations/prometheus-graphs_empty.svg1
-rw-r--r--app/assets/images/illustrations/web-ide_promotion.svg1
-rw-r--r--app/assets/javascripts/autosave.js32
-rw-r--r--app/assets/javascripts/awards_handler.js16
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js4
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js4
-rw-r--r--app/assets/javascripts/blob/notebook_viewer.js2
-rw-r--r--app/assets/javascripts/blob/pdf_viewer.js2
-rw-r--r--app/assets/javascripts/blob/sketch_viewer.js4
-rw-r--r--app/assets/javascripts/blob/stl_viewer.js4
-rw-r--r--app/assets/javascripts/blob/viewer/index.js33
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue5
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue (renamed from app/assets/javascripts/boards/components/board_new_issue.js)81
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js14
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue127
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js8
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js5
-rw-r--r--app/assets/javascripts/boards/index.js17
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js13
-rw-r--r--app/assets/javascripts/boards/models/issue.js12
-rw-r--r--app/assets/javascripts/boards/models/project.js6
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js4
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue6
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue162
-rw-r--r--app/assets/javascripts/clusters/constants.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js10
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js4
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue38
-rw-r--r--app/assets/javascripts/commons/vue.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js6
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js1
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js15
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js5
-rw-r--r--app/assets/javascripts/dispatcher.js237
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js4
-rw-r--r--app/assets/javascripts/environments/index.js (renamed from app/assets/javascripts/environments/environments_bundle.js)4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js10
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js29
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js22
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js23
-rw-r--r--app/assets/javascripts/groups/components/app.vue10
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue65
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue35
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue36
-rw-r--r--app/assets/javascripts/ide/components/ide.vue99
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue108
-rw-r--r--app/assets/javascripts/ide/components/ide_project_branches_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_project_tree.vue49
-rw-r--r--app/assets/javascripts/ide/components/ide_repo_tree.vue74
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue114
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue66
-rw-r--r--app/assets/javascripts/ide/components/new_branch_form.vue108
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue101
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue112
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue87
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue171
-rw-r--r--app/assets/javascripts/ide/components/repo_edit_button.vue57
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue136
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue165
-rw-r--r--app/assets/javascripts/ide/components/repo_file_buttons.vue60
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue42
-rw-r--r--app/assets/javascripts/ide/components/repo_prev_directory.vue32
-rw-r--r--app/assets/javascripts/ide/components/repo_preview.vue71
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue74
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue27
-rw-r--r--app/assets/javascripts/ide/ide_router.js101
-rw-r--r--app/assets/javascripts/ide/index.js31
-rw-r--r--app/assets/javascripts/ide/lib/common/disposable.js14
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js64
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js32
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js43
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js71
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff.js30
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js10
-rw-r--r--app/assets/javascripts/ide/lib/editor.js110
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js2
-rw-r--r--app/assets/javascripts/ide/monaco_loader.js16
-rw-r--r--app/assets/javascripts/ide/services/index.js47
-rw-r--r--app/assets/javascripts/ide/stores/actions.js196
-rw-r--r--app/assets/javascripts/ide/stores/actions/branch.js43
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js137
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js27
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js188
-rw-r--r--app/assets/javascripts/ide/stores/getters.js19
-rw-r--r--app/assets/javascripts/ide/stores/index.js15
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js46
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js70
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js28
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js74
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js23
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js36
-rw-r--r--app/assets/javascripts/ide/stores/state.js23
-rw-r--r--app/assets/javascripts/ide/stores/utils.js177
-rw-r--r--app/assets/javascripts/importer_status.js31
-rw-r--r--app/assets/javascripts/labels_select.js46
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js29
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js14
-rw-r--r--app/assets/javascripts/main.js7
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js4
-rw-r--r--app/assets/javascripts/merge_request_tabs.js4
-rw-r--r--app/assets/javascripts/milestone_select.js3
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue86
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue17
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue78
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue16
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js23
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js3
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js2
-rw-r--r--app/assets/javascripts/mr_notes/index.js41
-rw-r--r--app/assets/javascripts/network/network_bundle.js17
-rw-r--r--app/assets/javascripts/notes.js138
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue77
-rw-r--r--app/assets/javascripts/notes/components/diff_file_header.vue94
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue96
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue119
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue61
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue43
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue13
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue198
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue17
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue43
-rw-r--r--app/assets/javascripts/notes/constants.js6
-rw-r--r--app/assets/javascripts/notes/index.js12
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js5
-rw-r--r--app/assets/javascripts/notes/mixins/noteable.js22
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js50
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js7
-rw-r--r--app/assets/javascripts/notes/stores/actions.js17
-rw-r--r--app/assets/javascripts/notes/stores/getters.js36
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js43
-rw-r--r--app/assets/javascripts/notes/stores/utils.js1
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js4
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/cohorts/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/groups/show/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/labels/new/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/projects/index.js4
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js6
-rw-r--r--app/assets/javascripts/pages/groups/boards/index.js9
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue64
-rw-r--r--app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js84
-rw-r--r--app/assets/javascripts/pages/milestones/shared/index.js89
-rw-r--r--app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js82
-rw-r--r--app/assets/javascripts/pages/profiles/index.js16
-rw-r--r--app/assets/javascripts/pages/profiles/index/index.js4
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js (renamed from app/assets/javascripts/two_factor_auth.js)2
-rw-r--r--app/assets/javascripts/pages/projects/commit/pipelines/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/compare/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/cycle_analytics/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/environments/folder/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/environments/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/environments/terminal/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js13
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js14
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue79
-rw-r--r--app/assets/javascripts/pages/projects/labels/event_hub.js3
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js90
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js32
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js33
-rw-r--r--app/assets/javascripts/pages/projects/network/network.js (renamed from app/assets/javascripts/network/network.js)2
-rw-r--r--app/assets/javascripts/pages/projects/network/show/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/builds/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/index/index.js40
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/registry/repositories/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/snippets/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/snippets/new/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js4
-rw-r--r--app/assets/javascripts/pages/search/init_filtered_search.js18
-rw-r--r--app/assets/javascripts/pages/snippets/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/snippets/new/index.js6
-rw-r--r--app/assets/javascripts/pipelines/components/blank_state.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue49
-rw-r--r--app/assets/javascripts/pipelines/components/error_state.vue26
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue92
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue271
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js31
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js4
-rw-r--r--app/assets/javascripts/pipelines/pipelines_bundle.js27
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js1
-rw-r--r--app/assets/javascripts/profile/profile.js150
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js2
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js59
-rw-r--r--app/assets/javascripts/protected_branches/index.js9
-rw-r--r--app/assets/javascripts/protected_tags/index.js9
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue1
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue1
-rw-r--r--app/assets/javascripts/registry/index.js4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.js224
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue232
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue (renamed from app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js)91
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js2
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js8
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js13
-rw-r--r--app/assets/javascripts/sortable/sortable_config.js7
-rw-r--r--app/assets/javascripts/terminal/index.js (renamed from app/assets/javascripts/terminal/terminal_bundle.js)2
-rw-r--r--app/assets/javascripts/terminal/terminal.js10
-rw-r--r--app/assets/javascripts/test.js1
-rw-r--r--app/assets/javascripts/u2f/authenticate.js26
-rw-r--r--app/assets/javascripts/u2f/register.js27
-rw-r--r--app/assets/javascripts/u2f/util.js42
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js)87
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue34
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/memory_graph.vue (renamed from app/assets/javascripts/vue_shared/components/memory_graph.js)38
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue149
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue78
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue84
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue63
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue48
-rw-r--r--app/assets/javascripts/vue_shared/models/label.js (renamed from app/assets/javascripts/boards/models/label.js)4
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/framework/flash.scss6
-rw-r--r--app/assets/stylesheets/framework/modal.scss9
-rw-r--r--app/assets/stylesheets/pages/commits.scss8
-rw-r--r--app/assets/stylesheets/pages/environments.scss3
-rw-r--r--app/assets/stylesheets/pages/issuable.scss7
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/assets/stylesheets/pages/settings.scss3
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb1
-rw-r--r--app/controllers/application_controller.rb12
-rw-r--r--app/controllers/boards/issues_controller.rb15
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb1
-rw-r--r--app/controllers/concerns/boards_responses.rb44
-rw-r--r--app/controllers/concerns/creates_commit.rb8
-rw-r--r--app/controllers/concerns/issuable_actions.rb14
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/concerns/membership_actions.rb64
-rw-r--r--app/controllers/concerns/notes_actions.rb14
-rw-r--r--app/controllers/groups/application_controller.rb4
-rw-r--r--app/controllers/groups/boards_controller.rb27
-rw-r--r--app/controllers/groups/group_members_controller.rb29
-rw-r--r--app/controllers/groups/labels_controller.rb25
-rw-r--r--app/controllers/groups_controller.rb1
-rw-r--r--app/controllers/ide_controller.rb6
-rw-r--r--app/controllers/import/bitbucket_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb18
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb22
-rw-r--r--app/controllers/profiles/passwords_controller.rb1
-rw-r--r--app/controllers/projects/application_controller.rb12
-rw-r--r--app/controllers/projects/blob_controller.rb10
-rw-r--r--app/controllers/projects/branches_controller.rb41
-rw-r--r--app/controllers/projects/clusters_controller.rb5
-rw-r--r--app/controllers/projects/commits_controller.rb44
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/controllers/projects/deployments_controller.rb2
-rw-r--r--app/controllers/projects/discussions_controller.rb38
-rw-r--r--app/controllers/projects/issues_controller.rb14
-rw-r--r--app/controllers/projects/labels_controller.rb8
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/controllers/projects/milestones_controller.rb14
-rw-r--r--app/controllers/projects/network_controller.rb25
-rw-r--r--app/controllers/projects/notes_controller.rb30
-rw-r--r--app/controllers/projects/pages_domains_controller.rb29
-rw-r--r--app/controllers/projects/project_members_controller.rb29
-rw-r--r--app/controllers/projects/prometheus/metrics_controller.rb11
-rw-r--r--app/controllers/projects/services_controller.rb33
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb10
-rw-r--r--app/controllers/projects/tree_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb16
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/finders/branches_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb12
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/labels_finder.rb14
-rw-r--r--app/finders/merge_requests_finder.rb32
-rw-r--r--app/finders/notes_finder.rb12
-rw-r--r--app/finders/snippets_finder.rb28
-rw-r--r--app/finders/todos_finder.rb13
-rw-r--r--app/finders/user_recent_events_finder.rb45
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/application_settings_helper.rb2
-rw-r--r--app/helpers/auth_helper.rb6
-rw-r--r--app/helpers/blob_helper.rb14
-rw-r--r--app/helpers/boards_helper.rb28
-rw-r--r--app/helpers/branches_helper.rb11
-rw-r--r--app/helpers/form_helper.rb2
-rw-r--r--app/helpers/groups_helper.rb20
-rw-r--r--app/helpers/import_helper.rb76
-rw-r--r--app/helpers/issuables_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb1
-rw-r--r--app/helpers/merge_requests_helper.rb13
-rw-r--r--app/helpers/notes_helper.rb33
-rw-r--r--app/helpers/profiles_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb2
-rw-r--r--app/helpers/tree_helper.rb21
-rw-r--r--app/helpers/u2f_helper.rb5
-rw-r--r--app/mailers/notify.rb2
-rw-r--r--app/models/badge.rb51
-rw-r--r--app/models/badges/group_badge.rb5
-rw-r--r--app/models/badges/project_badge.rb15
-rw-r--r--app/models/board.rb8
-rw-r--r--app/models/chat_name.rb21
-rw-r--r--app/models/ci/group_variable.rb5
-rw-r--r--app/models/ci/runner.rb4
-rw-r--r--app/models/ci/variable.rb5
-rw-r--r--app/models/clusters/applications/helm.rb2
-rw-r--r--app/models/clusters/applications/ingress.rb28
-rw-r--r--app/models/clusters/applications/prometheus.rb15
-rw-r--r--app/models/clusters/applications/runner.rb69
-rw-r--r--app/models/clusters/cluster.rb10
-rw-r--r--app/models/clusters/concerns/application_core.rb5
-rw-r--r--app/models/clusters/concerns/application_data.rb23
-rw-r--r--app/models/commit.rb13
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/access_requestable.rb2
-rw-r--r--app/models/concerns/deployment_platform.rb3
-rw-r--r--app/models/concerns/issuable.rb5
-rw-r--r--app/models/concerns/prometheus_adapter.rb48
-rw-r--r--app/models/concerns/updated_at_filterable.rb12
-rw-r--r--app/models/cycle_analytics.rb6
-rw-r--r--app/models/cycle_analytics/summary.rb0
-rw-r--r--app/models/deployment.rb17
-rw-r--r--app/models/environment.rb22
-rw-r--r--app/models/event.rb10
-rw-r--r--app/models/group.rb3
-rw-r--r--app/models/identity.rb6
-rw-r--r--app/models/label.rb1
-rw-r--r--app/models/lfs_object.rb4
-rw-r--r--app/models/member.rb15
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/merge_request.rb40
-rw-r--r--app/models/namespace.rb5
-rw-r--r--app/models/network/commit.rb7
-rw-r--r--app/models/note.rb10
-rw-r--r--app/models/project.rb135
-rw-r--r--app/models/project_services/asana_service.rb2
-rw-r--r--app/models/project_services/campfire_service.rb2
-rw-r--r--app/models/project_services/chat_notification_service.rb14
-rw-r--r--app/models/project_services/hipchat_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb6
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb2
-rw-r--r--app/models/project_services/monitoring_service.rb4
-rw-r--r--app/models/project_services/prometheus_service.rb89
-rw-r--r--app/models/project_services/pushover_service.rb4
-rw-r--r--app/models/project_services/slash_commands_service.rb6
-rw-r--r--app/models/project_team.rb9
-rw-r--r--app/models/repository.rb112
-rw-r--r--app/models/service.rb11
-rw-r--r--app/models/snippet.rb4
-rw-r--r--app/models/todo.rb14
-rw-r--r--app/models/user.rb53
-rw-r--r--app/models/user_interacted_project.rb59
-rw-r--r--app/models/user_synced_attributes_metadata.rb2
-rw-r--r--app/policies/group_policy.rb7
-rw-r--r--app/policies/project_policy.rb14
-rw-r--r--app/presenters/merge_request_presenter.rb19
-rw-r--r--app/serializers/analytics_stage_entity.rb3
-rw-r--r--app/serializers/cluster_application_entity.rb1
-rw-r--r--app/serializers/diff_file_entity.rb41
-rw-r--r--app/serializers/discussion_entity.rb38
-rw-r--r--app/serializers/merge_request_widget_entity.rb20
-rw-r--r--app/serializers/note_entity.rb12
-rw-r--r--app/services/badges/base_service.rb11
-rw-r--r--app/services/badges/build_service.rb12
-rw-r--r--app/services/badges/create_service.rb10
-rw-r--r--app/services/badges/update_service.rb12
-rw-r--r--app/services/boards/issues/list_service.rb6
-rw-r--r--app/services/boards/issues/move_service.rb4
-rw-r--r--app/services/boards/lists/create_service.rb6
-rw-r--r--app/services/chat_names/find_user_service.rb4
-rw-r--r--app/services/ci/create_pipeline_service.rb2
-rw-r--r--app/services/ci/create_trace_artifact_service.rb36
-rw-r--r--app/services/clusters/applications/check_ingress_ip_address_service.rb36
-rw-r--r--app/services/issuable_base_service.rb6
-rw-r--r--app/services/members/approve_access_request_service.rb45
-rw-r--r--app/services/members/authorized_destroy_service.rb61
-rw-r--r--app/services/members/base_service.rb49
-rw-r--r--app/services/members/create_service.rb15
-rw-r--r--app/services/members/destroy_service.rb41
-rw-r--r--app/services/members/request_access_service.rb13
-rw-r--r--app/services/members/update_service.rb16
-rw-r--r--app/services/merge_requests/base_service.rb19
-rw-r--r--app/services/merge_requests/build_service.rb1
-rw-r--r--app/services/merge_requests/conflicts/list_service.rb10
-rw-r--r--app/services/merge_requests/create_service.rb6
-rw-r--r--app/services/merge_requests/update_service.rb11
-rw-r--r--app/services/notes/build_service.rb9
-rw-r--r--app/services/notes/post_process_service.rb2
-rw-r--r--app/services/notes/quick_actions_service.rb8
-rw-r--r--app/services/notification_recipient_service.rb2
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/services/projects/update_pages_service.rb35
-rw-r--r--app/services/projects/update_service.rb15
-rw-r--r--app/services/prometheus/adapter_service.rb36
-rw-r--r--app/services/quick_actions/interpret_service.rb6
-rw-r--r--app/services/system_hooks_service.rb6
-rw-r--r--app/services/todo_service.rb3
-rw-r--r--app/services/users/destroy_service.rb2
-rw-r--r--app/validators/url_placeholder_validator.rb32
-rw-r--r--app/validators/variable_duplicates_validator.rb2
-rw-r--r--app/views/admin/application_settings/_form.html.haml22
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml4
-rw-r--r--app/views/admin/hooks/_form.html.haml42
-rw-r--r--app/views/admin/hooks/index.html.haml58
-rw-r--r--app/views/admin/identities/_form.html.haml2
-rw-r--r--app/views/admin/identities/_identity.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml6
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml1
-rw-r--r--app/views/admin/runners/show.html.haml6
-rw-r--r--app/views/admin/users/projects.html.haml4
-rw-r--r--app/views/devise/sessions/two_factor.html.haml4
-rw-r--r--app/views/groups/boards/index.html.haml1
-rw-r--r--app/views/groups/boards/show.html.haml1
-rw-r--r--app/views/groups/group_members/update.js.haml4
-rw-r--r--app/views/groups/issues.html.haml10
-rw-r--r--app/views/groups/labels/index.html.haml6
-rw-r--r--app/views/groups/merge_requests.html.haml5
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/ide/index.html.haml11
-rw-r--r--app/views/import/_githubish_status.html.haml22
-rw-r--r--app/views/import/github/new.html.haml38
-rw-r--r--app/views/import/github/status.html.haml6
-rw-r--r--app/views/invites/show.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml14
-rw-r--r--app/views/layouts/project.html.haml2
-rw-r--r--app/views/notify/project_was_exported_email.html.haml2
-rw-r--r--app/views/notify/project_was_moved_email.html.haml2
-rw-r--r--app/views/profiles/_head.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml1
-rw-r--r--app/views/profiles/audit_log.html.haml1
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml2
-rw-r--r--app/views/profiles/chat_names/index.html.haml1
-rw-r--r--app/views/profiles/emails/index.html.haml1
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml1
-rw-r--r--app/views/profiles/keys/index.html.haml5
-rw-r--r--app/views/profiles/keys/show.html.haml1
-rw-r--r--app/views/profiles/notifications/show.html.haml1
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml1
-rw-r--r--app/views/profiles/preferences/show.html.haml1
-rw-r--r--app/views/profiles/show.html.haml1
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml7
-rw-r--r--app/views/projects/_commit_button.html.haml4
-rw-r--r--app/views/projects/_home_panel.html.haml6
-rw-r--r--app/views/projects/_issuable_by_email.html.haml9
-rw-r--r--app/views/projects/_new_project_fields.html.haml2
-rw-r--r--app/views/projects/blob/_header.html.haml1
-rw-r--r--app/views/projects/blob/_new_dir.html.haml4
-rw-r--r--app/views/projects/blob/_upload.html.haml4
-rw-r--r--app/views/projects/blob/_viewer.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_balsamiq.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_notebook.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_pdf.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_sketch.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_stl.html.haml3
-rw-r--r--app/views/projects/branches/_panel.html.haml19
-rw-r--r--app/views/projects/branches/index.html.haml53
-rw-r--r--app/views/projects/branches/new.html.haml1
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/clusters/_integration_form.html.haml6
-rw-r--r--app/views/projects/clusters/show.html.haml2
-rw-r--r--app/views/projects/commit/_change.html.haml4
-rw-r--r--app/views/projects/commit/_pipelines_list.haml4
-rw-r--r--app/views/projects/commit/show.html.haml3
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml3
-rw-r--r--app/views/projects/edit.html.haml1
-rw-r--r--app/views/projects/environments/folder.html.haml4
-rw-r--r--app/views/projects/environments/index.html.haml4
-rw-r--r--app/views/projects/environments/metrics.html.haml7
-rw-r--r--app/views/projects/environments/terminal.html.haml1
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml2
-rw-r--r--app/views/projects/graphs/charts.html.haml1
-rw-r--r--app/views/projects/imports/show.html.haml13
-rw-r--r--app/views/projects/issues/_discussion.html.haml14
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml3
-rw-r--r--app/views/projects/labels/index.html.haml1
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml2
-rw-r--r--app/views/projects/merge_requests/conflicts/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/index.html.haml3
-rw-r--r--app/views/projects/merge_requests/show.html.haml49
-rw-r--r--app/views/projects/milestones/index.html.haml1
-rw-r--r--app/views/projects/milestones/show.html.haml11
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml33
-rw-r--r--app/views/projects/pages_domains/_form.html.haml56
-rw-r--r--app/views/projects/pages_domains/edit.html.haml11
-rw-r--r--app/views/projects/pages_domains/new.html.haml6
-rw-r--r--app/views/projects/pages_domains/show.html.haml4
-rw-r--r--app/views/projects/pipelines/charts/_pipeline_times.haml1
-rw-r--r--app/views/projects/pipelines/charts/_pipelines.haml1
-rw-r--r--app/views/projects/pipelines/index.html.haml12
-rw-r--r--app/views/projects/pipelines/new.html.haml1
-rw-r--r--app/views/projects/pipelines/show.html.haml4
-rw-r--r--app/views/projects/project_members/update.js.haml4
-rw-r--r--app/views/projects/protected_branches/_index.html.haml3
-rw-r--r--app/views/projects/protected_tags/_index.html.haml3
-rw-r--r--app/views/projects/registry/repositories/index.html.haml3
-rw-r--r--app/views/projects/runners/_form.html.haml5
-rw-r--r--app/views/projects/runners/show.html.haml3
-rw-r--r--app/views/projects/services/_form.html.haml3
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml4
-rw-r--r--app/views/projects/services/prometheus/_configuration_banner.html.haml26
-rw-r--r--app/views/projects/services/prometheus/_help.html.haml28
-rw-r--r--app/views/projects/services/prometheus/_show.html.haml18
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/settings/repository/show.html.haml3
-rw-r--r--app/views/projects/tags/index.html.haml1
-rw-r--r--app/views/projects/tags/new.html.haml1
-rw-r--r--app/views/projects/tree/_tree_header.html.haml5
-rw-r--r--app/views/search/_category.html.haml8
-rw-r--r--app/views/search/_filter.html.haml2
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_note.html.haml2
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml19
-rw-r--r--app/views/shared/_label.html.haml12
-rw-r--r--app/views/shared/_new_commit_form.html.haml9
-rw-r--r--app/views/shared/_ref_switcher.html.haml12
-rw-r--r--app/views/shared/_service_settings.html.haml4
-rw-r--r--app/views/shared/boards/_show.html.haml6
-rw-r--r--app/views/shared/boards/components/_board.html.haml1
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml3
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml6
-rw-r--r--app/views/shared/issuable/form/_contribution.html.haml20
-rw-r--r--app/views/shared/members/update.js.haml6
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml33
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml2
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml8
-rw-r--r--app/views/shared/plugins/_index.html.haml23
-rw-r--r--app/views/shared/projects/_edit_information.html.haml6
-rw-r--r--app/views/shared/snippets/_form.html.haml1
-rw-r--r--app/views/shared/snippets/_snippet.html.haml2
-rw-r--r--app/views/u2f/_authenticate.html.haml1
-rw-r--r--app/views/u2f/_register.html.haml1
-rw-r--r--app/workers/all_queues.yml6
-rw-r--r--app/workers/archive_trace_worker.rb10
-rw-r--r--app/workers/build_finished_worker.rb2
-rw-r--r--app/workers/cluster_wait_for_ingress_ip_address_worker.rb11
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb2
-rw-r--r--app/workers/concerns/pipeline_background_queue.rb10
-rw-r--r--app/workers/create_trace_artifact_worker.rb10
-rw-r--r--app/workers/emails_on_push_worker.rb2
-rw-r--r--app/workers/git_garbage_collect_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/finish_import_worker.rb2
-rw-r--r--app/workers/pages_worker.rb2
-rw-r--r--app/workers/plugin_worker.rb15
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/process_commit_worker.rb7
-rw-r--r--app/workers/remove_expired_members_worker.rb2
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb2
610 files changed, 6721 insertions, 6588 deletions
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json
index 19843d24e22..d6f6e50398b 100644
--- a/app/assets/images/icons.json
+++ b/app/assets/images/icons.json
@@ -1 +1 @@
-{"iconCount":191,"spriteSize":86607,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","soft-unwrap","soft-wrap","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]} \ No newline at end of file
+{"iconCount":198,"spriteSize":89379,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","blame","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","compress","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","error","expand","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","go-back","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor-lines","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","soft-unwrap","soft-wrap","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","time-out","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]} \ No newline at end of file
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg
index 6aec54d0543..70205220e87 100644
--- a/app/assets/images/icons.svg
+++ b/app/assets/images/icons.svg
@@ -1 +1 @@
-<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-down" xmlns="http://www.w3.org/2000/svg"><path d="M10.472 7.282a.862.862 0 0 1 1.26-.006c.357.364.357.958 0 1.285L8.627 11.73A.886.886 0 0 1 8 12a.849.849 0 0 1-.627-.27L4.275 8.561a.904.904 0 0 1-.013-1.285.861.861 0 0 1 1.26-.007l2.486 2.527z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="bookmark" xmlns="http://www.w3.org/2000/svg"><path d="M6.746 10.505a2 2 0 0 1 2.508 0L11 11.911V3H5v8.91l1.746-1.405zM5 1h6a2 2 0 0 1 2 2v10.999a1 1 0 0 1-1.627.779L8 12.064l-3.373 2.714A1 1 0 0 1 3 13.998V3a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-o" xmlns="http://www.w3.org/2000/svg"><path d="M13 5l-4.365-.005a2 2 0 0 1-1.882-1.33A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1zm0-2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-open" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.59 5.464a2.998 2.998 0 0 1 1.096 3.845l-1.666 3.436A4 4 0 0 1 10.46 15H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.558a2 2 0 0 1 1.898 1.368l.21.632h4.973a2 2 0 0 1 2 2 2 2 0 0 1-.027.329l-.023.135zM5.285 7a1 1 0 0 0-.9.564l-1.939 4a1 1 0 0 0 .9 1.436h7.074a2 2 0 0 0 1.8-1.128l1.665-3.436a1 1 0 0 0-.9-1.436h-7.7z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M9 13h3v-3H4v3h3v-1a1 1 0 0 1 2 0v1zm5-3v3.659c0 .729-.657 1.341-1.5 1.341h-9c-.843 0-1.5-.612-1.5-1.341V10h-.88C.502 10 0 9.486 0 8.853c0-.307.12-.601.333-.816l6.405-6.463a1.56 1.56 0 0 1 2.374-.052L15.66 8.03c.444.441.455 1.167.024 1.622a1.108 1.108 0 0 1-.804.348H14zM7.95 3.273l-4.595 4.64h9.264l-4.67-4.64z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 38 38" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#1F78D1"/><path fill="#FFF" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 38 38" id="image-comment-light" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-external" xmlns="http://www.w3.org/2000/svg"><path d="M11 4a5.99 5.99 0 0 0-2 .341V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2.528a6.003 6.003 0 0 0 2.705 1.736A2.99 2.99 0 0 1 8 16H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3v1zM8.212 8.97l-.568-.876A.25.25 0 0 1 7.66 7.8l.404-.5a.25.25 0 0 1 .284-.076l.938.36c.256-.182.543-.325.85-.42l.323-.988a.25.25 0 0 1 .237-.173h.643a.25.25 0 0 1 .238.173l.321.989c.308.094.595.237.852.418l.937-.359a.25.25 0 0 1 .284.076l.404.5a.25.25 0 0 1 .016.293l-.568.875c.113.297.18.616.192.95l.9.54a.25.25 0 0 1 .114.27l-.145.627a.25.25 0 0 1-.221.192l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.282a.25.25 0 0 1-.29-.051l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.905a.25.25 0 0 1-.29.05l-.577-.281a.25.25 0 0 1-.138-.26L9 12.254a3.015 3.015 0 0 1-.512-.607l-1.114-.098a.25.25 0 0 1-.222-.192l-.145-.627a.25.25 0 0 1 .115-.27l.899-.54c.012-.334.08-.653.192-.95zm2.806 2.034a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7H2a1 1 0 1 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2H9V2a1 1 0 1 0-2 0v5z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="podcast" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862a1 1 0 0 1-.785 1.177A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-1-1 1 1 0 0 1 .02-.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 7.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4.464 2.464A1 1 0 0 1 5.88 3.88a3 3 0 0 0 0 4.242 1 1 0 0 1-1.415 1.415 5 5 0 0 1 0-7.072zm7.072 7.072A1 1 0 0 1 10.12 8.12a3 3 0 0 0 0-4.242 1 1 0 0 1 1.415-1.415 5 5 0 0 1 0 7.072zM2.343.343a1 1 0 1 1 1.414 1.414 6 6 0 0 0 0 8.486 1 1 0 1 1-1.414 1.414 8 8 0 0 1 0-11.314zm11.314 11.314a1 1 0 1 1-1.414-1.414 6 6 0 0 0 0-8.486A1 1 0 0 1 13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.625 4.423A4.897 4.897 0 0 1 8.079 3c2.73 0 4.944 2.239 4.944 5s-2.214 5-4.944 5c-1.41 0-2.723-.6-3.655-1.633a.98.98 0 0 0-1.397-.066 1.008 1.008 0 0 0-.064 1.413A6.87 6.87 0 0 0 8.079 15C11.9 15 15 11.866 15 8s-3.099-7-6.921-7A6.866 6.866 0 0 0 3.08 3.158L1.833 2.137a.49.49 0 0 0-.695.074.504.504 0 0 0-.11.311L1 7.26a.497.497 0 0 0 .6.492l4.576-1.013a.5.5 0 0 0 .206-.877L4.625 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.375 4.423A4.897 4.897 0 0 0 7.921 3c-2.73 0-4.944 2.239-4.944 5s2.214 5 4.944 5c1.41 0 2.723-.6 3.655-1.633a.98.98 0 0 1 1.397-.066c.403.373.432 1.005.064 1.413A6.87 6.87 0 0 1 7.921 15C4.1 15 1 11.866 1 8s3.099-7 6.921-7c1.915 0 3.706.792 4.999 2.158l1.247-1.021a.49.49 0 0 1 .695.074c.07.088.11.198.11.311L15 7.26a.497.497 0 0 1-.6.492L9.824 6.739a.5.5 0 0 1-.206-.877l1.757-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="fbfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="fbsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="fbthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8c1.657 0 3 1.373 3 3.067v7.346c0 1.065-.54 2.053-1.426 2.611l-4 2.52a2.944 2.944 0 0 1-3.148 0l-4-2.52A3.083 3.083 0 0 1 1 10.414V3.066C1 1.373 2.343 0 4 0zm0 2.045c-.552 0-1 .457-1 1.022v7.346c0 .355.18.685.475.87l4 2.52a.981.981 0 0 0 1.05 0l4-2.52c.295-.185.475-.515.475-.87V3.067c0-.565-.448-1.022-1-1.022H4zm0 1.533c0-.282.224-.511.5-.511h4V12.1a.52.52 0 0 1-.069.258.494.494 0 0 1-.684.183l-3.5-2.098a.513.513 0 0 1-.247-.44V3.577z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="soft-unwrap" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.5 11v-.598a.5.5 0 0 1 .765-.424l2.557 1.598a.5.5 0 0 1 0 .848l-2.557 1.598a.5.5 0 0 1-.765-.424V13H2a1 1 0 0 1 0-2h4.5zM2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm10 4h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="soft-wrap" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.5 13v.598a.5.5 0 0 1-.765.424l-2.557-1.598a.5.5 0 0 1 0-.848l2.557-1.598a.5.5 0 0 1 .765.424V11H12a1 1 0 0 0 0-2H2a1 1 0 1 1 0-2h10a3 3 0 0 1 0 6h-1.5zM2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 8h3a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="staged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h4a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm9 6a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM2 7h4a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 14 14" id="status_notfound" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M8.16 7.184c.519-.37.904-.857 1.07-1.477.384-1.427-.619-2.897-2.246-2.897-.732 0-1.327.26-1.766.692a2.163 2.163 0 0 0-.509.743.75.75 0 0 0 1.4.54.78.78 0 0 1 .16-.213c.168-.165.39-.262.715-.262.597 0 .936.496.798 1.007-.067.249-.235.462-.492.644-.231.165-.47.264-.601.3a.75.75 0 0 0-.556.724v1.421a.75.75 0 0 0 1.5 0v-.909a3.74 3.74 0 0 0 .526-.313z"/><ellipse cx="6.889" cy="10.634" rx="1" ry="1"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="unstaged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.572 10.506c.867 1.42.375 3.247-1.098 4.082a3.184 3.184 0 0 1-1.57.412h-9.81C1.387 15 0 13.665 0 12.018a2.9 2.9 0 0 1 .427-1.512L5.332 2.47C6.2 1.05 8.096.577 9.57 1.412c.453.257.831.622 1.098 1.059l4.905 8.035zM8.89 3.479a1.014 1.014 0 0 0-.366-.353 1.053 1.053 0 0 0-1.412.353l-4.905 8.035a.967.967 0 0 0-.143.504c0 .549.462.994 1.032.994h9.81c.184 0 .364-.048.523-.137a.974.974 0 0 0 .366-1.361L8.889 3.479zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-down" xmlns="http://www.w3.org/2000/svg"><path d="M10.472 7.282a.862.862 0 0 1 1.26-.006c.357.364.357.958 0 1.285L8.627 11.73A.886.886 0 0 1 8 12a.849.849 0 0 1-.627-.27L4.275 8.561a.904.904 0 0 1-.013-1.285.861.861 0 0 1 1.26-.007l2.486 2.527z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="blame" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 4h3a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm9 3h3a1 1 0 0 1 0 2h-3a1 1 0 0 1 0-2zm-9 3h3a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2zm9 0h3a1 1 0 0 1 0 2h-3a1 1 0 0 1 0-2zm0-6h3a1 1 0 0 1 0 2h-3a1 1 0 0 1 0-2zM2 7h3a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm6-6a1 1 0 0 1 1 1v12a1 1 0 0 1-2 0V2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="bookmark" xmlns="http://www.w3.org/2000/svg"><path d="M6.746 10.505a2 2 0 0 1 2.508 0L11 11.911V3H5v8.91l1.746-1.405zM5 1h6a2 2 0 0 1 2 2v10.999a1 1 0 0 1-1.627.779L8 12.064l-3.373 2.714A1 1 0 0 1 3 13.998V3a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="compress" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.587 12.707l-1.414 1.415a1 1 0 0 1-1.414-1.415l1.414-1.414-1.386-1.386a.5.5 0 0 1 .299-.85l4.708-.523a.5.5 0 0 1 .553.552l-.524 4.709a.5.5 0 0 1-.85.298l-1.386-1.386zm6.859-9.687l1.414-1.414a1 1 0 0 1 1.414 1.414L12.86 4.434l1.386 1.386a.5.5 0 0 1-.299.85l-4.708.524a.5.5 0 0 1-.553-.552l.524-4.71a.5.5 0 0 1 .85-.297l1.386 1.385z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="error" xmlns="http://www.w3.org/2000/svg"><title>error</title><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0-2A5 5 0 1 0 8 3a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="expand" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.435 10.092l1.414-1.414a1 1 0 1 1 1.414 1.414L5.85 11.507l1.386 1.385a.5.5 0 0 1-.299.85l-4.709.524a.5.5 0 0 1-.552-.552L2.2 9.005a.5.5 0 0 1 .85-.298l1.386 1.385zm7.283-4.455l-1.414 1.415A1 1 0 1 1 8.89 5.637l1.414-1.414-1.386-1.386a.5.5 0 0 1 .299-.85l4.708-.523a.5.5 0 0 1 .553.552l-.524 4.709a.5.5 0 0 1-.85.298l-1.386-1.386z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-o" xmlns="http://www.w3.org/2000/svg"><path d="M13 5l-4.365-.005a2 2 0 0 1-1.882-1.33A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1zm0-2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-open" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.59 5.464a2.998 2.998 0 0 1 1.096 3.845l-1.666 3.436A4 4 0 0 1 10.46 15H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.558a2 2 0 0 1 1.898 1.368l.21.632h4.973a2 2 0 0 1 2 2 2 2 0 0 1-.027.329l-.023.135zM5.285 7a1 1 0 0 0-.9.564l-1.939 4a1 1 0 0 0 .9 1.436h7.074a2 2 0 0 0 1.8-1.128l1.665-3.436a1 1 0 0 0-.9-1.436h-7.7z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="go-back" xmlns="http://www.w3.org/2000/svg"><path d="M4.9 7V6a.5.5 0 0 0-.8-.4l-2.667 2a.5.5 0 0 0 0 .8l2.667 2a.5.5 0 0 0 .8-.4V9h4a1 1 0 1 0 0-2h-4zM7 12a1 1 0 0 0-2 0 3 3 0 0 0 3 3h3a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4H8a3 3 0 0 0-3 3 1 1 0 1 0 2 0 1 1 0 0 1 1-1h3a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H8a1 1 0 0 1-1-1z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M9 13h3v-3H4v3h3v-1a1 1 0 0 1 2 0v1zm5-3v3.659c0 .729-.657 1.341-1.5 1.341h-9c-.843 0-1.5-.612-1.5-1.341V10h-.88C.502 10 0 9.486 0 8.853c0-.307.12-.601.333-.816l6.405-6.463a1.56 1.56 0 0 1 2.374-.052L15.66 8.03c.444.441.455 1.167.024 1.622a1.108 1.108 0 0 1-.804.348H14zM7.95 3.273l-4.595 4.64h9.264l-4.67-4.64z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 38 38" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#1F78D1"/><path fill="#FFF" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 38 38" id="image-comment-light" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-external" xmlns="http://www.w3.org/2000/svg"><path d="M11 4a5.99 5.99 0 0 0-2 .341V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2.528a6.003 6.003 0 0 0 2.705 1.736A2.99 2.99 0 0 1 8 16H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3v1zM8.212 8.97l-.568-.876A.25.25 0 0 1 7.66 7.8l.404-.5a.25.25 0 0 1 .284-.076l.938.36c.256-.182.543-.325.85-.42l.323-.988a.25.25 0 0 1 .237-.173h.643a.25.25 0 0 1 .238.173l.321.989c.308.094.595.237.852.418l.937-.359a.25.25 0 0 1 .284.076l.404.5a.25.25 0 0 1 .016.293l-.568.875c.113.297.18.616.192.95l.9.54a.25.25 0 0 1 .114.27l-.145.627a.25.25 0 0 1-.221.192l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.282a.25.25 0 0 1-.29-.051l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.905a.25.25 0 0 1-.29.05l-.577-.281a.25.25 0 0 1-.138-.26L9 12.254a3.015 3.015 0 0 1-.512-.607l-1.114-.098a.25.25 0 0 1-.222-.192l-.145-.627a.25.25 0 0 1 .115-.27l.899-.54c.012-.334.08-.653.192-.95zm2.806 2.034a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="monitor-lines" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 12v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H4a3 3 0 0 1-3-3V4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3h-2zM4 3a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4zm1.5 2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0 2h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7H2a1 1 0 1 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2H9V2a1 1 0 1 0-2 0v5z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="podcast" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862a1 1 0 0 1-.785 1.177A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-1-1 1 1 0 0 1 .02-.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 7.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4.464 2.464A1 1 0 0 1 5.88 3.88a3 3 0 0 0 0 4.242 1 1 0 0 1-1.415 1.415 5 5 0 0 1 0-7.072zm7.072 7.072A1 1 0 0 1 10.12 8.12a3 3 0 0 0 0-4.242 1 1 0 0 1 1.415-1.415 5 5 0 0 1 0 7.072zM2.343.343a1 1 0 1 1 1.414 1.414 6 6 0 0 0 0 8.486 1 1 0 1 1-1.414 1.414 8 8 0 0 1 0-11.314zm11.314 11.314a1 1 0 1 1-1.414-1.414 6 6 0 0 0 0-8.486A1 1 0 0 1 13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.625 4.423A4.897 4.897 0 0 1 8.079 3c2.73 0 4.944 2.239 4.944 5s-2.214 5-4.944 5c-1.41 0-2.723-.6-3.655-1.633a.98.98 0 0 0-1.397-.066 1.008 1.008 0 0 0-.064 1.413A6.87 6.87 0 0 0 8.079 15C11.9 15 15 11.866 15 8s-3.099-7-6.921-7A6.866 6.866 0 0 0 3.08 3.158L1.833 2.137a.49.49 0 0 0-.695.074.504.504 0 0 0-.11.311L1 7.26a.497.497 0 0 0 .6.492l4.576-1.013a.5.5 0 0 0 .206-.877L4.625 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.375 4.423A4.897 4.897 0 0 0 7.921 3c-2.73 0-4.944 2.239-4.944 5s2.214 5 4.944 5c1.41 0 2.723-.6 3.655-1.633a.98.98 0 0 1 1.397-.066c.403.373.432 1.005.064 1.413A6.87 6.87 0 0 1 7.921 15C4.1 15 1 11.866 1 8s3.099-7 6.921-7c1.915 0 3.706.792 4.999 2.158l1.247-1.021a.49.49 0 0 1 .695.074c.07.088.11.198.11.311L15 7.26a.497.497 0 0 1-.6.492L9.824 6.739a.5.5 0 0 1-.206-.877l1.757-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="fhfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="fhsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="fhthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8c1.657 0 3 1.373 3 3.067v7.346c0 1.065-.54 2.053-1.426 2.611l-4 2.52a2.944 2.944 0 0 1-3.148 0l-4-2.52A3.083 3.083 0 0 1 1 10.414V3.066C1 1.373 2.343 0 4 0zm0 2.045c-.552 0-1 .457-1 1.022v7.346c0 .355.18.685.475.87l4 2.52a.981.981 0 0 0 1.05 0l4-2.52c.295-.185.475-.515.475-.87V3.067c0-.565-.448-1.022-1-1.022H4zm0 1.533c0-.282.224-.511.5-.511h4V12.1a.52.52 0 0 1-.069.258.494.494 0 0 1-.684.183l-3.5-2.098a.513.513 0 0 1-.247-.44V3.577z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="soft-unwrap" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.5 11v-.598a.5.5 0 0 1 .765-.424l2.557 1.598a.5.5 0 0 1 0 .848l-2.557 1.598a.5.5 0 0 1-.765-.424V13H2a1 1 0 0 1 0-2h4.5zM2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm10 4h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="soft-wrap" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.5 13v.598a.5.5 0 0 1-.765.424l-2.557-1.598a.5.5 0 0 1 0-.848l2.557-1.598a.5.5 0 0 1 .765.424V11H12a1 1 0 0 0 0-2H2a1 1 0 1 1 0-2h10a3 3 0 0 1 0 6h-1.5zM2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 8h3a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="staged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h4a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm9 6a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM2 7h4a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 14 14" id="status_notfound" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M8.16 7.184c.519-.37.904-.857 1.07-1.477.384-1.427-.619-2.897-2.246-2.897-.732 0-1.327.26-1.766.692a2.163 2.163 0 0 0-.509.743.75.75 0 0 0 1.4.54.78.78 0 0 1 .16-.213c.168-.165.39-.262.715-.262.597 0 .936.496.798 1.007-.067.249-.235.462-.492.644-.231.165-.47.264-.601.3a.75.75 0 0 0-.556.724v1.421a.75.75 0 0 0 1.5 0v-.909a3.74 3.74 0 0 0 .526-.313z"/><ellipse cx="6.889" cy="10.634" rx="1" ry="1"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="time-out" xmlns="http://www.w3.org/2000/svg"><path d="M12 13.36l-1.36 1.358a.961.961 0 1 1-1.358-1.359L10.64 12l-1.36-1.36a.961.961 0 0 1 1.36-1.358L12 10.64l1.36-1.36a.961.961 0 1 1 1.358 1.36L13.36 12l1.36 1.36a.961.961 0 1 1-1.36 1.358L12 13.36zM8 9H5a1 1 0 1 1 0-2h2V5a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1zm0 6a7 7 0 1 1 7-7 4.976 4.976 0 0 0-2.084-.916A5.002 5.002 0 0 0 3 8a5.002 5.002 0 0 0 4.084 4.916c.143.772.462 1.48.916 2.084z"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="unstaged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.572 10.506c.867 1.42.375 3.247-1.098 4.082a3.184 3.184 0 0 1-1.57.412h-9.81C1.387 15 0 13.665 0 12.018a2.9 2.9 0 0 1 .427-1.512L5.332 2.47C6.2 1.05 8.096.577 9.57 1.412c.453.257.831.622 1.098 1.059l4.905 8.035zM8.89 3.479a1.014 1.014 0 0 0-.366-.353 1.053 1.053 0 0 0-1.412.353l-4.905 8.035a.967.967 0 0 0-.143.504c0 .549.462.994 1.032.994h9.81c.184 0 .364-.048.523-.137a.974.974 0 0 0 .366-1.361L8.889 3.479zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/canceled-job_empty.svg b/app/assets/images/illustrations/canceled-job_empty.svg
new file mode 100644
index 00000000000..e4a7c244f56
--- /dev/null
+++ b/app/assets/images/illustrations/canceled-job_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="430" height="172" viewBox="0 0 430 172"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M187 121a2 2 0 1 1 0-4c2.542 0 5.042-.305 7.463-.904a2 2 0 1 1 .96 3.884A35.075 35.075 0 0 1 187 121zm18.21-5.105a2 2 0 1 1-2.083-3.414 31.143 31.143 0 0 0 5.896-4.664 2 2 0 1 1 2.842 2.815 35.143 35.143 0 0 1-6.654 5.263zm12.895-13.835a2 2 0 0 1-3.552-1.838 30.77 30.77 0 0 0 2.612-7.042 2 2 0 1 1 3.892.922 34.77 34.77 0 0 1-2.952 7.958zm3.816-18.433a2 2 0 1 1-3.991.268 30.873 30.873 0 0 0-1.407-7.38 2 2 0 0 1 3.808-1.223 34.873 34.873 0 0 1 1.59 8.335zm-6.346-17.842a2 2 0 0 1-3.264 2.312 31.188 31.188 0 0 0-5.054-5.564 2 2 0 0 1 2.615-3.027 35.188 35.188 0 0 1 5.703 6.279zm-14.68-11.918a2 2 0 0 1-1.59 3.67 30.758 30.758 0 0 0-7.206-2.12 2 2 0 1 1 .653-3.946 34.758 34.758 0 0 1 8.143 2.396zm-18.632-2.549a2 2 0 0 1 .537 3.964c-2.505.339-4.94.98-7.266 1.907a2 2 0 1 1-1.48-3.716 34.774 34.774 0 0 1 8.209-2.155zm-17.356 7.535a2 2 0 0 1 2.527 3.1 31.196 31.196 0 0 0-5.213 5.416 2 2 0 0 1-3.196-2.406 35.196 35.196 0 0 1 5.882-6.11zm-10.918 15.49a2 2 0 0 1 3.772 1.331 30.82 30.82 0 0 0-1.619 7.337 2 2 0 1 1-3.982-.38 34.82 34.82 0 0 1 1.829-8.289zm-1.27 18.744a2 2 0 1 1 3.917-.806 30.757 30.757 0 0 0 2.4 7.118 2 2 0 1 1-3.605 1.73 34.757 34.757 0 0 1-2.713-8.042zm8.675 16.774a2 2 0 0 1 2.926-2.728 31.167 31.167 0 0 0 5.751 4.841 2 2 0 1 1-2.187 3.349 35.167 35.167 0 0 1-6.49-5.462zm16.245 9.873a2 2 0 1 1 1.067-3.855 30.979 30.979 0 0 0 7.434 1.11 2 2 0 1 1-.11 3.998 34.979 34.979 0 0 1-8.391-1.253z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M187 104c-9.941 0-18-8.059-18-18s8.059-18 18-18 18 8.059 18 18-8.059 18-18 18zm0-4c7.732 0 14-6.268 14-14s-6.268-14-14-14-14 6.268-14 14 6.268 14 14 14z"/><path fill="#6B4FBB" d="M189 84h5a2 2 0 1 1 0 4h-7a2 2 0 0 1-2-2v-8a2 2 0 1 1 4 0v6z"/><g transform="translate(261 16)"><path fill="#F9F9F9" d="M115.575 132.895C114.333 137.006 110.516 140 106 140H10c-5.523 0-10-4.477-10-10V15a10 10 0 0 1 5.884-9.116A9.964 9.964 0 0 0 5 10v114c0 5.523 4.477 10 10 10h96a9.957 9.957 0 0 0 4.575-1.105z"/><rect width="116" height="134" x="5" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M15 2a8 8 0 0 0-8 8v114a8 8 0 0 0 8 8h96a8 8 0 0 0 8-8V10a8 8 0 0 0-8-8H15zm0-4h96c6.627 0 12 5.373 12 12v114c0 6.627-5.373 12-12 12H15c-6.627 0-12-5.373-12-12V10C3 3.373 8.373-2 15-2z"/><g transform="translate(23 23)"><rect width="16" height="4" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="32" y="12" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="44" fill="#EEE" rx="2"/><rect width="16" height="4" x="12" y="24" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="64" y="36" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="20" fill="#FEE1D3" rx="2"/><rect width="8" height="4" x="32" y="36" fill="#FC6D26" rx="2"/><rect width="8" height="4" x="52" y="12" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="64" fill="#FEF0E8" rx="2"/><rect width="12" height="4" x="16" y="48" fill="#E1DBF1" rx="2"/><rect width="8" height="4" x="44" y="36" fill="#FC6D26" rx="2"/><rect width="4" height="4" x="56" y="36" fill="#E1DBF1" rx="2"/><rect width="4" height="4" x="64" y="60" fill="#E1DBF1" rx="2"/><rect width="4" height="4" x="72" y="60" fill="#FC6D26" rx="2"/><rect width="8" height="4" x="32" fill="#FC6D26" rx="2"/><rect width="28" height="4" y="36" fill="#EEE" rx="2"/><rect width="28" height="4" x="44" y="48" fill="#EEE" rx="2"/><rect width="28" height="4" x="32" y="60" fill="#EFEDF8" rx="2"/><rect width="28" height="4" y="12" fill="#6B4FBB" rx="2"/><rect width="28" height="4" x="32" y="24" fill="#C3B8E3" rx="2"/><rect width="8" height="4" y="24" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="32" y="48" fill="#6B4FBB" rx="2"/><rect width="12" height="4" y="48" fill="#FC6D26" rx="2"/><rect width="12" height="4" y="60" fill="#FEF0E8" rx="2"/><rect width="12" height="4" x="16" y="60" fill="#FEF0E8" rx="2"/></g><g transform="translate(23 95)"><rect width="16" height="4" fill="#EFEDF8" rx="2"/><rect width="16" height="4" x="18" y="12" fill="#FC6D26" rx="2"/><rect width="16" height="4" x="44" fill="#6B4FBB" rx="2"/><rect width="8" height="4" x="20" fill="#FEE1D3" rx="2"/><rect width="8" height="4" x="38" y="12" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="64" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="32" fill="#FC6D26" rx="2"/><rect width="14" height="4" y="12" fill="#EEE" rx="2"/></g></g><path fill="#FFF" d="M80 117c17.12 0 31-13.88 31-31 0-17.12-13.88-31-31-31-17.12 0-31 13.88-31 31 0 17.12 13.88 31 31 31z"/><path fill="#EEE" fill-rule="nonzero" d="M80 121c-19.33 0-35-15.67-35-35s15.67-35 35-35 35 15.67 35 35-15.67 35-35 35zm0-4c17.12 0 31-13.88 31-31 0-17.12-13.88-31-31-31-17.12 0-31 13.88-31 31 0 17.12 13.88 31 31 31z"/><g transform="translate(61 67)"><path fill="#CCC" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15z"/><rect width="18" height="4" x="10" y="17" fill="#2E2E2E" rx="2" transform="rotate(45 19 19)"/></g><path fill="#E5E5E5" fill-rule="nonzero" d="M122 88c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.994 1.994 0 0 1 122 88zm14 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.994 1.994 0 0 1 136 88zm93 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.994 1.994 0 0 1 229 88zm14 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.994 1.994 0 0 1 243 88z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/epics.svg b/app/assets/images/illustrations/epics.svg
index 1a37e6bba5f..be6c3fb76a5 100644
--- a/app/assets/images/illustrations/epics.svg
+++ b/app/assets/images/illustrations/epics.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="430" height="300" viewBox="0 0 430 300"><g fill="none" fill-rule="evenodd"><g transform="translate(75 53)"><rect width="284" height="208" y="5" fill="#F9F9F9" rx="10"/><rect width="284" height="208" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v188a6 6 0 0 0 6 6h264a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h264c5.523 0 10 4.477 10 10v188c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><path fill="#EEE" fill-rule="nonzero" d="M25.168 153.995c3.837-.215 7.173.028 10.119.691a3 3 0 1 0 1.318-5.853c-3.509-.79-7.4-1.074-11.773-.828a3 3 0 1 0 .336 5.99zm19.043 4.66c2.401 1.704 4.388 3.61 7.569 7.083a3 3 0 0 0 4.424-4.054c-3.448-3.763-5.686-5.911-8.522-7.923a3 3 0 1 0-3.471 4.894zm15.575 15.173c3.181 2.675 6.52 4.665 10.397 6.039a3 3 0 0 0 2.004-5.655c-3.162-1.121-5.884-2.743-8.54-4.976a3 3 0 1 0-3.861 4.592zm22.133 8.148c1.02.037 2.067.045 3.143.023a72.664 72.664 0 0 0 8.346-.638 3 3 0 1 0-.812-5.945c-2.442.334-4.996.53-7.658.585a48.55 48.55 0 0 1-2.796-.021 3 3 0 0 0-.223 5.996zm22.778-3.286c3.9-1.37 7.427-3.15 10.54-5.305a3 3 0 0 0-3.415-4.933c-2.665 1.845-5.712 3.382-9.114 4.578a3 3 0 0 0 1.989 5.66zm19.156-13.62a33.752 33.752 0 0 0 5.276-10.817 3 3 0 1 0-5.773-1.633 27.753 27.753 0 0 1-4.341 8.9 3 3 0 1 0 4.838 3.55zm6.577-22.657c-.187-3.817-.926-7.71-2.204-11.596a3 3 0 0 0-5.7 1.874c1.113 3.384 1.75 6.745 1.91 10.016a3 3 0 1 0 5.994-.294zm-7.097-22.26c-1.897-3.2-4.152-6.325-6.748-9.344a3 3 0 0 0-4.55 3.913c2.372 2.756 4.421 5.597 6.136 8.49a3 3 0 0 0 5.162-3.06zm-11.546-17.793c-.938-3.025-1.402-6.42-1.365-9.976a3 3 0 0 0-6-.063c-.043 4.163.506 8.177 1.634 11.816a3 3 0 1 0 5.731-1.777zm.053-20.107c.905-3.341 2.22-6.538 3.904-9.448a3 3 0 0 0-5.194-3.004c-1.948 3.368-3.463 7.048-4.501 10.884a3 3 0 1 0 5.791 1.568zm10.134-17.305c2.475-2.28 5.265-4.09 8.335-5.374a3 3 0 1 0-2.314-5.536c-3.725 1.558-7.105 3.75-10.086 6.497a3 3 0 1 0 4.065 4.413zm18.177-7.586c3.202-.18 6.599.092 10.18.843a3 3 0 0 0 1.23-5.872c-4.086-.857-8.009-1.172-11.747-.962a3 3 0 1 0 .337 5.99zm20.047 3.95c3.068 1.268 6.232 2.842 9.487 4.728a3 3 0 0 0 3.009-5.191c-3.48-2.017-6.883-3.71-10.204-5.083a3 3 0 1 0-2.292 5.545zm19.578 9.955c3.711 1.586 7.376 2.77 10.997 3.565a3 3 0 0 0 1.286-5.86c-3.248-.713-6.555-1.782-9.925-3.222a3 3 0 1 0-2.358 5.517zm22.591 4.789c3.94-.04 7.808-.553 11.61-1.513a3 3 0 1 0-1.468-5.817 43.358 43.358 0 0 1-10.203 1.33 3 3 0 0 0 .061 6zm22.52-5.558c3.335-1.637 6.607-3.613 9.845-5.916a3 3 0 1 0-3.477-4.89c-2.984 2.122-5.98 3.931-9.011 5.42a3 3 0 1 0 2.643 5.386zm18.678-13.054a3 3 0 0 1-4.02-4.454 130.547 130.547 0 0 0 5.31-5.088 3 3 0 1 1 4.265 4.22 136.507 136.507 0 0 1-5.555 5.322zm-48.722 25.641a3 3 0 1 1 4.314-4.17c3.056 3.16 5.075 6.744 6.172 10.754a3 3 0 0 1-5.787 1.584c-.834-3.047-2.35-5.739-4.699-8.168zm5.347 18.049a3 3 0 1 1 5.978.52c-.282 3.232-.805 6.273-1.832 11.206a3 3 0 0 1-5.874-1.222c.981-4.717 1.473-7.572 1.728-10.504zm-3.777 21.555a3 3 0 0 1 5.953.747c-.5 3.988-.397 7.09.399 9.67a3 3 0 1 1-5.733 1.769c-1.087-3.52-1.217-7.426-.62-12.186zm7.393 22.444a3 3 0 0 1 4.461-4.013c2.703 3.005 5.224 5.296 7.594 6.947a3 3 0 0 1-3.429 4.924c-2.775-1.932-5.632-4.53-8.626-7.858zm20.352 12.28a3 3 0 1 1 .334-5.99c2.77.154 5.453-.554 9.224-2.254a3 3 0 0 1 2.466 5.47c-4.57 2.06-8.103 2.993-12.024 2.775zm21.784-7.058a3 3 0 0 1-1.815-5.719c4.227-1.342 8.24-1.61 12.496-.572a3 3 0 0 1-1.421 5.83c-3.116-.76-6.025-.566-9.26.46zM106.53 56.038a3 3 0 1 1-3.45 4.909c-1.074-.755-6.723-6.044-8.083-7.204a68.019 68.019 0 0 0-.332-.281 3 3 0 1 1 3.865-4.59l.362.306c1.643 1.402 6.971 6.391 7.638 6.86zM88.536 42.422a3 3 0 0 1-2.285 5.548c-3.14-1.293-5.78-1.34-8.105-.05a3 3 0 0 1-2.91-5.247c4.087-2.266 8.597-2.187 13.3-.25zM66.698 48.73a3 3 0 0 1 2.029 5.647c-4.432 1.592-8.786.835-13.166-1.88a3 3 0 1 1 3.16-5.1c2.93 1.816 5.425 2.25 7.977 1.333zm-15.636-8.038a3 3 0 0 1-4.352 4.13c-.911-.96-1.85-1.98-3.061-3.32-.295-.325-2.437-2.703-3.07-3.4-.47-.518-.9-.988-1.313-1.436a3 3 0 0 1 4.41-4.068c.425.46.866.942 1.346 1.47.642.709 2.79 3.092 3.076 3.41a180.865 180.865 0 0 0 2.964 3.214z"/><path fill="#E1DBF1" d="M254.66 72.196l2-3.464a2 2 0 1 0-3.464-2l-2 3.464-3.464-2a2 2 0 0 0-2 3.464l3.464 2-2 3.464a2 2 0 0 0 3.464 2l2-3.464 3.464 2a2 2 0 1 0 2-3.464l-3.464-2zm-151.904 78.732l2.829-2.828a2 2 0 0 0-2.829-2.829l-2.828 2.829-2.828-2.829a2 2 0 0 0-2.829 2.829l2.829 2.828-2.829 2.829a2 2 0 1 0 2.829 2.828l2.828-2.828 2.828 2.828a2 2 0 1 0 2.829-2.828l-2.829-2.829z"/><path fill="#6B4FBB" d="M210.66 173.66l3.464-2a2 2 0 1 0-2-3.464l-3.464 2-2-3.464a2 2 0 0 0-3.464 2l2 3.464-3.464 2a2 2 0 1 0 2 3.464l3.464-2 2 3.464a2 2 0 1 0 3.464-2l-2-3.464z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M27 181a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-4a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M138 85a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M200 57a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#FC6D26" fill-rule="nonzero" d="M222.647 121.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M103.647 28.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M85 103.488L81.841 108h6.318L85 103.488zm6.436 2.218A4 4 0 0 1 88.159 112H81.84a4 4 0 0 1-3.277-6.294l3.16-4.512a4 4 0 0 1 6.553 0l3.159 4.512z"/></g><path fill="#F9F9F9" d="M334.376 99.43A48.805 48.805 0 0 0 366 111c27.062 0 49-21.938 49-49s-21.938-49-49-49-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71z"/><path fill="#FFF" d="M339.376 94.43A48.805 48.805 0 0 0 371 106c27.062 0 49-21.938 49-49S398.062 8 371 8s-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71z"/><path fill="#EEE" fill-rule="nonzero" d="M329.85 99.072a4.5 4.5 0 0 1-5.516-5.517l2.827-10.48C322.501 75.258 320 66.31 320 57c0-28.167 22.833-51 51-51s51 22.833 51 51-22.833 51-51 51c-11.859 0-23.096-4.064-32.102-11.37l-9.048 2.442zm10.817-6.169C349.091 100.027 359.737 104 371 104c25.957 0 47-21.043 47-47s-21.043-47-47-47-47 21.043-47 47c0 8.859 2.453 17.351 7.016 24.716l.456.737-3.277 12.144c.072.527.347.685.613.613l11.059-2.984.8.677z"/><g transform="translate(354 34)"><path fill="#E1DBF1" fill-rule="nonzero" d="M13 4a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-8zm0-4h8a5 5 0 0 1 5 5v1a5 5 0 0 1-5 5h-8a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M5 11a1 1 0 0 0 0 2h24a1 1 0 0 0 0-2H5zm0-4h24a5 5 0 0 1 0 10H5A5 5 0 0 1 5 7z"/><rect width="12" height="4" x="11" y="31" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="19" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="37" fill="#E1DBF1" rx="2"/><rect width="12" height="4" x="11" y="43" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="25" fill="#E1DBF1" rx="2"/></g><path fill="#F9F9F9" d="M344.238 225.072A38.83 38.83 0 0 1 368 217c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213z"/><path fill="#FFF" d="M348.238 221.072A38.83 38.83 0 0 1 372 213c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213z"/><path fill="#EEE" fill-rule="nonzero" d="M336.85 215.928a4.5 4.5 0 0 0-5.516 5.517l3.543 13.13A40.848 40.848 0 0 0 331 252c0 22.644 18.356 41 41 41s41-18.356 41-41-18.356-41-41-41a40.82 40.82 0 0 0-24.182 7.887l-10.968-2.96zm12.608 6.73A36.824 36.824 0 0 1 372 215c20.435 0 37 16.565 37 37s-16.565 37-37 37-37-16.565-37-37c0-5.747 1.31-11.304 3.795-16.343l.334-.677-3.934-14.577a.5.5 0 0 1 .613-.613l12.865 3.471.785-.604z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M356.097 255.962a7 7 0 0 0 8.81 10.88l1.093-.885v1.454a7 7 0 1 0 14 0v-1.454l1.092.885a7 7 0 1 0 8.81-10.88l-1.185-.96 1.455-.337a7 7 0 1 0-3.15-13.64l-1.4.323.623-1.278a7 7 0 0 0-12.583-6.137l-.662 1.356-.662-1.356a7 7 0 0 0-12.583 6.137l.623 1.278-1.4-.324a7 7 0 1 0-3.15 13.641l1.455.336-1.186.96zm5.464-.913a11.914 11.914 0 0 1-.444-1.95l-.19-1.362-4.2-.97a3 3 0 0 1 1.35-5.845l4.178.964.768-1.145c.373-.557.793-1.082 1.254-1.57l.95-1.006-1.877-3.849a3 3 0 0 1 5.393-2.63l1.892 3.879 1.363-.113a12.188 12.188 0 0 1 2.004 0l1.363.113 1.892-3.879a3 3 0 0 1 5.393 2.63l-1.877 3.849.95 1.006c.461.488.88 1.013 1.254 1.57l.768 1.145 4.178-.964a3 3 0 1 1 1.35 5.846l-4.2.97-.19 1.36a11.914 11.914 0 0 1-.444 1.95l-.413 1.302 3.36 2.72a3 3 0 1 1-3.776 4.663l-3.32-2.688-1.196.706a11.94 11.94 0 0 1-1.808.873l-1.286.492v4.295a3 3 0 1 1-6 0v-4.295l-1.286-.492a11.94 11.94 0 0 1-1.808-.873l-1.196-.706-3.32 2.688a3 3 0 1 1-3.776-4.663l3.36-2.72-.413-1.301z"/><path fill="#FC6D26" fill-rule="nonzero" d="M373 245.411a6 6 0 1 0 0 12 6 6 0 0 0 0-12zm0 4a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/><g><path fill="#F9F9F9" d="M94.624 162.43A48.805 48.805 0 0 1 63 174c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71z"/><path fill="#FFF" stroke="#EEE" stroke-width="4" d="M89.624 157.43A48.805 48.805 0 0 1 58 169c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71z"/><path fill="#EEE" fill-rule="nonzero" d="M99.15 162.072a4.5 4.5 0 0 0 5.516-5.517l-2.827-10.48C106.499 138.258 109 129.31 109 120c0-28.167-22.833-51-51-51S7 91.833 7 120s22.833 51 51 51c11.859 0 23.096-4.064 32.102-11.37l9.048 2.442zm-10.817-6.169C79.909 163.027 69.263 167 58 167c-25.957 0-47-21.043-47-47s21.043-47 47-47 47 21.043 47 47c0 8.859-2.453 17.351-7.016 24.716l-.456.737 3.277 12.144c-.072.527-.347.685-.613.613l-11.059-2.984-.8.677z"/><g fill-rule="nonzero"><path fill="#FEE1D3" d="M55.47 94.47l-16.148 6.688a4 4 0 0 0-2.164 2.164l-6.689 16.147a4 4 0 0 0 0 3.062l6.689 16.147a4 4 0 0 0 2.164 2.164l16.147 6.689a4 4 0 0 0 3.062 0l16.147-6.689a4 4 0 0 0 2.164-2.164l6.689-16.147a4 4 0 0 0 0-3.062l-6.689-16.147a4 4 0 0 0-2.164-2.164L58.53 94.469a4 4 0 0 0-3.062 0zM57 98.164l16.147 6.688L79.835 121l-6.688 16.147L57 143.835l-16.147-6.688L34.165 121l6.688-16.147L57 98.165zM57 107a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 20c6.075 0 11-4.925 11-11s-4.925-11-11-11-11 4.925-11 11 4.925 11 11 11zm0-4a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/><path fill="#FC6D26" d="M57 126.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0-3a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></g></g></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="430" height="300" viewBox="0 0 430 300"><defs><rect id="a" width="280" height="180" x="77" y="60" rx="10"/><rect id="c" width="100" height="48" rx="10"/><filter id="b" width="105%" height="120.8%" x="-2.5%" y="-5.2%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="e" width="84" height="76" rx="10"/><filter id="d" width="106%" height="113.2%" x="-3%" y="-3.3%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="g" width="52" height="76" rx="10"/><filter id="f" width="109.6%" height="113.2%" x="-4.8%" y="-3.3%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="i" width="36" height="132" rx="10"/><filter id="h" width="113.9%" height="107.6%" x="-6.9%" y="-1.9%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="k" width="52" height="48" rx="10"/><filter id="j" width="109.6%" height="120.8%" x="-4.8%" y="-5.2%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M0 0h430v300H0z"/><use fill="#FFF" xlink:href="#a"/><rect width="276" height="176" x="79" y="62" stroke="#EEE" stroke-width="4" rx="10"/><rect width="8" height="20" x="244" y="141" fill="#EEE" rx="4"/><rect width="8" height="20" x="311" y="103" fill="#EEE" rx="4"/><rect width="8" height="20" x="272" y="166" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="257" y="203" fill="#EEE" rx="4"/><rect width="8" height="20" x="169" y="77" fill="#EEE" rx="4"/><rect width="8" height="20" x="191" y="104" fill="#FFF" rx="4"/><rect width="8" height="20" x="140" y="157" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="154" y="119" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="163" y="205" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="294" y="186" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="327" y="73" fill="#FEE1D3" rx="4"/><g transform="translate(197 79)"><use fill="#000" filter="url(#b)" xlink:href="#c"/><use fill="#FFF" xlink:href="#c"/><rect width="96" height="44" x="2" y="2" stroke="#FEE1D3" stroke-width="4" rx="10"/><rect width="8" height="20" x="62" y="14" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="78" y="14" fill="#FDC4A8" rx="4"/><rect width="8" height="20" x="46" y="14" fill="#FC6D26" rx="4"/><rect width="8" height="20" x="30" y="14" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="14" y="14" fill="#EEE" rx="4"/></g><g transform="translate(57 30)"><use fill="#000" filter="url(#d)" xlink:href="#e"/><use fill="#FFF" xlink:href="#e"/><rect width="80" height="72" x="2" y="2" stroke="#E1DBF1" stroke-width="4" rx="10"/><rect width="8" height="20" x="62" y="42" fill="#6B4FBB" rx="4"/><rect width="8" height="20" x="46" y="42" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="30" y="42" fill="#C3B8E3" rx="4"/><rect width="8" height="20" x="14" y="42" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="62" y="14" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="46" y="14" fill="#C3B8E3" rx="4"/><rect width="8" height="20" x="30" y="14" fill="#EEE" rx="4"/><rect width="8" height="20" x="14" y="14" fill="#C3B8E3" rx="4"/></g><g transform="translate(317 133)"><use fill="#000" filter="url(#f)" xlink:href="#g"/><use fill="#FFF" xlink:href="#g"/><rect width="48" height="72" x="2" y="2" stroke="#FEE1D3" stroke-width="4" rx="10"/><rect width="8" height="20" x="14" y="14" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="14" y="42" fill="#FC6D26" rx="4"/><rect width="8" height="20" x="30" y="14" fill="#EEE" rx="4"/><rect width="8" height="20" x="30" y="42" fill="#FDC4A8" rx="4"/></g><g transform="translate(89 133)"><use fill="#000" filter="url(#h)" xlink:href="#i"/><use fill="#FFF" xlink:href="#i"/><rect width="32" height="128" x="2" y="2" stroke="#FEE1D3" stroke-width="4" rx="10"/><rect width="8" height="20" x="14" y="14" fill="#FDC4A8" rx="4"/><rect width="8" height="20" x="14" y="42" fill="#EEE" rx="4"/><rect width="8" height="20" x="14" y="70" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="14" y="98" fill="#FC6D26" rx="4"/></g><g transform="translate(181 161)"><use fill="#000" filter="url(#j)" xlink:href="#k"/><use fill="#FFF" xlink:href="#k"/><rect width="48" height="44" x="2" y="2" stroke="#E1DBF1" stroke-width="4" rx="10"/><rect width="8" height="20" x="30" y="14" fill="#6B4FBB" rx="4"/><rect width="8" height="20" x="14" y="14" fill="#C3B8E3" rx="4"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/epics/list.svg b/app/assets/images/illustrations/epics/list.svg
new file mode 100644
index 00000000000..be6c3fb76a5
--- /dev/null
+++ b/app/assets/images/illustrations/epics/list.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="430" height="300" viewBox="0 0 430 300"><defs><rect id="a" width="280" height="180" x="77" y="60" rx="10"/><rect id="c" width="100" height="48" rx="10"/><filter id="b" width="105%" height="120.8%" x="-2.5%" y="-5.2%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="e" width="84" height="76" rx="10"/><filter id="d" width="106%" height="113.2%" x="-3%" y="-3.3%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="g" width="52" height="76" rx="10"/><filter id="f" width="109.6%" height="113.2%" x="-4.8%" y="-3.3%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="i" width="36" height="132" rx="10"/><filter id="h" width="113.9%" height="107.6%" x="-6.9%" y="-1.9%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="k" width="52" height="48" rx="10"/><filter id="j" width="109.6%" height="120.8%" x="-4.8%" y="-5.2%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M0 0h430v300H0z"/><use fill="#FFF" xlink:href="#a"/><rect width="276" height="176" x="79" y="62" stroke="#EEE" stroke-width="4" rx="10"/><rect width="8" height="20" x="244" y="141" fill="#EEE" rx="4"/><rect width="8" height="20" x="311" y="103" fill="#EEE" rx="4"/><rect width="8" height="20" x="272" y="166" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="257" y="203" fill="#EEE" rx="4"/><rect width="8" height="20" x="169" y="77" fill="#EEE" rx="4"/><rect width="8" height="20" x="191" y="104" fill="#FFF" rx="4"/><rect width="8" height="20" x="140" y="157" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="154" y="119" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="163" y="205" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="294" y="186" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="327" y="73" fill="#FEE1D3" rx="4"/><g transform="translate(197 79)"><use fill="#000" filter="url(#b)" xlink:href="#c"/><use fill="#FFF" xlink:href="#c"/><rect width="96" height="44" x="2" y="2" stroke="#FEE1D3" stroke-width="4" rx="10"/><rect width="8" height="20" x="62" y="14" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="78" y="14" fill="#FDC4A8" rx="4"/><rect width="8" height="20" x="46" y="14" fill="#FC6D26" rx="4"/><rect width="8" height="20" x="30" y="14" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="14" y="14" fill="#EEE" rx="4"/></g><g transform="translate(57 30)"><use fill="#000" filter="url(#d)" xlink:href="#e"/><use fill="#FFF" xlink:href="#e"/><rect width="80" height="72" x="2" y="2" stroke="#E1DBF1" stroke-width="4" rx="10"/><rect width="8" height="20" x="62" y="42" fill="#6B4FBB" rx="4"/><rect width="8" height="20" x="46" y="42" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="30" y="42" fill="#C3B8E3" rx="4"/><rect width="8" height="20" x="14" y="42" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="62" y="14" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="46" y="14" fill="#C3B8E3" rx="4"/><rect width="8" height="20" x="30" y="14" fill="#EEE" rx="4"/><rect width="8" height="20" x="14" y="14" fill="#C3B8E3" rx="4"/></g><g transform="translate(317 133)"><use fill="#000" filter="url(#f)" xlink:href="#g"/><use fill="#FFF" xlink:href="#g"/><rect width="48" height="72" x="2" y="2" stroke="#FEE1D3" stroke-width="4" rx="10"/><rect width="8" height="20" x="14" y="14" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="14" y="42" fill="#FC6D26" rx="4"/><rect width="8" height="20" x="30" y="14" fill="#EEE" rx="4"/><rect width="8" height="20" x="30" y="42" fill="#FDC4A8" rx="4"/></g><g transform="translate(89 133)"><use fill="#000" filter="url(#h)" xlink:href="#i"/><use fill="#FFF" xlink:href="#i"/><rect width="32" height="128" x="2" y="2" stroke="#FEE1D3" stroke-width="4" rx="10"/><rect width="8" height="20" x="14" y="14" fill="#FDC4A8" rx="4"/><rect width="8" height="20" x="14" y="42" fill="#EEE" rx="4"/><rect width="8" height="20" x="14" y="70" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="14" y="98" fill="#FC6D26" rx="4"/></g><g transform="translate(181 161)"><use fill="#000" filter="url(#j)" xlink:href="#k"/><use fill="#FFF" xlink:href="#k"/><rect width="48" height="44" x="2" y="2" stroke="#E1DBF1" stroke-width="4" rx="10"/><rect width="8" height="20" x="30" y="14" fill="#6B4FBB" rx="4"/><rect width="8" height="20" x="14" y="14" fill="#C3B8E3" rx="4"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/epics/roadmap.svg b/app/assets/images/illustrations/epics/roadmap.svg
new file mode 100644
index 00000000000..a14f14b91c9
--- /dev/null
+++ b/app/assets/images/illustrations/epics/roadmap.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="415" height="289" viewBox="0 0 415 289"><g fill="none" fill-rule="evenodd"><path d="M-9-5h430v300H-9z"/><g transform="translate(68 47)"><rect width="284" height="208" y="5" fill="#F9F9F9" fill-rule="nonzero" rx="10"/><rect width="284" height="208" fill="#FFF" fill-rule="nonzero" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v188a6 6 0 0 0 6 6h264a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h264c5.523 0 10 4.477 10 10v188c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><path fill="#E1DBF1" d="M25.168 153.995c3.837-.215 7.173.028 10.119.691a3 3 0 1 0 1.318-5.853c-3.509-.79-7.4-1.074-11.773-.828a3 3 0 1 0 .336 5.99z"/><path fill="#C3B8E3" d="M44.211 158.655c2.401 1.704 4.388 3.61 7.569 7.083a3 3 0 1 0 4.424-4.054c-3.448-3.763-5.686-5.911-8.522-7.923a3 3 0 1 0-3.471 4.894z"/><path fill="#E1DBF1" d="M59.786 173.828c3.181 2.675 6.52 4.665 10.397 6.039a3 3 0 0 0 2.004-5.655c-3.162-1.121-5.884-2.743-8.54-4.976a3 3 0 1 0-3.861 4.592zm22.133 8.148c1.02.037 2.067.045 3.143.023a72.664 72.664 0 0 0 8.346-.638 3 3 0 0 0-.812-5.945c-2.442.334-4.996.53-7.658.585a48.55 48.55 0 0 1-2.796-.021 3 3 0 1 0-.223 5.996z"/><path fill="#C3B8E3" d="M104.697 178.69c3.9-1.37 7.427-3.15 10.54-5.305a3 3 0 0 0-3.415-4.933c-2.665 1.845-5.712 3.382-9.114 4.578a3 3 0 0 0 1.989 5.66z"/><path fill="#E1DBF1" d="M123.853 165.07a33.752 33.752 0 0 0 5.276-10.817 3 3 0 1 0-5.773-1.633 27.753 27.753 0 0 1-4.341 8.9 3 3 0 0 0 4.838 3.55zm6.577-22.657c-.187-3.817-.926-7.71-2.204-11.596a3 3 0 1 0-5.7 1.874c1.113 3.384 1.75 6.745 1.91 10.016a3 3 0 1 0 5.994-.294z"/><path fill="#C3B8E3" d="M123.333 120.153c-1.897-3.2-4.152-6.325-6.748-9.344a3 3 0 0 0-4.55 3.913c2.372 2.756 4.421 5.597 6.136 8.49a3 3 0 1 0 5.162-3.06v.001z"/><path fill="#E1DBF1" d="M111.787 102.36c-.938-3.025-1.402-6.42-1.365-9.976a3 3 0 1 0-6-.063c-.043 4.163.506 8.177 1.634 11.816a3 3 0 1 0 5.731-1.777z"/><path fill="#C3B8E3" d="M111.84 82.253c.905-3.341 2.22-6.538 3.904-9.448a3 3 0 0 0-5.194-3.004c-1.948 3.368-3.463 7.048-4.501 10.884a3 3 0 1 0 5.791 1.568z"/><path fill="#E1DBF1" d="M121.974 64.948c2.475-2.28 5.265-4.09 8.335-5.374a3 3 0 1 0-2.314-5.536c-3.725 1.558-7.105 3.75-10.086 6.497a3 3 0 1 0 4.065 4.413zm18.177-7.586c3.202-.18 6.599.092 10.18.843a3 3 0 0 0 1.23-5.872c-4.086-.857-8.009-1.172-11.747-.962a3 3 0 1 0 .337 5.99v.001z"/><path fill="#C3B8E3" d="M160.198 61.312c3.068 1.268 6.232 2.842 9.487 4.728a3 3 0 1 0 3.009-5.191c-3.48-2.017-6.883-3.71-10.204-5.083a3 3 0 0 0-2.292 5.545v.001z"/><path fill="#E1DBF1" d="M179.776 71.267c3.711 1.586 7.376 2.77 10.997 3.565a3 3 0 0 0 1.286-5.86c-3.248-.713-6.555-1.782-9.925-3.222a3 3 0 1 0-2.358 5.517zm22.591 4.789c3.94-.04 7.808-.553 11.61-1.513a3 3 0 1 0-1.468-5.817 43.358 43.358 0 0 1-10.203 1.33 3 3 0 0 0 .061 6z"/><path fill="#C3B8E3" d="M224.887 70.498c3.335-1.637 6.607-3.613 9.845-5.916a3 3 0 0 0-3.477-4.89c-2.984 2.122-5.98 3.931-9.011 5.42a3 3 0 1 0 2.643 5.386z"/><path fill="#E1DBF1" d="M243.565 57.444a3 3 0 0 1-4.02-4.454 130.547 130.547 0 0 0 5.31-5.088 3 3 0 1 1 4.265 4.22 136.506 136.506 0 0 1-5.555 5.322z"/><path fill="#FDC4A8" d="M194.843 83.085a3 3 0 1 1 4.314-4.17c3.056 3.16 5.075 6.744 6.172 10.754a3 3 0 0 1-5.787 1.584c-.834-3.047-2.35-5.739-4.699-8.168z"/><path fill="#FEE1D3" d="M200.19 101.134a3 3 0 1 1 5.978.52c-.282 3.232-.805 6.273-1.832 11.206a3 3 0 0 1-5.874-1.222c.981-4.717 1.473-7.572 1.728-10.504z"/><path fill="#FDC4A8" d="M196.413 122.689a3 3 0 0 1 5.953.747c-.5 3.988-.397 7.09.399 9.67a3 3 0 1 1-5.733 1.769c-1.087-3.52-1.217-7.426-.62-12.186h.001z"/><path fill="#FEE1D3" d="M203.806 145.133a3 3 0 0 1 4.461-4.013c2.703 3.005 5.224 5.296 7.594 6.947a3 3 0 0 1-3.429 4.924c-2.775-1.932-5.632-4.53-8.626-7.858zm20.352 12.28a3 3 0 1 1 .334-5.99c2.77.154 5.453-.554 9.224-2.254a3 3 0 0 1 2.466 5.47c-4.57 2.06-8.103 2.993-12.024 2.775v-.001z"/><path fill="#FDC4A8" d="M245.942 150.355a3 3 0 0 1-1.815-5.719c4.227-1.342 8.24-1.61 12.496-.572a3 3 0 0 1-1.421 5.83c-3.116-.76-6.025-.566-9.26.46v.001zM106.53 56.038a3 3 0 0 1-3.45 4.909c-1.074-.755-6.723-6.044-8.083-7.204l-.332-.281a3 3 0 1 1 3.865-4.59l.362.306c1.643 1.402 6.971 6.391 7.638 6.86z"/><path fill="#FEE1D3" d="M88.536 42.422a3 3 0 0 1-2.285 5.548c-3.14-1.293-5.78-1.34-8.105-.05a3 3 0 0 1-2.91-5.247c4.087-2.266 8.597-2.187 13.3-.25v-.001z"/><path fill="#FDC4A8" d="M66.698 48.73a3 3 0 0 1 2.029 5.647c-4.432 1.592-8.786.835-13.166-1.88a3 3 0 1 1 3.16-5.1c2.93 1.816 5.425 2.25 7.977 1.333z"/><path fill="#FEE1D3" d="M51.062 40.692a3 3 0 0 1-4.352 4.13c-.911-.96-1.85-1.98-3.061-3.32-.295-.325-2.437-2.703-3.07-3.4-.47-.518-.9-.988-1.313-1.436a3 3 0 0 1 4.41-4.068c.425.46.866.942 1.346 1.47.642.709 2.79 3.092 3.076 3.41a180.865 180.865 0 0 0 2.964 3.214z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M254.66 72.196l2-3.464a2 2 0 1 0-3.464-2l-2 3.464-3.464-2a2 2 0 0 0-2 3.464l3.464 2-2 3.464a2 2 0 0 0 3.464 2l2-3.464 3.464 2a2 2 0 1 0 2-3.464l-3.464-2zm-151.904 78.732l2.829-2.828a2 2 0 1 0-2.829-2.829l-2.828 2.829-2.828-2.829a2 2 0 1 0-2.829 2.829l2.829 2.828-2.829 2.829a2 2 0 0 0 2.829 2.828l2.828-2.828 2.828 2.828a2 2 0 1 0 2.829-2.828l-2.829-2.829z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M210.66 173.66l3.464-2a2 2 0 1 0-2-3.464l-3.464 2-2-3.464a2 2 0 0 0-3.464 2l2 3.464-3.464 2a2 2 0 1 0 2 3.464l3.464-2 2 3.464a2 2 0 1 0 3.464-2l-2-3.464z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M27 181a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-4a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M138 85a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M200 57a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#FC6D26" fill-rule="nonzero" d="M222.647 121.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M103.647 28.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M85 103.488L81.841 108h6.318L85 103.488zm6.436 2.218A4 4 0 0 1 88.159 112H81.84a4 4 0 0 1-3.277-6.294l3.16-4.512a4 4 0 0 1 6.553 0l3.159 4.512h.001z"/></g><path fill="#F9F9F9" fill-rule="nonzero" d="M327.376 93.43A48.805 48.805 0 0 0 359 105c27.062 0 49-21.938 49-49S386.062 7 359 7s-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71v-.001z"/><path fill="#FFF" fill-rule="nonzero" d="M332.376 88.43A48.805 48.805 0 0 0 364 100c27.062 0 49-21.938 49-49S391.062 2 364 2s-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71v-.001z"/><path fill="#EEE" fill-rule="nonzero" d="M322.85 93.072a4.5 4.5 0 0 1-5.516-5.517l2.827-10.48C315.501 69.258 313 60.31 313 51c0-28.167 22.833-51 51-51s51 22.833 51 51-22.833 51-51 51c-11.859 0-23.096-4.064-32.102-11.37l-9.048 2.442zm10.817-6.169C342.091 94.027 352.737 98 364 98c25.957 0 47-21.043 47-47S389.957 4 364 4s-47 21.043-47 47c0 8.859 2.453 17.351 7.016 24.716l.456.737-3.277 12.144c.072.527.347.685.613.613l11.059-2.984.8.677z"/><g fill-rule="nonzero" transform="translate(347 28)"><path fill="#E1DBF1" d="M13 4a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-8zm0-4h8a5 5 0 0 1 5 5v1a5 5 0 0 1-5 5h-8a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z"/><path fill="#6B4FBB" d="M5 11a1 1 0 0 0 0 2h24a1 1 0 0 0 0-2H5zm0-4h24a5 5 0 0 1 0 10H5A5 5 0 0 1 5 7z"/><rect width="12" height="4" x="11" y="31" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="19" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="37" fill="#E1DBF1" rx="2"/><rect width="12" height="4" x="11" y="43" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="25" fill="#E1DBF1" rx="2"/></g><g fill-rule="nonzero"><path fill="#F9F9F9" d="M337.238 219.072A38.83 38.83 0 0 1 361 211c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213h-.001z"/><path fill="#FFF" d="M341.238 215.072A38.83 38.83 0 0 1 365 207c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213h-.001z"/><path fill="#EEE" d="M329.85 209.928a4.5 4.5 0 0 0-5.516 5.517l3.543 13.13A40.848 40.848 0 0 0 324 246c0 22.644 18.356 41 41 41s41-18.356 41-41-18.356-41-41-41a40.82 40.82 0 0 0-24.182 7.887l-10.968-2.96v.001zm12.608 6.73A36.824 36.824 0 0 1 365 209c20.435 0 37 16.565 37 37s-16.565 37-37 37-37-16.565-37-37c0-5.747 1.31-11.304 3.795-16.343l.334-.677-3.934-14.577a.5.5 0 0 1 .613-.613l12.865 3.471.785-.604v.001z"/><path fill="#FEE1D3" d="M348.097 250.962a7 7 0 0 0 8.81 10.88l1.093-.885v1.454a7 7 0 0 0 14 0v-1.454l1.092.885a7 7 0 1 0 8.81-10.88l-1.185-.96 1.455-.337a7 7 0 1 0-3.15-13.64l-1.4.323.623-1.278a7 7 0 0 0-12.583-6.137l-.662 1.356-.662-1.356a7 7 0 0 0-12.583 6.137l.623 1.278-1.4-.324a7 7 0 1 0-3.15 13.641l1.455.336-1.186.96v.001zm5.464-.913a11.914 11.914 0 0 1-.444-1.95l-.19-1.362-4.2-.97a3 3 0 0 1 1.35-5.845l4.178.964.768-1.145c.373-.557.793-1.082 1.254-1.57l.95-1.006-1.877-3.849a3 3 0 0 1 5.393-2.63l1.892 3.879 1.363-.113a12.188 12.188 0 0 1 2.004 0l1.363.113 1.892-3.879a3 3 0 0 1 5.393 2.63l-1.877 3.849.95 1.006c.461.488.88 1.013 1.254 1.57l.768 1.145 4.178-.964a3 3 0 1 1 1.35 5.846l-4.2.97-.19 1.36a11.914 11.914 0 0 1-.444 1.95l-.413 1.302 3.36 2.72a3 3 0 1 1-3.776 4.663l-3.32-2.688-1.196.706a11.94 11.94 0 0 1-1.808.873l-1.286.492v4.295a3 3 0 0 1-6 0v-4.295l-1.286-.492a11.94 11.94 0 0 1-1.808-.873l-1.196-.706-3.32 2.688a3 3 0 0 1-3.776-4.663l3.36-2.72-.413-1.301z"/><path fill="#FC6D26" d="M365 240.411a6 6 0 1 0 0 12 6 6 0 0 0 0-12zm0 4a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/></g><g fill-rule="nonzero"><path fill="#F9F9F9" d="M87.624 156.43A48.805 48.805 0 0 1 56 168c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71v-.001z"/><path fill="#FFF" stroke="#EEE" stroke-width="4" d="M82.624 151.43A48.805 48.805 0 0 1 51 163c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71v-.001z"/><path fill="#EEE" d="M92.15 156.072a4.5 4.5 0 0 0 5.516-5.517l-2.827-10.48C99.499 132.258 102 123.31 102 114c0-28.167-22.833-51-51-51S0 85.833 0 114s22.833 51 51 51c11.859 0 23.096-4.064 32.102-11.37l9.048 2.442zm-10.817-6.169C72.909 157.027 62.263 161 51 161c-25.957 0-47-21.043-47-47s21.043-47 47-47 47 21.043 47 47c0 8.859-2.453 17.351-7.016 24.716l-.456.737 3.277 12.144c-.072.527-.347.685-.613.613l-11.059-2.984-.8.677z"/><path fill="#FEE1D3" d="M48.47 88.47l-16.148 6.688a4 4 0 0 0-2.164 2.164l-6.689 16.147a4 4 0 0 0 0 3.062l6.689 16.147a4 4 0 0 0 2.164 2.164l16.147 6.689a4 4 0 0 0 3.062 0l16.147-6.689a4 4 0 0 0 2.164-2.164l6.689-16.147a4 4 0 0 0 0-3.062l-6.689-16.147a4 4 0 0 0-2.164-2.164L51.53 88.469a4 4 0 0 0-3.062 0l.002.001zM50 92.164l16.147 6.688L72.835 115l-6.688 16.147L50 137.835l-16.147-6.688L27.165 115l6.688-16.147L50 92.165v-.001zM50 101a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 20c6.075 0 11-4.925 11-11s-4.925-11-11-11-11 4.925-11 11 4.925 11 11 11zm0-4a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/><path fill="#FC6D26" d="M50 120.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0-3a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/issue-dashboard_results-without-filiter.svg b/app/assets/images/illustrations/issue-dashboard_results-without-filiter.svg
new file mode 100644
index 00000000000..cd9070425b0
--- /dev/null
+++ b/app/assets/images/illustrations/issue-dashboard_results-without-filiter.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="104" height="104" viewBox="0 0 104 104"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M9.565 27.5a2 2 0 0 1 3.464 2C6.226 41.283 5.18 55.239 9.843 67.745a2 2 0 0 1-3.748 1.397C1.02 55.53 2.159 40.328 9.565 27.5zM27.5 94.435a2 2 0 0 1 2-3.464c9.19 5.306 19.725 7.137 29.97 5.406a2 2 0 1 1 .667 3.944A48.798 48.798 0 0 1 27.5 94.435zM94.435 76.5a2 2 0 0 1-3.464-2 44.808 44.808 0 0 0 5.261-30.78 2 2 0 0 1 3.932-.738A48.807 48.807 0 0 1 94.435 76.5zM76.5 9.565a2 2 0 0 1-2 3.464C62.824 6.288 49.012 5.198 36.585 9.72a2 2 0 0 1-1.368-3.758C48.744 1.039 63.789 2.226 76.5 9.565z"/><path fill="#EEE" fill-rule="nonzero" d="M62.612 12.397a2 2 0 0 1-1.036 3.864C51.55 13.574 41.08 15.214 32.44 20.593a2 2 0 0 1-2.114-3.396c9.571-5.958 21.18-7.776 32.287-4.8zM12.397 41.388a2 2 0 0 1 3.864 1.036 36.854 36.854 0 0 0 2.986 26.787 2 2 0 1 1-3.54 1.862 40.854 40.854 0 0 1-3.31-29.685zm28.991 50.215a2 2 0 0 1 1.036-3.864c10.098 2.706 20.644 1.022 29.316-4.445a2 2 0 1 1 2.134 3.384c-9.606 6.056-21.299 7.922-32.486 4.925zm50.215-28.991a2 2 0 0 1-3.864-1.036 36.86 36.86 0 0 0-3.208-27.204 2 2 0 1 1 3.517-1.906 40.859 40.859 0 0 1 3.555 30.146z"/><path fill="#EEE" d="M22.717 56.065c2.039 14.585 14.563 25.81 29.71 25.81 16.569 0 30-13.431 30-30v-.077c.191 1.369.29 2.768.29 4.19 0 16.568-13.431 30-30 30-16.543 0-29.958-13.39-30-29.923z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M52.427 83.875c-17.673 0-32-14.327-32-32 0-17.673 14.327-32 32-32 17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32zm0-4c15.464 0 28-12.536 28-28s-12.536-28-28-28-28 12.536-28 28 12.536 28 28 28zm6.877-16.24a2 2 0 1 1-2.882 2.773 4.979 4.979 0 0 0-3.603-1.533 4.977 4.977 0 0 0-3.539 1.468 2 2 0 1 1-2.83-2.826 8.976 8.976 0 0 1 6.369-2.642 8.98 8.98 0 0 1 6.485 2.76z"/><path fill="#6B4FBB" d="M41.755 49.774l2.122 2.122a2 2 0 1 1-2.829 2.828l-2.121-2.121-2.121 2.121a2 2 0 0 1-2.829-2.828l2.122-2.122-2.122-2.12a2 2 0 1 1 2.829-2.83l2.121 2.122 2.121-2.121a2 2 0 0 1 2.829 2.828l-2.122 2.121zm27 0l2.122 2.122a2 2 0 1 1-2.829 2.828l-2.121-2.121-2.121 2.121a2 2 0 0 1-2.829-2.828l2.122-2.122-2.122-2.12a2 2 0 1 1 2.829-2.83l2.121 2.122 2.121-2.121a2 2 0 0 1 2.829 2.828l-2.122 2.121z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/lock_promotion.svg b/app/assets/images/illustrations/lock_promotion.svg
new file mode 100644
index 00000000000..6f1f9b2b030
--- /dev/null
+++ b/app/assets/images/illustrations/lock_promotion.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><circle cx="39" cy="45" r="37" fill="#F9F9F9"/><circle cx="39" cy="39" r="37" fill="#FFF"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M30 35a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V39a4 4 0 0 0-4-4H30zm0-4h18a8 8 0 0 1 8 8v10a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V39a8 8 0 0 1 8-8z"/><path fill="#6B4FBB" d="M33 29v2h-4v-2c0-5.523 4.477-10 10-10a9.996 9.996 0 0 1 10 10v2h-4v-2a5.997 5.997 0 0 0-6-6 6 6 0 0 0-6 6zm5 13.732a2 2 0 1 1 2 0V44a1 1 0 0 1-2 0v-1.268z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/milestone_removing-page.svg b/app/assets/images/illustrations/milestone_removing-page.svg
new file mode 100644
index 00000000000..a8cd54da0d3
--- /dev/null
+++ b/app/assets/images/illustrations/milestone_removing-page.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42C3.647 61.032 19.575 76 39 76c19.425 0 35.353-14.968 36.88-34 .08.99.12 1.99.12 3 0 20.435-16.565 37-37 37S2 65.435 2 45c0-1.01.04-2.01.12-3z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FDE1D4" fill-rule="nonzero" d="M25.934 25.205a3 3 0 1 1 4.127 4.356A12.952 12.952 0 0 0 26 39c0 7.18 5.82 13 13 13s13-5.82 13-13c0-3.621-1.484-6.999-4.063-9.44a3 3 0 1 1 4.126-4.357A18.951 18.951 0 0 1 58 39c0 10.493-8.507 19-19 19s-19-8.507-19-19a18.951 18.951 0 0 1 5.934-13.795z"/><rect width="6" height="18" x="36" y="17" fill="#FA6D34" rx="3"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/profile-page/activity.svg b/app/assets/images/illustrations/profile-page/activity.svg
new file mode 100644
index 00000000000..a0304479737
--- /dev/null
+++ b/app/assets/images/illustrations/profile-page/activity.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M10 71.092a3 3 0 0 0 3.413-1.047L22.308 58H66a6 6 0 0 0 6-6V19.528A5.985 5.985 0 0 1 74 24v33a6 6 0 0 1-6 6H24.308l-8.895 12.045A3 3 0 0 1 10 73.263v-2.17z"/><path fill="#EEE" fill-rule="nonzero" d="M20.699 56.812A2 2 0 0 1 22.308 56H66a4 4 0 0 0 4-4V19a4 4 0 0 0-4-4H14a4 4 0 0 0-4 4v49.263a1 1 0 0 0 1.804.594L20.7 56.812zM23.317 60l-8.295 11.233A5 5 0 0 1 6 68.263V19a8 8 0 0 1 8-8h52a8 8 0 0 1 8 8v33a8 8 0 0 1-8 8H23.317z"/><rect width="26" height="4" x="27" y="26" fill="#6B4FBB" rx="2"/><rect width="26" height="4" x="27" y="34" fill="#C3B8E3" rx="2"/><rect width="26" height="4" x="27" y="42" fill="#E1DBF1" rx="2"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/profile-page/contributed-projects.svg b/app/assets/images/illustrations/profile-page/contributed-projects.svg
new file mode 100644
index 00000000000..023a7e3b956
--- /dev/null
+++ b/app/assets/images/illustrations/profile-page/contributed-projects.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M31.526 21.774c.152.38.266.78.339 1.198l.005.028h-.13l-.214-1.226zM69 30.682c.632.95 1 2.091 1 3.318v6.08c2.86 5.48 2.867 12.028 0 17.41v3.267l7.255 7.255a3 3 0 0 1 0 4.243l-1.414 1.414a3 3 0 0 1-4.243 0l-4.519-4.518c-.9.539-1.953.849-3.079.849H12a6 6 0 0 1-6-6v-1.682A5.994 5.994 0 0 0 11 65h52a6 6 0 0 0 6-6V30.682z"/><path fill="#EEE" fill-rule="nonzero" d="M31.74 25a2 2 0 0 1-1.971-1.657l-.875-5.028A4 4 0 0 0 24.954 15H11a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4h52a4 4 0 0 0 4-4V29a4 4 0 0 0-4-4H31.74zm1.681-4H63a8 8 0 0 1 8 8v30a8 8 0 0 1-8 8H11a8 8 0 0 1-8-8V19a8 8 0 0 1 8-8h13.953a8 8 0 0 1 7.882 6.63l.586 3.37z"/><path fill="#E1DBF1" d="M60.991 56.82l1.414-1.414a3 3 0 0 1 4.243 0l9.9 9.9a3 3 0 0 1 0 4.242l-1.415 1.414a3 3 0 0 1-4.242 0l-9.9-9.9a3 3 0 0 1 0-4.242z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M66.294 59.295c-7.224 7.225-18.938 7.225-26.163 0-7.224-7.225-7.224-18.938 0-26.163 7.225-7.225 18.939-7.225 26.163 0 7.225 7.225 7.225 18.938 0 26.163zm-4.95-4.95c4.492-4.49 4.492-11.772 0-16.263-4.49-4.491-11.772-4.491-16.263 0-4.49 4.49-4.49 11.772 0 16.263 4.491 4.491 11.773 4.491 16.264 0z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/profile-page/groups.svg b/app/assets/images/illustrations/profile-page/groups.svg
new file mode 100644
index 00000000000..97aca4d69ee
--- /dev/null
+++ b/app/assets/images/illustrations/profile-page/groups.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M5 23v-2 2zm70 40v4a6 6 0 0 1-6 6H11a6 6 0 0 1-6-6v-4a6 6 0 0 0 6 6h58a6 6 0 0 0 6-6z"/><path stroke="#EEE" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M32.087 23H69a6 6 0 0 1 6 6v34a6 6 0 0 1-6 6H11a6 6 0 0 1-6-6V17a6 6 0 0 1 6-6h13.953a6 6 0 0 1 5.912 4.972L32.087 23z"/><path fill="#6B4FBB" d="M38 44v-7a2 2 0 1 1 4 0v7h7a2 2 0 1 1 0 4h-7v7a2 2 0 1 1-4 0v-7h-7a2 2 0 1 1 0-4h7z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/profile-page/personal-project.svg b/app/assets/images/illustrations/profile-page/personal-project.svg
new file mode 100644
index 00000000000..3e8df10ed38
--- /dev/null
+++ b/app/assets/images/illustrations/profile-page/personal-project.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M67 66v5a6 6 0 0 1-6 6H19a6 6 0 0 1-6-6v-5a6 6 0 0 0 6 6h42a6 6 0 0 0 6-6z"/><path fill="#EEE" fill-rule="nonzero" d="M44 6v4H19a4 4 0 0 0-4 4v52a4 4 0 0 0 4 4h42a4 4 0 0 0 4-4V14a4.002 4.002 0 0 0-3-3.874V6.062c3.946.492 7 3.858 7 7.938v52a8 8 0 0 1-8 8H19a8 8 0 0 1-8-8V14a8 8 0 0 1 8-8h25z"/><path fill="#E1DBF1" d="M55.142 41.9l-5.657-5.657a2 2 0 1 1 2.829-2.829l7.07 7.071a2 2 0 0 1 0 2.829l-7.07 7.07a2 2 0 1 1-2.829-2.828l5.657-5.657zm-29.485 0l5.657 5.656a2 2 0 1 1-2.829 2.829l-7.07-7.071a2 2 0 0 1 0-2.829l7.07-7.07a2 2 0 1 1 2.829 2.828l-5.657 5.656z"/><path fill="#6B4FBB" d="M35 44a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm6 0a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm6 0a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/><path fill="#EFEDF8" d="M47 2h12a3 3 0 0 1 3 3v19.91a1.5 1.5 0 0 1-2.308 1.265l-5.81-3.714a1.5 1.5 0 0 0-1.605-.006l-5.98 3.753A1.5 1.5 0 0 1 44 24.937V5a3 3 0 0 1 3-3z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M48 20.416l2.15-1.35a5.5 5.5 0 0 1 5.886.025L58 20.346V6H48v14.416zM47 2h12a3 3 0 0 1 3 3v19.91a1.5 1.5 0 0 1-2.308 1.265l-5.81-3.714a1.5 1.5 0 0 0-1.605-.006l-5.98 3.753A1.5 1.5 0 0 1 44 24.937V5a3 3 0 0 1 3-3z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/profile-page/personal-projects.svg b/app/assets/images/illustrations/profile-page/personal-projects.svg
new file mode 100644
index 00000000000..023a7e3b956
--- /dev/null
+++ b/app/assets/images/illustrations/profile-page/personal-projects.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M31.526 21.774c.152.38.266.78.339 1.198l.005.028h-.13l-.214-1.226zM69 30.682c.632.95 1 2.091 1 3.318v6.08c2.86 5.48 2.867 12.028 0 17.41v3.267l7.255 7.255a3 3 0 0 1 0 4.243l-1.414 1.414a3 3 0 0 1-4.243 0l-4.519-4.518c-.9.539-1.953.849-3.079.849H12a6 6 0 0 1-6-6v-1.682A5.994 5.994 0 0 0 11 65h52a6 6 0 0 0 6-6V30.682z"/><path fill="#EEE" fill-rule="nonzero" d="M31.74 25a2 2 0 0 1-1.971-1.657l-.875-5.028A4 4 0 0 0 24.954 15H11a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4h52a4 4 0 0 0 4-4V29a4 4 0 0 0-4-4H31.74zm1.681-4H63a8 8 0 0 1 8 8v30a8 8 0 0 1-8 8H11a8 8 0 0 1-8-8V19a8 8 0 0 1 8-8h13.953a8 8 0 0 1 7.882 6.63l.586 3.37z"/><path fill="#E1DBF1" d="M60.991 56.82l1.414-1.414a3 3 0 0 1 4.243 0l9.9 9.9a3 3 0 0 1 0 4.242l-1.415 1.414a3 3 0 0 1-4.242 0l-9.9-9.9a3 3 0 0 1 0-4.242z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M66.294 59.295c-7.224 7.225-18.938 7.225-26.163 0-7.224-7.225-7.224-18.938 0-26.163 7.225-7.225 18.939-7.225 26.163 0 7.225 7.225 7.225 18.938 0 26.163zm-4.95-4.95c4.492-4.49 4.492-11.772 0-16.263-4.49-4.491-11.772-4.491-16.263 0-4.49 4.49-4.49 11.772 0 16.263 4.491 4.491 11.773 4.491 16.264 0z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/profile-page/snippets.svg b/app/assets/images/illustrations/profile-page/snippets.svg
new file mode 100644
index 00000000000..ef0275cede3
--- /dev/null
+++ b/app/assets/images/illustrations/profile-page/snippets.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M23 12a2 2 0 1 1 0-4h3a2 2 0 1 1 0 4h-3zm11 0a2 2 0 1 1 0-4h3a2 2 0 1 1 0 4h-3zm11 0a2 2 0 1 1 0-4h3a2 2 0 1 1 0 4h-3zm11 0a2 2 0 1 1 0-4h3a2 2 0 1 1 0 4h-3zm11 0a2 2 0 1 1 0-4c1.576 0 3.061.613 4.172 1.688a2 2 0 1 1-2.782 2.874A1.986 1.986 0 0 0 67 12zm2 6.877a2 2 0 1 1 4 0v3a2 2 0 1 1-4 0v-3zm0 11a2 2 0 1 1 4 0v3a2 2 0 1 1-4 0v-3zm0 11a2 2 0 1 1 4 0v3a2 2 0 1 1-4 0v-3zm0 11a2 2 0 1 1 4 0v3a2 2 0 1 1-4 0v-3zm-1.348 8.015a2 2 0 0 1 1.303 3.782 6.042 6.042 0 0 1-1.877.325L65.247 64a2 2 0 1 1-.002-4h1.794a1.99 1.99 0 0 0 .613-.108zM57.246 60a2 2 0 1 1 0 4h-3a2 2 0 1 1 0-4h3zm-11 0a2 2 0 1 1 0 4h-3a2 2 0 1 1 0-4h3zM21 32.369a2 2 0 1 1-4 0v-3a2 2 0 1 1 4 0v3zm0-11a2 2 0 1 1-4 0v-3a2 2 0 1 1 4 0v3z"/><g transform="rotate(30 -8.776 48.352)"><path fill="#E1DBF1" fill-rule="nonzero" d="M31.021 27.176l-1.732-3 4.365-7.561c2.01-3.48 2.455-6.572 1.81-9.285a9.09 9.09 0 0 0-.962-2.446c-.231-.4-.419-.655-.508-.756L25.85 18.222l-1.735-3.006 7.38-12.782c1.164-2.016 3.6-2.128 4.842-.18.205.25.472.628.761 1.129.542.937.99 2.024 1.283 3.252.823 3.457.264 7.335-2.13 11.48l-5.231 9.06zm-6.905 11.96L20.55 45.31l-10.357-5.98 7.018-12.155 1.733 3.002-4.653 8.055 5.161 2.98 2.932-5.077 1.732 3z"/><path fill="#6B4FBB" d="M8.282 54.601a8.96 8.96 0 1 1 8.96-15.519 8.96 8.96 0 0 1-8.96 15.52zm2.24-3.88a4.48 4.48 0 1 0 4.48-7.759 4.48 4.48 0 0 0-4.48 7.76z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M33.94 38.233L14.239 4.128c-.09.1-.277.357-.508.756a9.09 9.09 0 0 0-.962 2.446c-.646 2.713-.2 5.805 1.81 9.285l14.201 24.598 5.162-2.98zM11.134 3.383c.289-.501.555-.879.761-1.13 1.241-1.947 3.678-1.835 4.843.181l21.302 36.897-10.358 5.98L11.98 18.115c-2.393-4.145-2.952-8.023-2.13-11.48a12.075 12.075 0 0 1 1.284-3.252z"/><circle cx="24.39" cy="27.383" r="2.24" fill="#6B4FBB" transform="scale(-1 1) rotate(30 0 -63.642)"/><path fill="#6B4FBB" d="M39.95 54.602a8.96 8.96 0 1 1-8.96-15.52 8.96 8.96 0 0 1 8.96 15.52zm-2.24-3.88a4.48 4.48 0 1 0-4.48-7.76 4.48 4.48 0 0 0 4.48 7.76z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/prometheus-graphs_empty.svg b/app/assets/images/illustrations/prometheus-graphs_empty.svg
new file mode 100644
index 00000000000..0ef198f59cd
--- /dev/null
+++ b/app/assets/images/illustrations/prometheus-graphs_empty.svg
@@ -0,0 +1 @@
+<svg width="430" height="267" viewBox="0 0 430 267" xmlns="http://www.w3.org/2000/svg"><title>prometheus-graphs_empty</title><g fill="none" fill-rule="evenodd"><path d="M187 43a2 2 0 0 1 0-4h8a2 2 0 0 1 0 4h-8zm18 0a2 2 0 0 1 0-4h8a2 2 0 0 1 0 4h-8zm18 0a2 2 0 0 1 0-4h8a2 2 0 0 1 0 4h-8zm18 0a2 2 0 0 1 0-4h8a2 2 0 0 1 0 4h-8zm18 0a2 2 0 0 1 0-4h8a2 2 0 0 1 0 4h-8zm18 0a2 2 0 0 1 0-4h8a2 2 0 0 1 0 4h-8zm18 0a2 2 0 0 1 0-4h8a2 2 0 0 1 0 4h-8zm18 0a2 2 0 0 1 0-4h8a2 2 0 0 1 0 4h-8zm15.769 2.457a2 2 0 0 1 2.883-2.772A11.964 11.964 0 0 1 335 51v.39a2 2 0 0 1-4 0V51a7.965 7.965 0 0 0-2.231-5.543zm2.225 159.857a2 2 0 0 1 3.997.154 11.973 11.973 0 0 1-4.02 8.503 2 2 0 0 1-2.658-2.99 7.974 7.974 0 0 0 2.681-5.667zM320.218 213a2 2 0 1 1 0 4h-8a2 2 0 0 1 0-4h8zm-18 0a2 2 0 1 1 0 4h-8a2 2 0 0 1 0-4h8zm-18 0a2 2 0 1 1 0 4h-8a2 2 0 0 1 0-4h8zm-18 0a2 2 0 1 1 0 4h-8a2 2 0 0 1 0-4h8zm-18 0a2 2 0 1 1 0 4h-8a2 2 0 0 1 0-4h8zm-18 0a2 2 0 1 1 0 4h-8a2 2 0 0 1 0-4h8zm-18 0a2 2 0 1 1 0 4h-8a2 2 0 0 1 0-4h8zm-18 0a2 2 0 1 1 0 4h-8a2 2 0 0 1 0-4h8zm-18 0a2 2 0 1 1 0 4h-8a2 2 0 0 1 0-4h8zm-18 0a2 2 0 1 1 0 4h-8a2 2 0 0 1 0-4h8zM89 155.827a2 2 0 1 1-4 0v-8a2 2 0 0 1 4 0v8zm0-18a2 2 0 1 1-4 0v-8a2 2 0 0 1 4 0v8z" fill="#EEE" fill-rule="nonzero"/><path d="M175.116 125.116A10.002 10.002 0 0 1 166 131H54c-5.523 0-10-4.477-10-10V31a10 10 0 0 1 5.884-9.116A9.964 9.964 0 0 0 49 26v90c0 5.523 4.477 10 10 10h112c1.467 0 2.86-.316 4.116-.884z" fill="#F9F9F9"/><path d="M59 20a6 6 0 0 0-6 6v90a6 6 0 0 0 6 6h112a6 6 0 0 0 6-6V26a6 6 0 0 0-6-6H59zm0-4h112c5.523 0 10 4.477 10 10v90c0 5.523-4.477 10-10 10H59c-5.523 0-10-4.477-10-10V26c0-5.523 4.477-10 10-10z" fill="#EEE" fill-rule="nonzero"/><path d="M73 58h4a3 3 0 0 1 3 3v45H70V61a3 3 0 0 1 3-3z" fill="#EFEDF8"/><path d="M93 70h4a3 3 0 0 1 3 3v33H90V73a3 3 0 0 1 3-3z" fill="#E1DBF1"/><path d="M153 70h4a3 3 0 0 1 3 3v33h-10V73a3 3 0 0 1 3-3z" fill="#C3B8E3"/><path d="M133 89h4a3 3 0 0 1 3 3v14h-10V92a3 3 0 0 1 3-3z" fill="#EFEDF8"/><path d="M113 46h4a3 3 0 0 1 3 3v57h-10V49a3 3 0 0 1 3-3z" fill="#6B4FBB"/><path d="M385.4 191.418A10.004 10.004 0 0 1 376 198H221c-5.523 0-10-4.477-10-10V74a9.992 9.992 0 0 1 4.6-8.418A9.981 9.981 0 0 0 215 69v114c0 5.523 4.477 10 10 10h155c1.99 0 3.843-.58 5.4-1.582z" fill="#F9F9F9" opacity=".99"/><path d="M225 63a6 6 0 0 0-6 6v114a6 6 0 0 0 6 6h155a6 6 0 0 0 6-6V69a6 6 0 0 0-6-6H225zm0-4h155c5.523 0 10 4.477 10 10v114c0 5.523-4.477 10-10 10H225c-5.523 0-10-4.477-10-10V69c0-5.523 4.477-10 10-10z" fill="#EEE" fill-rule="nonzero"/><g transform="translate(238 92)"><path d="M44.584 31.917L28.808 49.434a2 2 0 0 1-2.695.255L3.791 32.749a2 2 0 0 1 2.418-3.186l20.858 15.828 15.966-17.73a2 2 0 0 1 2.912-.064l16.09 16.344L77.35.058c.641-1.838 3.263-1.77 3.808.098l19.827 67.92 22.3-37.106a2 2 0 0 1 3.43 2.06l-24.656 41.024c-.898 1.494-3.145 1.204-3.634-.47L79.07 7.273l-14.315 41.02a2 2 0 0 1-3.314.745L44.584 31.917z" fill="#FEF0E8" fill-rule="nonzero"/><circle fill="#FEE1D3" cx="100" cy="71" r="4"/><circle fill="#FDC4A8" cx="44" cy="30" r="4"/><circle stroke="#FC6D26" stroke-width="4" fill="#FFF" cx="5" cy="32" r="5"/><circle stroke="#FC6D26" stroke-width="4" fill="#FFF" cx="125" cy="32" r="5"/></g><path d="M80.41 166.41C72.69 175.07 68 186.486 68 199c0 27.062 21.938 49 49 49 12.513 0 23.93-4.69 32.59-12.41C140.618 245.66 127.55 252 113 252c-27.062 0-49-21.938-49-49 0-14.549 6.34-27.617 16.41-36.59z" fill="#F9F9F9"/><path d="M117 244c24.853 0 45-20.147 45-45s-20.147-45-45-45-45 20.147-45 45 20.147 45 45 45zm0 4c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49-21.938 49-49 49z" fill="#EEE" fill-rule="nonzero"/><path d="M117 223c-13.255 0-24-10.745-24-24s10.745-24 24-24 24 10.745 24 24-10.745 24-24 24zm0-12c6.627 0 12-5.373 12-12s-5.373-12-12-12-12 5.373-12 12 5.373 12 12 12z" fill="#FEF0E8"/><path d="M117 175c13.255 0 24 10.745 24 24 0 .674-.028 1.34-.082 2h-12.084c.11-.65.166-1.319.166-2 0-6.627-5.373-12-12-12v-12z" fill="#FDC4A8"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/web-ide_promotion.svg b/app/assets/images/illustrations/web-ide_promotion.svg
new file mode 100644
index 00000000000..ef61348057d
--- /dev/null
+++ b/app/assets/images/illustrations/web-ide_promotion.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="114" height="88" viewBox="0 0 114 88"><g fill="none" fill-rule="evenodd"><rect width="110" height="80" x="2" y="8" fill="#F9F9F9" rx="10"/><path fill="#FFF" d="M12 2h90c5.523 0 10 4.477 10 10v60c0 5.523-4.477 10-10 10H12C6.477 82 2 77.523 2 72V12C2 6.477 6.477 2 12 2z"/><path fill="#EEE" fill-rule="nonzero" d="M33 4v4.255A6.968 6.968 0 0 1 37 7h19a6.98 6.98 0 0 1 4.899 2H75a7 7 0 0 1 7 7v2h28v-6a8 8 0 0 0-8-8H33zm-3 0H12a8 8 0 0 0-8 8v60a8 8 0 0 0 8 8h18V4zm48 14v-2a3 3 0 0 0-3-3H62.93c.046.327.07.66.07 1v4h15zM12 0h90c6.627 0 12 5.373 12 12v60c0 6.627-5.373 12-12 12H12C5.373 84 0 78.627 0 72V12C0 5.373 5.373 0 12 0zm98 22H61a2 2 0 0 1-2-2v-6a3 3 0 0 0-3-3H37a3 3 0 0 0-3 3v66h68a8 8 0 0 0 8-8V22z"/><path fill="#EFEDF8" d="M52 36h40a2 2 0 1 1 0 4H52a2 2 0 1 1 0-4z"/><path fill="#E1DBF2" d="M52 46h30a2 2 0 1 1 0 4H52a2 2 0 1 1 0-4z"/><path fill="#EFEDF8" d="M52 56h40a2 2 0 1 1 0 4H52a2 2 0 1 1 0-4z"/><path fill="#E1DBF2" d="M52 66h30a2 2 0 1 1 0 4H52a2 2 0 1 1 0-4zM10 16h14a2 2 0 1 1 0 4H10a2 2 0 1 1 0-4zm0 10h10a2 2 0 1 1 0 4H10a2 2 0 1 1 0-4z"/><path fill="#EFEDF8" d="M10 36h14a2 2 0 1 1 0 4H10a2 2 0 1 1 0-4z"/><path fill="#6B4FBB" d="M10 46h10a2 2 0 1 1 0 4H10a2 2 0 1 1 0-4z"/><path fill="#E1DBF2" d="M10 56h14a2 2 0 1 1 0 4H10a2 2 0 1 1 0-4z"/><path fill="#EFEDF8" d="M10 66h10a2 2 0 1 1 0 4H10a2 2 0 1 1 0-4z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 0f28bd233ac..0da872db7e5 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -3,10 +3,10 @@
import AccessorUtilities from './lib/utils/accessor';
export default class Autosave {
- constructor(field, key, resource) {
+ constructor(field, key) {
this.field = field;
+
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
- this.resource = resource;
if (key.join != null) {
key = key.join('/');
}
@@ -17,31 +17,27 @@ export default class Autosave {
}
restore() {
- var text;
-
if (!this.isLocalStorageAvailable) return;
+ if (!this.field.length) return;
- text = window.localStorage.getItem(this.key);
+ const text = window.localStorage.getItem(this.key);
if ((text != null ? text.length : void 0) > 0) {
this.field.val(text);
}
- if (!this.resource && this.resource !== 'issue') {
- this.field.trigger('input');
- } else {
- // v-model does not update with jQuery trigger
- // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
- const event = new Event('change', { bubbles: true, cancelable: false });
- const field = this.field.get(0);
- if (field) {
- field.dispatchEvent(event);
- }
- }
+
+ this.field.trigger('input');
+ // v-model does not update with jQuery trigger
+ // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
+ const event = new Event('change', { bubbles: true, cancelable: false });
+ const field = this.field.get(0);
+ field.dispatchEvent(event);
}
save() {
- var text;
- text = this.field.val();
+ if (!this.field.length) return;
+
+ const text = this.field.val();
if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) {
return window.localStorage.setItem(this.key, text);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 9456edebccb..26e62732b33 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -2,7 +2,7 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
import { __ } from './locale';
-import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
+import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -239,9 +239,9 @@ class AwardsHandler {
}
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
- const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length;
+ const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
- if (isInIssuePage() && !isMainAwardsBlock) {
+ if (this.isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu'));
@@ -293,8 +293,16 @@ class AwardsHandler {
}
}
+ isVueMRDiscussions() {
+ return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
+ }
+
+ isInVueNoteablePage() {
+ return isInIssuePage() || this.isVueMRDiscussions();
+ }
+
getVotesBlock() {
- if (isInIssuePage()) {
+ if (this.isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
if ($el.length) {
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 312edc0cd69..51bd5b8ebe5 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -72,5 +72,5 @@ $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-q
title,
trigger: 'manual',
});
- $this.tooltip('show').one('blur', () => $this.tooltip('hide'));
+ $this.tooltip('show').one('blur click', () => $this.tooltip('hide'));
});
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 417ac31fc86..81c89441424 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -12,7 +12,7 @@ $(() => {
const $container = $(container);
$container
- .find('.js-toggle-button .fa')
+ .find('.js-toggle-button .fa-chevron-up, .js-toggle-button .fa-chevron-down')
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
@@ -22,7 +22,7 @@ $(() => {
}
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
- e.target.classList.toggle('open');
+ e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'open');
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index 062577af385..06ef86ecb77 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -7,7 +7,7 @@ function onError() {
return flash;
}
-function loadBalsamiqFile() {
+export default function loadBalsamiqFile() {
const viewer = document.getElementById('js-balsamiq-viewer');
if (!(viewer instanceof Element)) return;
@@ -17,5 +17,3 @@ function loadBalsamiqFile() {
const balsamiqViewer = new BalsamiqViewer(viewer);
balsamiqViewer.loadFile(endpoint).catch(onError);
}
-
-$(loadBalsamiqFile);
diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js
index b7a0a195a92..226ae69893e 100644
--- a/app/assets/javascripts/blob/notebook_viewer.js
+++ b/app/assets/javascripts/blob/notebook_viewer.js
@@ -1,3 +1,3 @@
import renderNotebook from './notebook';
-document.addEventListener('DOMContentLoaded', renderNotebook);
+export default renderNotebook;
diff --git a/app/assets/javascripts/blob/pdf_viewer.js b/app/assets/javascripts/blob/pdf_viewer.js
index 91abe9dd699..cabbb396ea7 100644
--- a/app/assets/javascripts/blob/pdf_viewer.js
+++ b/app/assets/javascripts/blob/pdf_viewer.js
@@ -1,3 +1,3 @@
import renderPDF from './pdf';
-document.addEventListener('DOMContentLoaded', renderPDF);
+export default renderPDF;
diff --git a/app/assets/javascripts/blob/sketch_viewer.js b/app/assets/javascripts/blob/sketch_viewer.js
index 0640dd26855..2c1c6339fdb 100644
--- a/app/assets/javascripts/blob/sketch_viewer.js
+++ b/app/assets/javascripts/blob/sketch_viewer.js
@@ -1,8 +1,8 @@
/* eslint-disable no-new */
import SketchLoader from './sketch';
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
const el = document.getElementById('js-sketch-viewer');
new SketchLoader(el);
-});
+};
diff --git a/app/assets/javascripts/blob/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js
index f611c4fe640..63236b6477f 100644
--- a/app/assets/javascripts/blob/stl_viewer.js
+++ b/app/assets/javascripts/blob/stl_viewer.js
@@ -1,6 +1,6 @@
import Renderer from './3d_viewer';
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
const viewer = new Renderer(document.getElementById('js-stl-viewer'));
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
@@ -16,4 +16,4 @@ document.addEventListener('DOMContentLoaded', () => {
viewer.changeObjectMaterials(target.dataset.type);
});
});
-});
+};
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 612f604e725..92ea91c45a8 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -5,6 +5,7 @@ import axios from '../../lib/utils/axios_utils';
export default class BlobViewer {
constructor() {
BlobViewer.initAuxiliaryViewer();
+ BlobViewer.initRichViewer();
this.initMainViewers();
}
@@ -16,6 +17,38 @@ export default class BlobViewer {
BlobViewer.loadViewer(auxiliaryViewer);
}
+ static initRichViewer() {
+ const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
+ if (!viewer || !viewer.dataset.richType) return;
+
+ const initViewer = promise => promise
+ .then(module => module.default(viewer))
+ .catch((error) => {
+ Flash('Error loading file viewer.');
+ throw error;
+ });
+
+ switch (viewer.dataset.richType) {
+ case 'balsamiq':
+ initViewer(import(/* webpackChunkName: 'balsamiq_viewer' */ '../balsamiq_viewer'));
+ break;
+ case 'notebook':
+ initViewer(import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer'));
+ break;
+ case 'pdf':
+ initViewer(import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer'));
+ break;
+ case 'sketch':
+ initViewer(import(/* webpackChunkName: 'sketch_viewer' */ '../sketch_viewer'));
+ break;
+ case 'stl':
+ initViewer(import(/* webpackChunkName: 'stl_viewer' */ '../stl_viewer'));
+ break;
+ default:
+ break;
+ }
+ }
+
initMainViewers() {
this.$fileHolder = $('.file-holder');
if (!this.$fileHolder.length) return;
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 23fec503586..84885ca9306 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/require-default-prop */
import './issue_card_inner';
import eventHub from '../eventhub';
@@ -34,6 +35,9 @@ export default {
type: String,
default: '',
},
+ groupId: {
+ type: Number,
+ },
},
data() {
return {
@@ -88,6 +92,7 @@ export default {
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
+ :group-id="groupId"
:root-path="rootPath"
:update-filters="true"
/>
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 9a0442e2afe..0d03c1c419c 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,6 +1,6 @@
<script>
import Sortable from 'vendor/Sortable';
-import boardNewIssue from './board_new_issue';
+import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
@@ -15,6 +15,11 @@ export default {
loadingIcon,
},
props: {
+ groupId: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
disabled: {
type: Boolean,
required: true,
@@ -170,6 +175,7 @@ export default {
<loading-icon />
</div>
<board-new-issue
+ :group-id="groupId"
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
@@ -185,6 +191,7 @@ export default {
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
+ :group-id="groupId"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.vue
index bc28f7f45f4..870d242e774 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,11 +1,21 @@
-/* global ListIssue */
+<script>
import eventHub from '../eventhub';
+import ProjectSelect from './project_select.vue';
+import ListIssue from '../models/issue';
const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardNewIssue',
+ components: {
+ ProjectSelect,
+ },
props: {
+ groupId: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
list: {
type: Object,
required: true,
@@ -15,8 +25,21 @@ export default {
return {
title: '',
error: false,
+ selectedProject: {},
};
},
+ computed: {
+ disabled() {
+ if (this.groupId) {
+ return this.title === '' || !this.selectedProject.name;
+ }
+ return this.title === '';
+ },
+ },
+ mounted() {
+ this.$refs.input.focus();
+ eventHub.$on('setSelectedProject', this.setSelectedProject);
+ },
methods: {
submit(e) {
e.preventDefault();
@@ -30,6 +53,7 @@ export default {
labels,
subscribed: true,
assignees: [],
+ project_id: this.selectedProject.id,
});
eventHub.$emit(`scroll-board-list-${this.list.id}`);
@@ -58,43 +82,62 @@ export default {
this.title = '';
eventHub.$emit(`hide-issue-form-${this.list.id}`);
},
+ setSelectedProject(selectedProject) {
+ this.selectedProject = selectedProject;
+ },
},
- mounted() {
- this.$refs.input.focus();
- },
- template: `
- <div class="card board-new-issue-form">
+};
+</script>
+
+<template>
+ <div class="board-new-issue-form">
+ <div class="card">
<form @submit="submit($event)">
- <div class="flash-container"
- v-if="error">
+ <div
+ class="flash-container"
+ v-if="error"
+ >
<div class="flash-alert">
An error occurred. Please try again.
</div>
</div>
- <label class="label-light"
- :for="list.id + '-title'">
+ <label
+ class="label-light"
+ :for="list.id + '-title'"
+ >
Title
</label>
- <input class="form-control"
+ <input
+ class="form-control"
type="text"
v-model="title"
ref="input"
autocomplete="off"
- :id="list.id + '-title'" />
+ :id="list.id + '-title'"
+ />
+ <project-select
+ v-if="groupId"
+ :group-id="groupId"
+ />
<div class="clearfix prepend-top-10">
- <button class="btn btn-success pull-left"
+ <button
+ class="btn btn-success pull-left"
type="submit"
- :disabled="title === ''"
- ref="submit-button">
+ :disabled="disabled"
+ ref="submit-button"
+ >
Submit issue
</button>
- <button class="btn btn-default pull-right"
+ <button
+ class="btn btn-default pull-right"
type="button"
- @click="cancel">
+ @click="cancel"
+ >
Cancel
</button>
</div>
</form>
</div>
- `,
-};
+ </div>
+</template>
+
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index add24303e7b..9501e35b178 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -6,7 +6,7 @@ import { __ } from '../../locale';
import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
-import assignees from '../../sidebar/components/assignees/assignees';
+import assignees from '../../sidebar/components/assignees/assignees.vue';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index bf474879024..fc2bad2415f 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -31,6 +31,10 @@ gl.issueBoards.IssueCardInner = Vue.extend({
required: false,
default: false,
},
+ groupId: {
+ type: Number,
+ required: false,
+ },
},
data() {
return {
@@ -64,7 +68,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return this.issue.assignees.length > this.numberOverLimit;
},
cardUrl() {
- return `${this.issueLinkBase}/${this.issue.iid}`;
+ let baseUrl = this.issueLinkBase;
+
+ if (this.groupId && this.issue.project) {
+ baseUrl = this.issueLinkBase.replace(':project_path', this.issue.project.path);
+ }
+
+ return `${baseUrl}/${this.issue.iid}`;
},
issueId() {
if (this.issue.iid) {
@@ -148,7 +158,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
class="card-number"
v-if="issueId"
>
- {{ issueId }}
+ <template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }}
</span>
</h4>
<div class="card-assignee">
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
new file mode 100644
index 00000000000..d99b222c305
--- /dev/null
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -0,0 +1,127 @@
+<script>
+ /* global ListIssue */
+ import _ from 'underscore';
+ import eventHub from '../eventhub';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import Api from '../../api';
+
+ export default {
+ name: 'BoardProjectSelect',
+ components: {
+ loadingIcon,
+ },
+ props: {
+ groupId: {
+ type: Number,
+ required: true,
+ default: 0,
+ },
+ },
+ data() {
+ return {
+ loading: true,
+ selectedProject: {},
+ };
+ },
+ computed: {
+ selectedProjectName() {
+ return this.selectedProject.name || 'Select a project';
+ },
+ },
+ mounted() {
+ $(this.$refs.projectsDropdown).glDropdown({
+ filterable: true,
+ filterRemote: true,
+ search: {
+ fields: ['name_with_namespace'],
+ },
+ clicked: ({ $el, e }) => {
+ e.preventDefault();
+ this.selectedProject = {
+ id: $el.data('project-id'),
+ name: $el.data('project-name'),
+ };
+ eventHub.$emit('setSelectedProject', this.selectedProject);
+ },
+ selectable: true,
+ data: (term, callback) => {
+ this.loading = true;
+ return Api.groupProjects(this.groupId, term, (projects) => {
+ this.loading = false;
+ callback(projects);
+ });
+ },
+ renderRow(project) {
+ return `
+ <li>
+ <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
+ ${_.escape(project.name)}
+ </a>
+ </li>
+ `;
+ },
+ text: project => project.name,
+ });
+ },
+ };
+</script>
+
+<template>
+ <div>
+ <label class="label-light prepend-top-10">
+ Project
+ </label>
+ <div
+ ref="projectsDropdown"
+ class="dropdown"
+ >
+ <button
+ class="dropdown-menu-toggle wide"
+ type="button"
+ data-toggle="dropdown"
+ aria-expanded="false"
+ >
+ {{ selectedProjectName }}
+ <i
+ class="fa fa-chevron-down"
+ aria-hidden="true"
+ >
+ </i>
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
+ <div class="dropdown-title">
+ <span>Projects</span>
+ <button
+ aria-label="Close"
+ type="button"
+ class="dropdown-title-button dropdown-menu-close"
+ >
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-times dropdown-menu-close-icon"
+ >
+ </i>
+ </button>
+ </div>
+ <div class="dropdown-input">
+ <input
+ class="dropdown-input-field"
+ type="search"
+ placeholder="Search projects"
+ />
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ >
+ </i>
+ </div>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading">
+ <loading-icon />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index 0ae32bb4d0a..09c683ff621 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -24,7 +24,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
},
computed: {
updateUrl() {
- return this.issueUpdate;
+ return this.issueUpdate.replace(':project_path', this.issue.project.path);
},
},
methods: {
@@ -32,17 +32,21 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
const issue = this.issue;
const lists = issue.getLists();
const listLabelIds = lists.map(list => list.label.id);
- let labelIds = this.issue.labels
+
+ let labelIds = issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
+
const data = {
issue: {
label_ids: labelIds,
},
};
+
+ // Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.'));
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 0df1f7a6f82..fb40b9f5565 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -4,7 +4,10 @@ import FilteredSearchManager from '../filtered_search/filtered_search_manager';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
- super('boards');
+ super({
+ page: 'boards',
+ stateFiltersSelector: '.issues-state-filters',
+ });
this.store = store;
this.updateUrl = updateUrl;
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 06a8abea940..efc0da2e7a2 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -2,15 +2,18 @@
import _ from 'underscore';
import Vue from 'vue';
-import Flash from '../flash';
-import { __ } from '../locale';
+
+import Flash from '~/flash';
+import { __ } from '~/locale';
+import '~/vue_shared/models/label';
+
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
-import sidebarEventHub from '../sidebar/event_hub';
+import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first
import './models/issue';
-import './models/label';
import './models/list';
import './models/milestone';
+import './models/project';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
@@ -22,7 +25,7 @@ import './components/board';
import './components/board_sidebar';
import './components/new_list_dropdown';
import './components/modal/index';
-import '../vue_shared/vue_resource_interceptor';
+import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first
export default () => {
const $boardApp = document.getElementById('board-app');
@@ -87,7 +90,7 @@ export default () => {
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
- this.filterManager = new FilteredSearchBoards(Store.filter, true);
+ this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
this.filterManager.setup();
Store.disabled = this.disabled;
@@ -177,6 +180,7 @@ export default () => {
return {
modal: ModalStore.store,
store: Store.state,
+ canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
};
},
computed: {
@@ -230,6 +234,7 @@ export default () => {
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
:aria-disabled="disabled"
+ v-if="canAdminList"
@click="openModal">
Add issues
</button>
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index 38a0eb12f92..5e31c6314b2 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -1,6 +1,8 @@
/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */
+import sortableConfig from '../../sortable/sortable_config';
+
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -18,19 +20,14 @@ gl.issueBoards.onEnd = () => {
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
- const defaultSortOptions = {
- animation: 200,
- forceFallback: true,
- fallbackClass: 'is-dragging',
- fallbackOnBody: true,
- ghostClass: 'is-ghost',
+ const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
- onEnd: gl.issueBoards.onEnd
- };
+ onEnd: gl.issueBoards.onEnd,
+ });
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
return defaultSortOptions;
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 81edd95bf2b..4c5079efc8b 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -4,6 +4,7 @@
/* global ListAssignee */
import Vue from 'vue';
+import IssueProject from './project';
class ListIssue {
constructor (obj, defaultAvatar) {
@@ -23,6 +24,12 @@ class ListIssue {
this.isLoading = {};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
+ this.milestone_id = obj.milestone_id;
+ this.project_id = obj.project_id;
+
+ if (obj.project) {
+ this.project = new IssueProject(obj.project);
+ }
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
@@ -105,8 +112,11 @@ class ListIssue {
data.issue.label_ids = [''];
}
- return Vue.http.patch(url, data);
+ const projectPath = this.project ? this.project.path : '';
+ return Vue.http.patch(url.replace(':project_path', projectPath), data);
}
}
window.ListIssue = ListIssue;
+
+export default ListIssue;
diff --git a/app/assets/javascripts/boards/models/project.js b/app/assets/javascripts/boards/models/project.js
new file mode 100644
index 00000000000..a3d5c7af7ac
--- /dev/null
+++ b/app/assets/javascripts/boards/models/project.js
@@ -0,0 +1,6 @@
+export default class IssueProject {
+ constructor(obj) {
+ this.id = obj.id;
+ this.path = obj.path;
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 798d7e0d147..348cdeec737 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -2,7 +2,7 @@
/* global List */
import _ from 'underscore';
import Cookies from 'js-cookie';
-import { getUrlParamsArray } from '../../lib/utils/common_utils';
+import { getUrlParamsArray } from '~/lib/utils/common_utils';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index b070a59cf15..01aec4f36af 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -37,10 +37,11 @@ export default class Clusters {
clusterStatusReason,
helpPath,
ingressHelpPath,
+ ingressDnsHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore();
- this.store.setHelpPaths(helpPath, ingressHelpPath);
+ this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
@@ -98,6 +99,7 @@ export default class Clusters {
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
+ ingressDnsHelpPath: this.state.ingressDnsHelpPath,
},
});
},
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 50e35bbbba5..c2a35341eb2 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -36,10 +36,6 @@
type: String,
required: false,
},
- description: {
- type: String,
- required: true,
- },
status: {
type: String,
required: false,
@@ -148,7 +144,7 @@
class="table-section section-wrap"
role="gridcell"
>
- <div v-html="description"></div>
+ <slot name="description"></slot>
</div>
<div
class="table-section table-button-footer section-align-top"
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 978881a4831..f8dcdf3f60a 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -2,10 +2,16 @@
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
+ import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+ import {
+ APPLICATION_INSTALLED,
+ INGRESS,
+ } from '../constants';
export default {
components: {
applicationRow,
+ clipboardButton,
},
props: {
applications: {
@@ -23,6 +29,11 @@
required: false,
default: '',
},
+ ingressDnsHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
managePrometheusPath: {
type: String,
required: false,
@@ -43,19 +54,16 @@
false,
);
},
- helmTillerDescription() {
- return _.escape(s__(
- `ClusterIntegration|Helm streamlines installing and managing Kubernetes applications.
- Tiller runs inside of your Kubernetes Cluster, and manages
- releases of your charts.`,
- ));
+ ingressId() {
+ return INGRESS;
+ },
+ ingressInstalled() {
+ return this.applications.ingress.status === APPLICATION_INSTALLED;
+ },
+ ingressExternalIp() {
+ return this.applications.ingress.externalIp;
},
ingressDescription() {
- const descriptionParagraph = _.escape(s__(
- `ClusterIntegration|Ingress gives you a way to route requests to services based on the
- request host or path, centralizing a number of services into a single entrypoint.`,
- ));
-
const extraCostParagraph = sprintf(
_.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
@@ -84,9 +92,6 @@
return `
<p>
- ${descriptionParagraph}
- </p>
- <p>
${extraCostParagraph}
</p>
<p class="settings-message append-bottom-0">
@@ -94,12 +99,6 @@
</p>
`;
},
- gitlabRunnerDescription() {
- return _.escape(s__(
- `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs
- and send the results back to GitLab.`,
- ));
- },
prometheusDescription() {
return sprintf(
_.escape(s__(
@@ -118,7 +117,10 @@
</script>
<template>
- <section class="settings no-animate expanded">
+ <section
+ id="cluster-applications"
+ class="settings no-animate expanded"
+ >
<div class="settings-header">
<h4>
{{ s__('ClusterIntegration|Applications') }}
@@ -136,33 +138,137 @@
id="helm"
:title="applications.helm.title"
title-link="https://docs.helm.sh/"
- :description="helmTillerDescription"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
- />
+ >
+ <div slot="description">
+ {{ s__(`ClusterIntegration|Helm streamlines installing
+ and managing Kubernetes applications.
+ Tiller runs inside of your Kubernetes Cluster,
+ and manages releases of your charts.`) }}
+ </div>
+ </application-row>
<application-row
- id="ingress"
+ :id="ingressId"
:title="applications.ingress.title"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
- :description="ingressDescription"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
- />
+ >
+ <div slot="description">
+ <p>
+ {{ s__(`ClusterIntegration|Ingress gives you a way to route
+ requests to services based on the request host or path,
+ centralizing a number of services into a single entrypoint.`) }}
+ </p>
+
+ <template v-if="ingressInstalled">
+ <div class="form-group">
+ <label for="ingress-ip-address">
+ {{ s__('ClusterIntegration|Ingress IP Address') }}
+ </label>
+ <div
+ v-if="ingressExternalIp"
+ class="input-group"
+ >
+ <input
+ type="text"
+ id="ingress-ip-address"
+ class="form-control js-ip-address"
+ :value="ingressExternalIp"
+ readonly
+ />
+ <span class="input-group-btn">
+ <clipboard-button
+ :text="ingressExternalIp"
+ :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
+ class="js-clipboard-btn"
+ />
+ </span>
+ </div>
+ <input
+ v-else
+ type="text"
+ class="form-control js-ip-address"
+ readonly
+ value="?"
+ />
+ </div>
+
+ <p
+ v-if="!ingressExternalIp"
+ class="settings-message js-no-ip-message"
+ >
+ {{ s__(`ClusterIntegration|The IP address is in
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on GKE if it takes a long time.`) }}
+
+ <a
+ :href="ingressHelpPath"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
+
+ <p>
+ {{ s__(`ClusterIntegration|Point a wildcard DNS to this
+ generated IP address in order to access
+ your application after it has been deployed.`) }}
+ <a
+ :href="ingressDnsHelpPath"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
+
+ </template>
+ <div
+ v-else
+ v-html="ingressDescription"
+ >
+ </div>
+ </div>
+ </application-row>
<application-row
id="prometheus"
:title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath"
- :description="prometheusDescription"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
- />
+ >
+ <div
+ slot="description"
+ v-html="prometheusDescription"
+ >
+ </div>
+ </application-row>
+ <application-row
+ id="runner"
+ :title="applications.runner.title"
+ title-link="https://docs.gitlab.com/runner/"
+ :status="applications.runner.status"
+ :status-reason="applications.runner.statusReason"
+ :request-status="applications.runner.requestStatus"
+ :request-reason="applications.runner.requestReason"
+ >
+ <div slot="description">
+ {{ s__(`ClusterIntegration|GitLab Runner connects to this
+ project's repository and executes CI/CD jobs,
+ pushing results back and deploying,
+ applications to production.`) }}
+ </div>
+ </application-row>
<!--
NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 93223aefff8..b7179f52bb3 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -10,3 +10,4 @@ export const APPLICATION_ERROR = 'errored';
export const REQUEST_LOADING = 'request-loading';
export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
+export const INGRESS = 'ingress';
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 904ee5fd475..348bbec3b25 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,4 +1,5 @@
import { s__ } from '../../locale';
+import { INGRESS } from '../constants';
export default class ClusterStore {
constructor() {
@@ -21,6 +22,7 @@ export default class ClusterStore {
statusReason: null,
requestStatus: null,
requestReason: null,
+ externalIp: null,
},
runner: {
title: s__('ClusterIntegration|GitLab Runner'),
@@ -40,9 +42,10 @@ export default class ClusterStore {
};
}
- setHelpPaths(helpPath, ingressHelpPath) {
+ setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath) {
this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath;
+ this.state.ingressDnsHelpPath = ingressDnsHelpPath;
}
setManagePrometheusPath(managePrometheusPath) {
@@ -64,6 +67,7 @@ export default class ClusterStore {
updateStateFromServer(serverState = {}) {
this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason;
+
serverState.applications.forEach((serverAppEntry) => {
const {
name: appId,
@@ -76,6 +80,10 @@ export default class ClusterStore {
status,
statusReason,
};
+
+ if (appId === INGRESS) {
+ this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
+ }
});
}
}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 1f9153d95bd..3d89bf1316e 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -15,7 +15,7 @@ const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable;
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl) {
@@ -43,4 +43,4 @@ document.addEventListener('DOMContentLoaded', () => {
pipelineTableViewEl.appendChild(table.$el);
}
}
-});
+};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index ce19069f103..466a5b5d635 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -20,10 +20,6 @@
type: String,
required: true,
},
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
errorStateSvgPath: {
type: String,
required: true,
@@ -45,23 +41,14 @@
},
computed: {
- /**
- * Empty state is only rendered if after the first request we receive no pipelines.
- *
- * @return {Boolean}
- */
- shouldRenderEmptyState() {
- return !this.state.pipelines.length &&
- !this.isLoading &&
- this.hasMadeRequest &&
- !this.hasError;
- },
-
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
+ },
},
created() {
this.service = new PipelinesService(this.endpoint);
@@ -92,25 +79,22 @@
<div class="content-list pipelines">
<loading-icon
- label="Loading pipelines"
+ :label="s__('Pipelines|Loading Pipelines')"
size="3"
v-if="isLoading"
+ class="prepend-top-20"
/>
- <empty-state
- v-if="shouldRenderEmptyState"
- :help-page-path="helpPagePath"
- :empty-state-svg-path="emptyStateSvgPath"
- />
-
- <error-state
- v-if="shouldRenderErrorState"
- :error-state-svg-path="errorStateSvgPath"
+ <svg-blank-state
+ v-else-if="shouldRenderErrorState"
+ :svg-path="errorStateSvgPath"
+ :message="s__(`Pipelines|There was an error fetching the pipelines.
+ Try again in a few moments or contact your support team.`)"
/>
<div
class="table-holder"
- v-if="shouldRenderTable"
+ v-else-if="shouldRenderTable"
>
<pipelines-table-component
:pipelines="state.pipelines"
diff --git a/app/assets/javascripts/commons/vue.js b/app/assets/javascripts/commons/vue.js
index 8b62d78c043..798623b94fb 100644
--- a/app/assets/javascripts/commons/vue.js
+++ b/app/assets/javascripts/commons/vue.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import '../vue_shared/vue_resource_interceptor';
if (process.env.NODE_ENV !== 'production') {
Vue.config.productionTip = false;
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 034f2923b3b..46d89c825f9 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -14,10 +14,10 @@ import CycleAnalyticsStore from './cycle_analytics_store';
Vue.use(Translate);
-$(() => {
+export default () => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
- gl.cycleAnalyticsApp = new Vue({
+ new Vue({ // eslint-disable-line no-new
el: '#cycle-analytics',
name: 'CycleAnalytics',
components: {
@@ -132,4 +132,4 @@ $(() => {
},
},
});
-});
+};
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index e77910a83d4..fadc34959e1 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -197,7 +197,7 @@ const JumpToDiscussion = Vue.extend({
}
$.scrollTo($target, {
- offset: 0
+ offset: -150
});
}
},
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index 20ddcbfb8bd..cc9192deae3 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -87,6 +87,7 @@ const ResolveBtn = Vue.extend({
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
+ document.dispatchEvent(new CustomEvent('refreshVueNotes'));
this.updateTooltip();
})
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index 679057e787c..5f49609fe88 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -14,6 +14,7 @@ import './components/resolve_count';
import './components/resolve_discussion_btn';
import './components/diff_note_avatars';
import './components/new_issue_for_discussion';
+import { hasVueMRDiscussionsCookie } from '../lib/utils/common_utils';
export default () => {
const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
@@ -67,12 +68,14 @@ export default () => {
gl.diffNotesCompileComponents();
- new Vue({
- el: '#resolve-count-app',
- components: {
- 'resolve-count': ResolveCount
- },
- });
+ if (!hasVueMRDiscussionsCookie()) {
+ new Vue({
+ el: '#resolve-count-app',
+ components: {
+ 'resolve-count': ResolveCount
+ },
+ });
+ }
$(window).trigger('resize.nav');
};
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index 96fe23640af..d16f9297de1 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -8,8 +8,8 @@ window.gl = window.gl || {};
class ResolveServiceClass {
constructor(root) {
- this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
- this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
+ this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
+ this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`);
}
resolve(noteId) {
@@ -45,6 +45,7 @@ class ResolveServiceClass {
if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
+ document.dispatchEvent(new CustomEvent('refreshVueNotes'));
})
.catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index acf0effa00d..1ccf96a75dc 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -6,177 +6,80 @@ import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts';
import SearchAutocomplete from './search_autocomplete';
-var Dispatcher;
-
-(function() {
- Dispatcher = (function() {
- function Dispatcher() {
- this.initSearch();
- this.initFieldErrors();
- this.initPageScripts();
- }
-
- Dispatcher.prototype.initPageScripts = function() {
- var path, shortcut_handler;
- const page = $('body').attr('data-page');
- if (!page) {
- return false;
- }
-
- const fail = () => Flash('Error loading dynamic module');
- const callDefault = m => m.default();
-
- path = page.split(':');
- shortcut_handler = null;
-
- $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
- const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
- const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
- gfm.setup($(el), {
- emojis: true,
- members: enableGFM,
- issues: enableGFM,
- milestones: enableGFM,
- mergeRequests: enableGFM,
- labels: enableGFM,
- });
- });
-
- const shortcutHandlerPages = [
- 'projects:activity',
- 'projects:artifacts:browse',
- 'projects:artifacts:file',
- 'projects:blame:show',
- 'projects:blob:show',
- 'projects:commit:show',
- 'projects:commits:show',
- 'projects:find_file:show',
- 'projects:issues:edit',
- 'projects:issues:index',
- 'projects:issues:new',
- 'projects:issues:show',
- 'projects:merge_requests:creations:diffs',
- 'projects:merge_requests:creations:new',
- 'projects:merge_requests:edit',
- 'projects:merge_requests:index',
- 'projects:merge_requests:show',
- 'projects:network:show',
- 'projects:show',
- 'projects:tree:show',
- 'groups:show',
- ];
+function initSearch() {
+ // Only when search form is present
+ if ($('.search').length) {
+ return new SearchAutocomplete();
+ }
+}
- if (shortcutHandlerPages.indexOf(page) !== -1) {
- shortcut_handler = true;
- }
+function initFieldErrors() {
+ $('.gl-show-field-errors').each((i, form) => {
+ new GlFieldErrors(form);
+ });
+}
- switch (path[0]) {
- case 'admin':
- switch (path[1]) {
- case 'broadcast_messages':
- import('./pages/admin/broadcast_messages')
- .then(callDefault)
- .catch(fail);
- break;
- case 'cohorts':
- import('./pages/admin/cohorts')
- .then(callDefault)
- .catch(fail);
- break;
- case 'groups':
- switch (path[2]) {
- case 'show':
- import('./pages/admin/groups/show')
- .then(callDefault)
- .catch(fail);
- break;
- }
- break;
- case 'projects':
- import('./pages/admin/projects')
- .then(callDefault)
- .catch(fail);
- break;
- case 'labels':
- switch (path[2]) {
- case 'new':
- import('./pages/admin/labels/new')
- .then(callDefault)
- .catch(fail);
- break;
- case 'edit':
- import('./pages/admin/labels/edit')
- .then(callDefault)
- .catch(fail);
- break;
- }
- case 'abuse_reports':
- import('./pages/admin/abuse_reports')
- .then(callDefault)
- .catch(fail);
- break;
- }
- break;
- case 'profiles':
- import('./pages/profiles/index')
- .then(callDefault)
- .catch(fail);
- break;
- case 'projects':
- import('./pages/projects')
- .then(callDefault)
- .catch(fail);
- shortcut_handler = true;
- switch (path[1]) {
- case 'compare':
- import('./pages/projects/compare')
- .then(callDefault)
- .catch(fail);
- break;
- case 'create':
- case 'new':
- import('./pages/projects/new')
- .then(callDefault)
- .catch(fail);
- break;
- case 'wikis':
- import('./pages/projects/wikis')
- .then(callDefault)
- .catch(fail);
- shortcut_handler = true;
- break;
- }
- break;
- }
- // If we haven't installed a custom shortcut handler, install the default one
- if (!shortcut_handler) {
- new Shortcuts();
- }
+function initPageShortcuts(page) {
+ const pagesWithCustomShortcuts = [
+ 'projects:activity',
+ 'projects:artifacts:browse',
+ 'projects:artifacts:file',
+ 'projects:blame:show',
+ 'projects:blob:show',
+ 'projects:commit:show',
+ 'projects:commits:show',
+ 'projects:find_file:show',
+ 'projects:issues:edit',
+ 'projects:issues:index',
+ 'projects:issues:new',
+ 'projects:issues:show',
+ 'projects:merge_requests:creations:diffs',
+ 'projects:merge_requests:creations:new',
+ 'projects:merge_requests:edit',
+ 'projects:merge_requests:index',
+ 'projects:merge_requests:show',
+ 'projects:network:show',
+ 'projects:show',
+ 'projects:tree:show',
+ 'groups:show',
+ ];
- if (document.querySelector('#peek')) {
- import('./performance_bar')
- .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
- .catch(fail);
- }
- };
+ if (pagesWithCustomShortcuts.indexOf(page) === -1) {
+ new Shortcuts();
+ }
+}
- Dispatcher.prototype.initSearch = function() {
- // Only when search form is present
- if ($('.search').length) {
- return new SearchAutocomplete();
- }
- };
+function initGFMInput() {
+ $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
+ const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
+ const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
+ gfm.setup($(el), {
+ emojis: true,
+ members: enableGFM,
+ issues: enableGFM,
+ milestones: enableGFM,
+ mergeRequests: enableGFM,
+ labels: enableGFM,
+ });
+ });
+}
- Dispatcher.prototype.initFieldErrors = function() {
- $('.gl-show-field-errors').each((i, form) => {
- new GlFieldErrors(form);
- });
- };
+function initPerformanceBar() {
+ if (document.querySelector('#peek')) {
+ import('./performance_bar')
+ .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
+ .catch(() => Flash('Error loading performance bar module'));
+ }
+}
- return Dispatcher;
- })();
-})();
+export default () => {
+ initSearch();
+ initFieldErrors();
-export default function initDispatcher() {
- return new Dispatcher();
-}
+ const page = $('body').attr('data-page');
+ if (page) {
+ initPageShortcuts(page);
+ initGFMInput();
+ initPerformanceBar();
+ }
+};
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index b4eca47957e..22863e926d4 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -2,8 +2,8 @@
/**
* Render environments table.
*/
+import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import environmentItem from './environment_item.vue';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 5d2d14c7682..de0fbdb2e91 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -5,7 +5,7 @@ import Translate from '../../vue_shared/translate';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => new Vue({
+export default () => new Vue({
el: '#environments-folder-list-view',
components: {
environmentsFolderApp,
@@ -32,4 +32,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
-}));
+});
diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/index.js
index 2e0a4001b7c..afc4aba6554 100644
--- a/app/assets/javascripts/environments/environments_bundle.js
+++ b/app/assets/javascripts/environments/index.js
@@ -5,7 +5,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => new Vue({
+export default () => new Vue({
el: '#environments-list-view',
components: {
environmentsComponent,
@@ -36,4 +36,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
-}));
+});
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
deleted file mode 100644
index 293154917fa..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import './dropdown_emoji';
-import './dropdown_hint';
-import './dropdown_non_user';
-import './dropdown_user';
-import './dropdown_utils';
-import './filtered_search_dropdown_manager';
-import './filtered_search_dropdown';
-import './filtered_search_manager';
-import './filtered_search_tokenizer';
-import './filtered_search_visual_tokens';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index c64553a1b92..e6390f0855b 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -10,13 +10,24 @@ import DropdownUser from './dropdown_user';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager {
- constructor(baseEndpoint = '', tokenizer, page, isGroup, filteredSearchTokenKeys) {
+ constructor({
+ baseEndpoint = '',
+ tokenizer,
+ page,
+ isGroup,
+ isGroupAncestor,
+ isGroupDecendent,
+ filteredSearchTokenKeys,
+ }) {
this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page;
+ this.groupsOnly = isGroup;
+ this.groupAncestor = isGroupAncestor;
+ this.isGroupDecendent = isGroupDecendent;
this.setupMapping();
@@ -59,7 +70,7 @@ export default class FilteredSearchDropdownManager {
reference: null,
gl: DropdownNonUser,
extraArguments: {
- endpoint: `${this.baseEndpoint}/milestones.json`,
+ endpoint: this.getMilestoneEndpoint(),
symbol: '%',
},
element: this.container.querySelector('#js-dropdown-milestone'),
@@ -68,7 +79,7 @@ export default class FilteredSearchDropdownManager {
reference: null,
gl: DropdownNonUser,
extraArguments: {
- endpoint: `${this.baseEndpoint}/labels.json`,
+ endpoint: this.getLabelsEndpoint(),
symbol: '~',
preprocessing: DropdownUtils.duplicateLabelPreprocessing,
},
@@ -90,6 +101,18 @@ export default class FilteredSearchDropdownManager {
this.mapping = allowedMappings;
}
+ getMilestoneEndpoint() {
+ const endpoint = `${this.baseEndpoint}/milestones.json`;
+
+ return endpoint;
+ }
+
+ getLabelsEndpoint() {
+ const endpoint = `${this.baseEndpoint}/labels.json`;
+
+ return endpoint;
+ }
+
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index e294b629bd0..71b7e80335b 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -20,10 +20,15 @@ import DropdownUtils from './dropdown_utils';
export default class FilteredSearchManager {
constructor({
page,
+ isGroup = false,
+ isGroupAncestor = false,
+ isGroupDecendent = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
}) {
- this.isGroup = false;
+ this.isGroup = isGroup;
+ this.isGroupAncestor = isGroupAncestor;
+ this.isGroupDecendent = isGroupDecendent;
this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page;
@@ -75,13 +80,14 @@ export default class FilteredSearchManager {
if (this.filteredSearchInput) {
this.tokenizer = FilteredSearchTokenizer;
- this.dropdownManager = new FilteredSearchDropdownManager(
- this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
- this.tokenizer,
- this.page,
- this.isGroup,
- this.filteredSearchTokenKeys,
- );
+ this.dropdownManager = new FilteredSearchDropdownManager({
+ baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
+ tokenizer: this.tokenizer,
+ page: this.page,
+ isGroup: this.isGroup,
+ isGroupAncestor: this.isGroupAncestor,
+ filteredSearchTokenKeys: this.filteredSearchTokenKeys,
+ });
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index a19bb882410..600024c21c3 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,5 +1,6 @@
import _ from 'underscore';
-import AjaxCache from '../lib/utils/ajax_cache';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import { objectToQueryString } from '~/lib/utils/common_utils';
import Flash from '../flash';
import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache';
@@ -16,6 +17,21 @@ export default class FilteredSearchVisualTokens {
};
}
+ /**
+ * Returns a computed API endpoint
+ * and query string composed of values from endpointQueryParams
+ * @param {String} endpoint
+ * @param {String} endpointQueryParams
+ */
+ static getEndpointWithQueryParams(endpoint, endpointQueryParams) {
+ if (!endpointQueryParams) {
+ return endpoint;
+ }
+
+ const queryString = objectToQueryString(JSON.parse(endpointQueryParams));
+ return `${endpoint}?${queryString}`;
+ }
+
static unselectTokens() {
const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
@@ -86,7 +102,10 @@ export default class FilteredSearchVisualTokens {
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
- const labelsEndpoint = `${baseEndpoint}/labels.json`;
+ const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
+ `${baseEndpoint}/labels.json`,
+ filteredSearchInput.dataset.endpointQueryParams,
+ );
return AjaxCache.retrieve(labelsEndpoint)
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index b8f0566f48c..0578f43d5af 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -152,14 +152,14 @@ export default {
showLeaveGroupModal(group, parentGroup) {
this.targetGroup = group;
this.targetParentGroup = parentGroup;
- this.updateModal = true;
+ this.showModal = true;
this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
},
hideLeaveGroupModal() {
- this.updateModal = false;
+ this.showModal = false;
},
leaveGroup() {
- this.updateModal = false;
+ this.showModal = false;
this.targetGroup.isBeingRemoved = true;
this.service.leaveGroup(this.targetGroup.leavePath)
.then(res => res.json())
@@ -208,9 +208,9 @@ export default {
:page-info="pageInfo"
/>
<modal
- v-show="showModal"
- :primary-button-label="__('Leave')"
+ v-if="showModal"
kind="warning"
+ :primary-button-label="__('Leave')"
:title="__('Are you sure?')"
:text="groupLeaveConfirmationMessage"
@cancel="hideLeaveGroupModal"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
deleted file mode 100644
index a8459b011df..00000000000
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import icon from '../../../vue_shared/components/icon.vue';
- import listItem from './list_item.vue';
- import listCollapsed from './list_collapsed.vue';
-
- export default {
- components: {
- icon,
- listItem,
- listCollapsed,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- fileList: {
- type: Array,
- required: true,
- },
- },
- computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- ]),
- },
- methods: {
- toggleCollapsed() {
- this.$emit('toggleCollapsed');
- },
- },
- };
-</script>
-
-<template>
- <div class="multi-file-commit-list">
- <list-collapsed
- v-if="rightPanelCollapsed"
- />
- <template v-else>
- <ul
- v-if="fileList.length"
- class="list-unstyled append-bottom-0"
- >
- <li
- v-for="file in fileList"
- :key="file.key"
- >
- <list-item
- :file="file"
- />
- </li>
- </ul>
- <div
- v-else
- class="help-block prepend-top-0"
- >
- No changes
- </div>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
deleted file mode 100644
index 6a0262f271b..00000000000
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<script>
- import { mapGetters } from 'vuex';
- import icon from '../../../vue_shared/components/icon.vue';
-
- export default {
- components: {
- icon,
- },
- computed: {
- ...mapGetters([
- 'addedFiles',
- 'modifiedFiles',
- ]),
- },
- };
-</script>
-
-<template>
- <div
- class="multi-file-commit-list-collapsed text-center"
- >
- <icon
- name="file-addition"
- :size="18"
- css-classes="multi-file-addition append-bottom-10"
- />
- {{ addedFiles.length }}
- <icon
- name="file-modified"
- :size="18"
- css-classes="multi-file-modified prepend-top-10 append-bottom-10"
- />
- {{ modifiedFiles.length }}
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
deleted file mode 100644
index 742f746e02f..00000000000
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<script>
- import icon from '../../../vue_shared/components/icon.vue';
-
- export default {
- components: {
- icon,
- },
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- iconName() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
- },
- iconClass() {
- return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
- },
- },
- };
-</script>
-
-<template>
- <div class="multi-file-commit-list-item">
- <icon
- :name="iconName"
- :size="16"
- :css-classes="iconClass"
- />
- <span class="multi-file-commit-list-path">
- {{ file.path }}
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
deleted file mode 100644
index 89981ab2c65..00000000000
--- a/app/assets/javascripts/ide/components/ide.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<script>
- import { mapState, mapGetters } from 'vuex';
- import ideSidebar from './ide_side_bar.vue';
- import ideContextbar from './ide_context_bar.vue';
- import repoTabs from './repo_tabs.vue';
- import repoFileButtons from './repo_file_buttons.vue';
- import ideStatusBar from './ide_status_bar.vue';
- import repoPreview from './repo_preview.vue';
- import repoEditor from './repo_editor.vue';
-
- export default {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- repoFileButtons,
- ideStatusBar,
- repoEditor,
- repoPreview,
- },
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState([
- 'currentBlobView',
- 'selectedFile',
- ]),
- ...mapGetters([
- 'changedFiles',
- 'activeFile',
- ]),
- },
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = (e) => {
- if (!this.changedFiles.length) return undefined;
-
- Object.assign(e, {
- returnValue,
- });
- return returnValue;
- };
- },
- };
-</script>
-
-<template>
- <div
- class="ide-view"
- >
- <ide-sidebar />
- <div
- class="multi-file-edit-pane"
- >
- <template
- v-if="activeFile"
- >
- <repo-tabs/>
- <component
- class="multi-file-edit-pane-content"
- :is="currentBlobView"
- />
- <repo-file-buttons />
- <ide-status-bar
- :file="selectedFile"
- />
- </template>
- <template
- v-else
- >
- <div class="ide-empty-state">
- <div class="row js-empty-state">
- <div class="col-xs-12">
- <div class="svg-content svg-250">
- <img :src="emptyStateSvgPath" />
- </div>
- </div>
- <div class="col-xs-12">
- <div class="text-content text-center">
- <h4>
- Welcome to the GitLab IDE
- </h4>
- <p>
- You can select a file in the left sidebar to begin
- editing and use the right sidebar to commit your changes.
- </p>
- </div>
- </div>
- </div>
- </div>
- </template>
- </div>
- <ide-contextbar/>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
deleted file mode 100644
index dd947f66969..00000000000
--- a/app/assets/javascripts/ide/components/ide_context_bar.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<script>
- import { mapGetters, mapState, mapActions } from 'vuex';
- import repoCommitSection from './repo_commit_section.vue';
- import icon from '../../vue_shared/components/icon.vue';
- import panelResizer from '../../vue_shared/components/panel_resizer.vue';
-
- export default {
- components: {
- repoCommitSection,
- icon,
- panelResizer,
- },
- data() {
- return {
- width: 290,
- };
- },
- computed: {
- ...mapState([
- 'rightPanelCollapsed',
- ]),
- ...mapGetters([
- 'changedFiles',
- ]),
- currentIcon() {
- return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
- },
- maxSize() {
- return window.innerWidth / 2;
- },
- panelStyle() {
- if (!this.rightPanelCollapsed) {
- return { width: `${this.width}px` };
- }
- return {};
- },
- },
- methods: {
- ...mapActions([
- 'setPanelCollapsedStatus',
- 'setResizingStatus',
- ]),
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'right',
- collapsed: !this.rightPanelCollapsed,
- });
- },
- resizingStarted() {
- this.setResizingStatus(true);
- },
- resizingEnded() {
- this.setResizingStatus(false);
- },
- },
- };
-</script>
-
-<template>
- <div
- class="multi-file-commit-panel"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
- :style="panelStyle"
- >
- <div class="multi-file-commit-panel-section">
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
- >
- <div
- class="multi-file-commit-panel-header-title"
- v-if="!rightPanelCollapsed"
- >
- <icon
- name="list-bulleted"
- :size="18"
- />
- Staged
- </div>
- <button
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- @click="toggleCollapsed"
- >
- <icon
- :name="currentIcon"
- :size="18"
- />
- </button>
- </header>
- <repo-commit-section />
- </div>
- <panel-resizer
- :size.sync="width"
- :enabled="!rightPanelCollapsed"
- :start-size="290"
- :min-size="200"
- :max-size="maxSize"
- @resize-start="resizingStarted"
- @resize-end="resizingEnded"
- side="left"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
deleted file mode 100644
index af2f7341a91..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
-import repoTree from './ide_repo_tree.vue';
-import icon from '../../vue_shared/components/icon.vue';
-import newDropdown from './new_dropdown/index.vue';
-
-export default {
- components: {
- repoTree,
- icon,
- newDropdown,
- },
- props: {
- projectId: {
- type: String,
- required: true,
- },
- branch: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="branch-container">
- <div class="branch-header">
- <div class="branch-header-title">
- <icon
- name="branch"
- :size="12"
- />
- {{ branch.name }}
- </div>
- <div class="branch-header-btns">
- <new-dropdown
- :project-id="projectId"
- :branch="branch.name"
- path=""
- />
- </div>
- </div>
- <div>
- <repo-tree :tree-id="branch.treeId" />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue
deleted file mode 100644
index ed49a0e72a2..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_tree.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<script>
-import branchesTree from './ide_project_branches_tree.vue';
-import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue';
-
-export default {
- components: {
- branchesTree,
- projectAvatarImage,
- },
- props: {
- project: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="projects-sidebar">
- <div class="context-header">
- <a
- :title="project.name"
- :href="project.web_url"
- >
- <div class="avatar-container s40 project-avatar">
- <project-avatar-image
- class="avatar-container project-avatar"
- :link-href="project.path"
- :img-src="project.avatar_url"
- :img-alt="project.name"
- :img-size="40"
- />
- </div>
- <div class="sidebar-context-title">
- {{ project.name }}
- </div>
- </a>
- </div>
- <div class="multi-file-commit-panel-inner-scroll">
- <branches-tree
- v-for="branch in project.branches"
- :key="branch.name"
- :project-id="project.path_with_namespace"
- :branch="branch"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue
deleted file mode 100644
index 4651e345d75..00000000000
--- a/app/assets/javascripts/ide/components/ide_repo_tree.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<script>
-import { mapState } from 'vuex';
-import repoPreviousDirectory from './repo_prev_directory.vue';
-import repoFile from './repo_file.vue';
-import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
-import { treeList } from '../stores/utils';
-
-export default {
- components: {
- repoPreviousDirectory,
- repoFile,
- skeletonLoadingContainer,
- },
- props: {
- treeId: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState([
- 'trees',
- 'isRoot',
- ]),
- ...mapState({
- projectName(state) {
- return state.project.name;
- },
- }),
- fetchedList() {
- return treeList(this.$store.state, this.treeId);
- },
- hasPreviousDirectory() {
- return !this.isRoot && this.fetchedList.length;
- },
- showLoading() {
- if (this.trees[this.treeId]) {
- return this.trees[this.treeId].loading;
- }
- return true;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div class="ide-file-list">
- <table class="table">
- <tbody
- v-if="treeId"
- >
- <repo-previous-directory
- v-if="hasPreviousDirectory"
- />
- <template v-if="showLoading">
- <div
- class="multi-file-loading-container"
- v-for="n in 3"
- :key="n"
- >
- <skeleton-loading-container />
- </div>
- </template>
- <repo-file
- v-for="file in fetchedList"
- :key="file.key"
- :file="file"
- />
- </tbody>
- </table>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
deleted file mode 100644
index a68f8ce0169..00000000000
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<script>
- import { mapState, mapActions } from 'vuex';
- import projectTree from './ide_project_tree.vue';
- import icon from '../../vue_shared/components/icon.vue';
- import panelResizer from '../../vue_shared/components/panel_resizer.vue';
- import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
-
- export default {
- components: {
- projectTree,
- icon,
- panelResizer,
- skeletonLoadingContainer,
- },
- data() {
- return {
- width: 290,
- };
- },
- computed: {
- ...mapState([
- 'loading',
- 'projects',
- 'leftPanelCollapsed',
- ]),
- currentIcon() {
- return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
- },
- maxSize() {
- return window.innerWidth / 2;
- },
- panelStyle() {
- if (!this.leftPanelCollapsed) {
- return { width: `${this.width}px` };
- }
- return {};
- },
- showLoading() {
- return this.loading;
- },
- },
- methods: {
- ...mapActions([
- 'setPanelCollapsedStatus',
- 'setResizingStatus',
- ]),
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'left',
- collapsed: !this.leftPanelCollapsed,
- });
- },
- resizingStarted() {
- this.setResizingStatus(true);
- },
- resizingEnded() {
- this.setResizingStatus(false);
- },
- },
- };
-</script>
-
-<template>
- <div
- class="multi-file-commit-panel"
- :class="{
- 'is-collapsed': leftPanelCollapsed,
- }"
- :style="panelStyle"
- >
- <div class="multi-file-commit-panel-inner">
- <template v-if="showLoading">
- <div
- class="multi-file-loading-container"
- v-for="n in 3"
- :key="n"
- >
- <skeleton-loading-container />
- </div>
- </template>
- <project-tree
- v-for="project in projects"
- :key="project.id"
- :project="project"
- />
- </div>
- <button
- type="button"
- class="btn btn-transparent left-collapse-btn"
- @click="toggleCollapsed"
- >
- <icon
- :name="currentIcon"
- :size="18"
- />
- <span
- v-if="!leftPanelCollapsed"
- class="collapse-text"
- >
- Collapse sidebar
- </span>
- </button>
- <panel-resizer
- :size.sync="width"
- :enabled="!leftPanelCollapsed"
- :start-size="290"
- :min-size="200"
- :max-size="maxSize"
- @resize-start="resizingStarted"
- @resize-end="resizingEnded"
- side="right"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
deleted file mode 100644
index e48c446c4a4..00000000000
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import icon from '../../vue_shared/components/icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
- import timeAgoMixin from '../../vue_shared/mixins/timeago';
-
- export default {
- components: {
- icon,
- },
- directives: {
- tooltip,
- },
- mixins: [
- timeAgoMixin,
- ],
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- ...mapState([
- 'selectedFile',
- ]),
- },
- };
-</script>
-
-<template>
- <div class="ide-status-bar">
- <div>
- <icon
- name="branch"
- :size="12"
- />
- {{ selectedFile.branchId }}
- </div>
- <div>
- <div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
- Last commit:
- <a
- v-tooltip
- :title="selectedFile.lastCommit.message"
- :href="selectedFile.lastCommit.url"
- >
- {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
- {{ selectedFile.lastCommit.author }}
- </a>
- </div>
- </div>
- <div class="text-right">
- {{ selectedFile.name }}
- </div>
- <div class="text-right">
- {{ selectedFile.eol }}
- </div>
- <div class="text-right">
- {{ file.editorRow }}:{{ file.editorColumn }}
- </div>
- <div class="text-right">
- {{ selectedFile.fileLanguage }}
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue
deleted file mode 100644
index 56e31256132..00000000000
--- a/app/assets/javascripts/ide/components/new_branch_form.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<script>
- import { mapState, mapActions } from 'vuex';
- import flash, { hideFlash } from '../../flash';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-
- export default {
- components: {
- loadingIcon,
- },
- data() {
- return {
- branchName: '',
- loading: false,
- };
- },
- computed: {
- ...mapState([
- 'currentBranch',
- ]),
- btnDisabled() {
- return this.loading || this.branchName === '';
- },
- },
- created() {
- // Dropdown is outside of Vue instance & is controlled by Bootstrap
- this.$dropdown = $('.git-revision-dropdown');
-
- // text element is outside Vue app
- this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
- },
- methods: {
- ...mapActions([
- 'createNewBranch',
- ]),
- toggleDropdown() {
- this.$dropdown.dropdown('toggle');
- },
- submitNewBranch() {
- // need to query as the element is appended outside of Vue
- const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
-
- this.loading = true;
-
- if (flashEl) {
- hideFlash(flashEl, false);
- }
-
- this.createNewBranch(this.branchName)
- .then(() => {
- this.loading = false;
- this.branchName = '';
-
- if (this.dropdownText) {
- this.dropdownText.textContent = this.currentBranchId;
- }
-
- this.toggleDropdown();
- })
- .catch(res => res.json().then((data) => {
- this.loading = false;
- flash(data.message, 'alert', this.$el);
- }));
- },
- },
- };
-</script>
-
-<template>
- <div>
- <div
- class="flash-container"
- ref="flashContainer"
- >
- </div>
- <p>
- Create from:
- <code>{{ currentBranch }}</code>
- </p>
- <input
- class="form-control js-new-branch-name"
- type="text"
- placeholder="Name new branch"
- v-model="branchName"
- @keyup.enter.stop.prevent="submitNewBranch"
- />
- <div class="prepend-top-default clearfix">
- <button
- type="button"
- class="btn btn-primary pull-left"
- :disabled="btnDisabled"
- @click.stop.prevent="submitNewBranch"
- >
- <loading-icon
- v-if="loading"
- :inline="true"
- />
- <span>Create</span>
- </button>
- <button
- type="button"
- class="btn btn-default pull-right"
- @click.stop.prevent="toggleDropdown"
- >
- Cancel
- </button>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
deleted file mode 100644
index ef653357f5f..00000000000
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script>
- import newModal from './modal.vue';
- import upload from './upload.vue';
- import icon from '../../../vue_shared/components/icon.vue';
-
- export default {
- components: {
- icon,
- newModal,
- upload,
- },
- props: {
- branch: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: true,
- },
- parent: {
- type: Object,
- default: null,
- },
- },
- data() {
- return {
- openModal: false,
- modalType: '',
- };
- },
- methods: {
- createNewItem(type) {
- this.modalType = type;
- this.openModal = true;
- },
- hideModal() {
- this.openModal = false;
- },
- },
- };
-</script>
-
-<template>
- <div class="repo-new-btn pull-right">
- <div class="dropdown">
- <button
- type="button"
- class="btn btn-sm btn-default dropdown-toggle add-to-tree"
- data-toggle="dropdown"
- aria-label="Create new file or directory"
- >
- <icon
- name="plus"
- :size="12"
- css-classes="pull-left"
- />
- <icon
- name="arrow-down"
- :size="12"
- css-classes="pull-left"
- />
- </button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li>
- <a
- href="#"
- role="button"
- @click.prevent="createNewItem('blob')"
- >
- {{ __('New file') }}
- </a>
- </li>
- <li>
- <upload
- :branch-id="branch"
- :path="path"
- :parent="parent"
- />
- </li>
- <li>
- <a
- href="#"
- role="button"
- @click.prevent="createNewItem('tree')"
- >
- {{ __('New directory') }}
- </a>
- </li>
- </ul>
- </div>
- <new-modal
- v-if="openModal"
- :type="modalType"
- :branch-id="branch"
- :path="path"
- :parent="parent"
- @hide="hideModal"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
deleted file mode 100644
index 36cd825c6dd..00000000000
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<script>
- import { mapActions, mapState } from 'vuex';
- import { __ } from '../../../locale';
- import modal from '../../../vue_shared/components/modal.vue';
-
- export default {
- components: {
- modal,
- },
- props: {
- branchId: {
- type: String,
- required: true,
- },
- parent: {
- type: Object,
- default: null,
- },
- type: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- entryName: this.path !== '' ? `${this.path}/` : '',
- };
- },
- computed: {
- ...mapState([
- 'currentProjectId',
- ]),
- modalTitle() {
- if (this.type === 'tree') {
- return __('Create new directory');
- }
-
- return __('Create new file');
- },
- buttonLabel() {
- if (this.type === 'tree') {
- return __('Create directory');
- }
-
- return __('Create file');
- },
- formLabelName() {
- if (this.type === 'tree') {
- return __('Directory name');
- }
-
- return __('File name');
- },
- },
- mounted() {
- this.$refs.fieldName.focus();
- },
- methods: {
- ...mapActions([
- 'createTempEntry',
- ]),
- createEntryInStore() {
- this.createTempEntry({
- projectId: this.currentProjectId,
- branchId: this.branchId,
- parent: this.parent,
- name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
- type: this.type,
- });
-
- this.hideModal();
- },
- hideModal() {
- this.$emit('hide');
- },
- },
- };
-</script>
-
-<template>
- <modal
- :title="modalTitle"
- :primary-button-label="buttonLabel"
- kind="success"
- @cancel="hideModal"
- @submit="createEntryInStore"
- >
- <form
- class="form-horizontal"
- slot="body"
- @submit.prevent="createEntryInStore"
- >
- <fieldset class="form-group append-bottom-0">
- <label class="label-light col-sm-3">
- {{ formLabelName }}
- </label>
- <div class="col-sm-9">
- <input
- type="text"
- class="form-control"
- v-model="entryName"
- ref="fieldName"
- />
- </div>
- </fieldset>
- </form>
- </modal>
-</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
deleted file mode 100644
index 6244737fa43..00000000000
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<script>
- import { mapActions, mapState } from 'vuex';
-
- export default {
- props: {
- branchId: {
- type: String,
- required: true,
- },
- parent: {
- type: Object,
- default: null,
- },
- },
- computed: {
- ...mapState([
- 'trees',
- 'currentProjectId',
- ]),
- },
- mounted() {
- this.$refs.fileUpload.addEventListener('change', this.openFile);
- },
- beforeDestroy() {
- this.$refs.fileUpload.removeEventListener('change', this.openFile);
- },
- methods: {
- ...mapActions([
- 'createTempEntry',
- ]),
- createFile(target, file, isText) {
- const { name } = file;
- let { result } = target;
-
- if (!isText) {
- result = result.split('base64,')[1];
- }
-
- this.createTempEntry({
- name,
- projectId: this.currentProjectId,
- branchId: this.branchId,
- parent: this.parent,
- type: 'blob',
- content: result,
- base64: !isText,
- });
- },
- readFile(file) {
- const reader = new FileReader();
- const isText = file.type.match(/text.*/) !== null;
-
- reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
-
- if (isText) {
- reader.readAsText(file);
- } else {
- reader.readAsDataURL(file);
- }
- },
- openFile() {
- Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
- },
- startFileUpload() {
- this.$refs.fileUpload.click();
- },
- },
- };
-</script>
-
-<template>
- <div>
- <a
- href="#"
- role="button"
- @click.prevent="startFileUpload"
- >
- {{ __('Upload file') }}
- </a>
- <input
- id="file-upload"
- type="file"
- class="hidden"
- ref="fileUpload"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
deleted file mode 100644
index 96b1bb78c1d..00000000000
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ /dev/null
@@ -1,171 +0,0 @@
-<script>
-import { mapGetters, mapState, mapActions } from 'vuex';
-import tooltip from '../../vue_shared/directives/tooltip';
-import icon from '../../vue_shared/components/icon.vue';
-import modal from '../../vue_shared/components/modal.vue';
-import commitFilesList from './commit_sidebar/list.vue';
-
-export default {
- components: {
- modal,
- icon,
- commitFilesList,
- },
- directives: {
- tooltip,
- },
- data() {
- return {
- showNewBranchModal: false,
- submitCommitsLoading: false,
- startNewMR: false,
- commitMessage: '',
- };
- },
- computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- ]),
- ...mapGetters([
- 'changedFiles',
- ]),
- commitButtonDisabled() {
- return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
- },
- commitMessageCount() {
- return this.commitMessage.length;
- },
- },
- methods: {
- ...mapActions([
- 'checkCommitStatus',
- 'commitChanges',
- 'getTreeData',
- 'setPanelCollapsedStatus',
- ]),
- makeCommit(newBranch = false) {
- const createNewBranch = newBranch || this.startNewMR;
-
- const payload = {
- branch: createNewBranch ?
- `${this.currentBranchId}-${new Date().getTime().toString()}` :
- this.currentBranchId,
- commit_message: this.commitMessage,
- actions: this.changedFiles.map(f => ({
- action: f.tempFile ? 'create' : 'update',
- file_path: f.path,
- content: f.content,
- encoding: f.base64 ? 'base64' : 'text',
- })),
- start_branch: createNewBranch ? this.currentBranchId : undefined,
- };
-
- this.showNewBranchModal = false;
- this.submitCommitsLoading = true;
-
- this.commitChanges({ payload, newMr: this.startNewMR })
- .then(() => {
- this.submitCommitsLoading = false;
- this.commitMessage = '';
- this.startNewMR = false;
- })
- .catch(() => {
- this.submitCommitsLoading = false;
- });
- },
- tryCommit() {
- this.submitCommitsLoading = true;
-
- this.checkCommitStatus()
- .then((branchChanged) => {
- if (branchChanged) {
- this.showNewBranchModal = true;
- } else {
- this.makeCommit();
- }
- })
- .catch(() => {
- this.submitCommitsLoading = false;
- });
- },
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'right',
- collapsed: !this.rightPanelCollapsed,
- });
- },
- },
-};
-</script>
-
-<template>
- <div class="multi-file-commit-panel-section">
- <modal
- v-if="showNewBranchModal"
- :primary-button-label="__('Create new branch')"
- kind="primary"
- :title="__('Branch has changed')"
- :text="__(`This branch has changed since
-you started editing. Would you like to create a new branch?`)"
- @cancel="showNewBranchModal = false"
- @submit="makeCommit(true)"
- />
- <commit-files-list
- title="Staged"
- :file-list="changedFiles"
- :collapsed="rightPanelCollapsed"
- @toggleCollapsed="toggleCollapsed"
- />
- <form
- class="form-horizontal multi-file-commit-form"
- @submit.prevent="tryCommit"
- v-if="!rightPanelCollapsed"
- >
- <div class="multi-file-commit-fieldset">
- <textarea
- class="form-control multi-file-commit-message"
- name="commit-message"
- v-model="commitMessage"
- placeholder="Commit message"
- >
- </textarea>
- </div>
- <div class="multi-file-commit-fieldset">
- <label
- v-tooltip
- title="Create a new merge request with these changes"
- data-container="body"
- data-placement="top"
- >
- <input
- type="checkbox"
- v-model="startNewMR"
- />
- Merge Request
- </label>
- <button
- type="submit"
- :disabled="commitButtonDisabled"
- class="btn btn-default btn-sm append-right-10 prepend-left-10"
- :class="{ disabled: submitCommitsLoading }"
- >
- <i
- v-if="submitCommitsLoading"
- class="js-commit-loading-icon fa fa-spinner fa-spin"
- aria-hidden="true"
- aria-label="loading"
- >
- </i>
- Commit
- </button>
- <div
- class="multi-file-commit-message-count"
- >
- {{ commitMessageCount }}
- </div>
- </div>
- </form>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue
deleted file mode 100644
index c43e9163340..00000000000
--- a/app/assets/javascripts/ide/components/repo_edit_button.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
-import { mapGetters, mapActions, mapState } from 'vuex';
-import modal from '../../vue_shared/components/modal.vue';
-
-export default {
- components: {
- modal,
- },
- computed: {
- ...mapState([
- 'editMode',
- 'discardPopupOpen',
- ]),
- ...mapGetters([
- 'canEditFile',
- ]),
- buttonLabel() {
- return this.editMode ? this.__('Cancel edit') : this.__('Edit');
- },
- },
- methods: {
- ...mapActions([
- 'toggleEditMode',
- 'closeDiscardPopup',
- ]),
- },
-};
-</script>
-
-<template>
- <div class="editable-mode">
- <button
- v-if="canEditFile"
- class="btn btn-default"
- type="button"
- @click.prevent="toggleEditMode()">
- <i
- v-if="!editMode"
- class="fa fa-pencil"
- aria-hidden="true">
- </i>
- <span>
- {{ buttonLabel }}
- </span>
- </button>
- <modal
- v-if="discardPopupOpen"
- class="text-left"
- :primary-button-label="__('Discard changes')"
- kind="warning"
- :title="__('Are you sure?')"
- :text="__('Are you sure you want to discard your changes?')"
- @cancel="closeDiscardPopup"
- @submit="toggleEditMode(true)"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
deleted file mode 100644
index f99228012f4..00000000000
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<script>
-/* global monaco */
-import { mapState, mapGetters, mapActions } from 'vuex';
-import flash from '../../flash';
-import monacoLoader from '../monaco_loader';
-import Editor from '../lib/editor';
-
-export default {
- computed: {
- ...mapGetters([
- 'activeFile',
- 'activeFileExtension',
- ]),
- ...mapState([
- 'leftPanelCollapsed',
- 'rightPanelCollapsed',
- 'panelResizing',
- ]),
- shouldHideEditor() {
- return this.activeFile.binary && !this.activeFile.raw;
- },
- },
- watch: {
- activeFile(oldVal, newVal) {
- if (newVal && !newVal.active) {
- this.initMonaco();
- }
- },
- leftPanelCollapsed() {
- this.editor.updateDimensions();
- },
- rightPanelCollapsed() {
- this.editor.updateDimensions();
- },
- panelResizing(isResizing) {
- if (isResizing === false) {
- this.editor.updateDimensions();
- }
- },
- },
- beforeDestroy() {
- this.editor.dispose();
- },
- mounted() {
- if (this.editor && monaco) {
- this.initMonaco();
- } else {
- monacoLoader(['vs/editor/editor.main'], () => {
- this.editor = Editor.create(monaco);
-
- this.initMonaco();
- });
- }
- },
- methods: {
- ...mapActions([
- 'getRawFileData',
- 'changeFileContent',
- 'setFileLanguage',
- 'setEditorPosition',
- 'setFileEOL',
- ]),
- initMonaco() {
- if (this.shouldHideEditor) return;
-
- this.editor.clearEditor();
-
- this.getRawFileData(this.activeFile)
- .then(() => {
- this.editor.createInstance(this.$refs.editor);
- })
- .then(() => this.setupEditor())
- .catch((err) => {
- flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
- throw err;
- });
- },
- setupEditor() {
- if (!this.activeFile) return;
-
- const model = this.editor.createModel(this.activeFile);
-
- this.editor.attachModel(model);
-
- model.onChange((m) => {
- this.changeFileContent({
- file: this.activeFile,
- content: m.getValue(),
- });
- });
-
- // Handle Cursor Position
- this.editor.onPositionChange((instance, e) => {
- this.setEditorPosition({
- editorRow: e.position.lineNumber,
- editorColumn: e.position.column,
- });
- });
-
- this.editor.setPosition({
- lineNumber: this.activeFile.editorRow,
- column: this.activeFile.editorColumn,
- });
-
- // Handle File Language
- this.setFileLanguage({
- fileLanguage: model.language,
- });
-
- // Get File eol
- this.setFileEOL({
- eol: model.eol,
- });
- },
- },
-};
-</script>
-
-<template>
- <div
- id="ide"
- class="blob-viewer-container blob-editor-container"
- >
- <div
- v-if="shouldHideEditor"
- v-html="activeFile.html"
- >
- </div>
- <div
- v-show="!shouldHideEditor"
- ref="editor"
- class="multi-file-editor-holder"
- >
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
deleted file mode 100644
index 110918872fb..00000000000
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ /dev/null
@@ -1,165 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import timeAgoMixin from '../../vue_shared/mixins/timeago';
- import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
- import newDropdown from './new_dropdown/index.vue';
- import fileIcon from '../../vue_shared/components/file_icon.vue';
-
- export default {
- components: {
- skeletonLoadingContainer,
- newDropdown,
- fileIcon,
- },
- mixins: [
- timeAgoMixin,
- ],
- props: {
- file: {
- type: Object,
- required: true,
- },
- showExtraColumns: {
- type: Boolean,
- default: false,
- },
- },
- computed: {
- ...mapState([
- 'leftPanelCollapsed',
- ]),
- isSubmodule() {
- return this.file.type === 'submodule';
- },
- isTree() {
- return this.file.type === 'tree';
- },
- levelIndentation() {
- if (this.file.level > 0) {
- return {
- marginLeft: `${this.file.level * 16}px`,
- };
- }
- return {};
- },
- shortId() {
- return this.file.id.substr(0, 8);
- },
- submoduleColSpan() {
- return !this.leftPanelCollapsed && this.isSubmodule ? 3 : 1;
- },
- fileClass() {
- if (this.file.type === 'blob') {
- if (this.file.active) {
- return 'file-open file-active';
- }
- return this.file.opened ? 'file-open' : '';
- }
- return '';
- },
- changedClass() {
- return {
- 'fa-circle unsaved-icon': this.file.changed || this.file.tempFile,
- };
- },
- },
- updated() {
- if (this.file.type === 'blob' && this.file.active) {
- this.$el.scrollIntoView();
- }
- },
- methods: {
- clickFile(row) {
- // Manual Action if a tree is selected/opened
- if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
- this.$store.dispatch('toggleTreeOpen', {
- endpoint: this.file.url,
- tree: this.file,
- });
- }
- this.$router.push(`/project${row.url}`);
- },
- },
- };
-</script>
-
-<template>
- <tr
- class="file"
- :class="fileClass"
- @click="clickFile(file)">
- <td
- class="multi-file-table-name"
- :colspan="submoduleColSpan"
- >
- <a
- class="repo-file-name"
- >
- <file-icon
- :file-name="file.name"
- :loading="file.loading"
- :folder="file.type === 'tree'"
- :opened="file.opened"
- :style="levelIndentation"
- :size="16"
- />
- {{ file.name }}
- </a>
- <new-dropdown
- v-if="isTree"
- :project-id="file.projectId"
- :branch="file.branchId"
- :path="file.path"
- :parent="file"
- />
- <i
- class="fa"
- v-if="file.changed || file.tempFile"
- :class="changedClass"
- aria-hidden="true"
- >
- </i>
- <template v-if="isSubmodule && file.id">
- @
- <span class="commit-sha">
- <a
- @click.stop
- :href="file.tree_url"
- >
- {{ shortId }}
- </a>
- </span>
- </template>
- </td>
-
- <template v-if="showExtraColumns && !isSubmodule">
- <td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
- <a
- v-if="file.lastCommit.message"
- @click.stop
- :href="file.lastCommit.url"
- >
- {{ file.lastCommit.message }}
- </a>
- <skeleton-loading-container
- v-else
- :small="true"
- />
- </td>
-
- <td class="commit-update hidden-xs text-right">
- <span
- v-if="file.lastCommit.updatedAt"
- :title="tooltipTitle(file.lastCommit.updatedAt)"
- >
- {{ timeFormated(file.lastCommit.updatedAt) }}
- </span>
- <skeleton-loading-container
- v-else
- class="animation-container-right"
- :small="true"
- />
- </td>
- </template>
- </tr>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue
deleted file mode 100644
index aabc0d8eada..00000000000
--- a/app/assets/javascripts/ide/components/repo_file_buttons.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<script>
-import { mapGetters } from 'vuex';
-
-export default {
- computed: {
- ...mapGetters([
- 'activeFile',
- ]),
- showButtons() {
- return this.activeFile.rawPath ||
- this.activeFile.blamePath ||
- this.activeFile.commitsPath ||
- this.activeFile.permalink;
- },
- rawDownloadButtonLabel() {
- return this.activeFile.binary ? 'Download' : 'Raw';
- },
- },
-};
-</script>
-
-<template>
- <div
- v-if="showButtons"
- class="multi-file-editor-btn-group"
- >
- <a
- :href="activeFile.rawPath"
- target="_blank"
- class="btn btn-default btn-sm raw"
- rel="noopener noreferrer">
- {{ rawDownloadButtonLabel }}
- </a>
-
- <div
- class="btn-group"
- role="group"
- aria-label="File actions"
- >
- <a
- :href="activeFile.blamePath"
- class="btn btn-default btn-sm blame"
- >
- Blame
- </a>
- <a
- :href="activeFile.commitsPath"
- class="btn btn-default btn-sm history"
- >
- History
- </a>
- <a
- :href="activeFile.permalink"
- class="btn btn-default btn-sm permalink"
- >
- Permalink
- </a>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
deleted file mode 100644
index 3aeb6f0b28f..00000000000
--- a/app/assets/javascripts/ide/components/repo_loading_file.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
-
- export default {
- components: {
- skeletonLoadingContainer,
- },
- computed: {
- ...mapState([
- 'leftPanelCollapsed',
- ]),
- },
- };
-</script>
-
-<template>
- <tr
- class="loading-file"
- aria-label="Loading files"
- >
- <td class="multi-file-table-col-name">
- <skeleton-loading-container
- :small="true"
- />
- </td>
- <template v-if="!leftPanelCollapsed">
- <td class="hidden-sm hidden-xs">
- <skeleton-loading-container
- :small="true"
- />
- </td>
-
- <td class="hidden-xs">
- <skeleton-loading-container
- class="animation-container-right"
- :small="true"
- />
- </td>
- </template>
- </tr>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_prev_directory.vue b/app/assets/javascripts/ide/components/repo_prev_directory.vue
deleted file mode 100644
index 7cd359ea4ed..00000000000
--- a/app/assets/javascripts/ide/components/repo_prev_directory.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
- import { mapState, mapActions } from 'vuex';
-
- export default {
- computed: {
- ...mapState([
- 'parentTreeUrl',
- 'leftPanelCollapsed',
- ]),
- colSpanCondition() {
- return this.leftPanelCollapsed ? undefined : 3;
- },
- },
- methods: {
- ...mapActions([
- 'getTreeData',
- ]),
- },
- };
-</script>
-
-<template>
- <tr class="file prev-directory">
- <td
- :colspan="colSpanCondition"
- class="table-cell"
- @click.prevent="getTreeData({ endpoint: parentTreeUrl })"
- >
- <a :href="parentTreeUrl">...</a>
- </td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue
deleted file mode 100644
index e47270a9855..00000000000
--- a/app/assets/javascripts/ide/components/repo_preview.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<script>
- import { mapGetters } from 'vuex';
- import LineHighlighter from '../../line_highlighter';
- import syntaxHighlight from '../../syntax_highlight';
-
- export default {
- computed: {
- ...mapGetters([
- 'activeFile',
- ]),
- renderErrorTooLarge() {
- return this.activeFile.renderError === 'too_large';
- },
- },
- mounted() {
- this.highlightFile();
- this.lineHighlighter = new LineHighlighter({
- fileHolderSelector: '.blob-viewer-container',
- scrollFileHolder: true,
- });
- },
- updated() {
- this.$nextTick(() => {
- this.highlightFile();
- });
- },
- methods: {
- highlightFile() {
- syntaxHighlight($(this.$el).find('.file-content'));
- },
- },
- };
-</script>
-
-<template>
- <div>
- <div
- v-if="!activeFile.renderError"
- v-html="activeFile.html"
- class="multi-file-preview-holder"
- >
- </div>
- <div
- v-else-if="activeFile.tempFile"
- class="vertical-center render-error">
- <p class="text-center">
- The source could not be displayed for this temporary file.
- </p>
- </div>
- <div
- v-else-if="renderErrorTooLarge"
- class="vertical-center render-error">
- <p class="text-center">
- The source could not be displayed because it is too large.
- You can <a
- :href="activeFile.rawPath"
- download>download</a> it instead.
- </p>
- </div>
- <div
- v-else
- class="vertical-center render-error">
- <p class="text-center">
- The source could not be displayed because a rendering error occurred.
- You can <a
- :href="activeFile.rawPath"
- download>download</a> it instead.
- </p>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
deleted file mode 100644
index 5ed7bddf6ae..00000000000
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<script>
- import { mapActions } from 'vuex';
- import fileIcon from '../../vue_shared/components/file_icon.vue';
-
- export default {
- components: {
- fileIcon,
- },
- props: {
- tab: {
- type: Object,
- required: true,
- },
- },
- computed: {
- closeLabel() {
- if (this.tab.changed || this.tab.tempFile) {
- return `${this.tab.name} changed`;
- }
- return `Close ${this.tab.name}`;
- },
- changedClass() {
- const tabChangedObj = {
- 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
- 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
- };
- return tabChangedObj;
- },
- },
-
- methods: {
- ...mapActions([
- 'closeFile',
- ]),
- clickFile(tab) {
- this.$router.push(`/project${tab.url}`);
- },
- },
- };
-</script>
-
-<template>
- <li @click="clickFile(tab)">
- <button
- type="button"
- class="multi-file-tab-close"
- @click.stop.prevent="closeFile({ file: tab })"
- :aria-label="closeLabel"
- :class="{
- 'modified': tab.changed,
- }"
- :disabled="tab.changed"
- >
- <i
- class="fa"
- :class="changedClass"
- aria-hidden="true"
- >
- </i>
- </button>
-
- <div
- class="multi-file-tab"
- :class="{active : tab.active }"
- :title="tab.url"
- >
- <file-icon
- :file-name="tab.name"
- :size="16"
- />
- {{ tab.name }}
- </div>
- </li>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
deleted file mode 100644
index ca363bba0ef..00000000000
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import RepoTab from './repo_tab.vue';
-
- export default {
- components: {
- 'repo-tab': RepoTab,
- },
- computed: {
- ...mapState([
- 'openFiles',
- ]),
- },
- };
-</script>
-
-<template>
- <ul
- class="multi-file-tabs list-unstyled append-bottom-0"
- >
- <repo-tab
- v-for="tab in openFiles"
- :key="tab.key"
- :tab="tab"
- />
- </ul>
-</template>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
deleted file mode 100644
index a7fb9e0588a..00000000000
--- a/app/assets/javascripts/ide/ide_router.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-import store from './stores';
-import flash from '../flash';
-import {
- getTreeEntry,
-} from './stores/utils';
-
-Vue.use(VueRouter);
-
-/**
- * Routes below /-/ide/:
-
-/project/h5bp/html5-boilerplate/blob/master
-/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
-
-/project/h5bp/html5-boilerplate/mr/123
-/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
-
-/workspace/123
-/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
-/workspace/project/h5bp/html5-boilerplate/mr/123
-
-/ = /workspace
-
-/settings
-*/
-
-// Unfortunately Vue Router doesn't work without at least a fake component
-// If you do only data handling
-const EmptyRouterComponent = {
- render(createElement) {
- return createElement('div');
- },
-};
-
-const router = new VueRouter({
- mode: 'history',
- base: `${gon.relative_url_root}/-/ide/`,
- routes: [
- {
- path: '/project/:namespace/:project',
- component: EmptyRouterComponent,
- children: [
- {
- path: ':targetmode/:branch/*',
- component: EmptyRouterComponent,
- },
- {
- path: 'mr/:mrid',
- component: EmptyRouterComponent,
- },
- ],
- },
- ],
-});
-
-router.beforeEach((to, from, next) => {
- if (to.params.namespace && to.params.project) {
- store.dispatch('getProjectData', {
- namespace: to.params.namespace,
- projectId: to.params.project,
- })
- .then(() => {
- const fullProjectId = `${to.params.namespace}/${to.params.project}`;
-
- if (to.params.branch) {
- store.dispatch('getBranchData', {
- projectId: fullProjectId,
- branchId: to.params.branch,
- });
-
- store.dispatch('getTreeData', {
- projectId: fullProjectId,
- branch: to.params.branch,
- endpoint: `/tree/${to.params.branch}`,
- })
- .then(() => {
- if (to.params[0]) {
- const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
- if (treeEntry) {
- store.dispatch('handleTreeEntryAction', treeEntry);
- }
- }
- })
- .catch((e) => {
- flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true);
- throw e;
- });
- }
- })
- .catch((e) => {
- flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true);
- throw e;
- });
- }
-
- next();
-});
-
-export default router;
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
deleted file mode 100644
index e8a19f47cee..00000000000
--- a/app/assets/javascripts/ide/index.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import Vue from 'vue';
-import ide from './components/ide.vue';
-import store from './stores';
-import router from './ide_router';
-import Translate from '../vue_shared/translate';
-
-function initIde(el) {
- if (!el) return null;
-
- return new Vue({
- el,
- store,
- router,
- components: {
- ide,
- },
- render(createElement) {
- return createElement('ide', {
- props: {
- emptyStateSvgPath: el.dataset.emptyStateSvgPath,
- },
- });
- },
- });
-}
-
-const ideElement = document.getElementById('ide');
-
-Vue.use(Translate);
-
-initIde(ideElement);
diff --git a/app/assets/javascripts/ide/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js
deleted file mode 100644
index 84b29bdb600..00000000000
--- a/app/assets/javascripts/ide/lib/common/disposable.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default class Disposable {
- constructor() {
- this.disposers = new Set();
- }
-
- add(...disposers) {
- disposers.forEach(disposer => this.disposers.add(disposer));
- }
-
- dispose() {
- this.disposers.forEach(disposer => disposer.dispose());
- this.disposers.clear();
- }
-}
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
deleted file mode 100644
index 14d9fe4771e..00000000000
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* global monaco */
-import Disposable from './disposable';
-
-export default class Model {
- constructor(monaco, file) {
- this.monaco = monaco;
- this.disposable = new Disposable();
- this.file = file;
- this.content = file.content !== '' ? file.content : file.raw;
-
- this.disposable.add(
- this.originalModel = this.monaco.editor.createModel(
- this.file.raw,
- undefined,
- new this.monaco.Uri(null, null, `original/${this.file.path}`),
- ),
- this.model = this.monaco.editor.createModel(
- this.content,
- undefined,
- new this.monaco.Uri(null, null, this.file.path),
- ),
- );
-
- this.events = new Map();
- }
-
- get url() {
- return this.model.uri.toString();
- }
-
- get language() {
- return this.model.getModeId();
- }
-
- get eol() {
- return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
- }
-
- get path() {
- return this.file.path;
- }
-
- getModel() {
- return this.model;
- }
-
- getOriginalModel() {
- return this.originalModel;
- }
-
- onChange(cb) {
- this.events.set(
- this.path,
- this.disposable.add(
- this.model.onDidChangeContent(e => cb(this.model, e)),
- ),
- );
- }
-
- dispose() {
- this.disposable.dispose();
- this.events.clear();
- }
-}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
deleted file mode 100644
index fd462252795..00000000000
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Disposable from './disposable';
-import Model from './model';
-
-export default class ModelManager {
- constructor(monaco) {
- this.monaco = monaco;
- this.disposable = new Disposable();
- this.models = new Map();
- }
-
- hasCachedModel(path) {
- return this.models.has(path);
- }
-
- addModel(file) {
- if (this.hasCachedModel(file.path)) {
- return this.models.get(file.path);
- }
-
- const model = new Model(this.monaco, file);
- this.models.set(model.path, model);
- this.disposable.add(model);
-
- return model;
- }
-
- dispose() {
- // dispose of all the models
- this.disposable.dispose();
- this.models.clear();
- }
-}
diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
deleted file mode 100644
index 0954b7973c4..00000000000
--- a/app/assets/javascripts/ide/lib/decorations/controller.js
+++ /dev/null
@@ -1,43 +0,0 @@
-export default class DecorationsController {
- constructor(editor) {
- this.editor = editor;
- this.decorations = new Map();
- this.editorDecorations = new Map();
- }
-
- getAllDecorationsForModel(model) {
- if (!this.decorations.has(model.url)) return [];
-
- const modelDecorations = this.decorations.get(model.url);
- const decorations = [];
-
- modelDecorations.forEach(val => decorations.push(...val));
-
- return decorations;
- }
-
- addDecorations(model, decorationsKey, decorations) {
- const decorationMap = this.decorations.get(model.url) || new Map();
-
- decorationMap.set(decorationsKey, decorations);
-
- this.decorations.set(model.url, decorationMap);
-
- this.decorate(model);
- }
-
- decorate(model) {
- const decorations = this.getAllDecorationsForModel(model);
- const oldDecorations = this.editorDecorations.get(model.url) || [];
-
- this.editorDecorations.set(
- model.url,
- this.editor.instance.deltaDecorations(oldDecorations, decorations),
- );
- }
-
- dispose() {
- this.decorations.clear();
- this.editorDecorations.clear();
- }
-}
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
deleted file mode 100644
index dc0b1c95e59..00000000000
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/* global monaco */
-import { throttle } from 'underscore';
-import DirtyDiffWorker from './diff_worker';
-import Disposable from '../common/disposable';
-
-export const getDiffChangeType = (change) => {
- if (change.modified) {
- return 'modified';
- } else if (change.added) {
- return 'added';
- } else if (change.removed) {
- return 'removed';
- }
-
- return '';
-};
-
-export const getDecorator = change => ({
- range: new monaco.Range(
- change.lineNumber,
- 1,
- change.endLineNumber,
- 1,
- ),
- options: {
- isWholeLine: true,
- linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
- },
-});
-
-export default class DirtyDiffController {
- constructor(modelManager, decorationsController) {
- this.disposable = new Disposable();
- this.editorSimpleWorker = null;
- this.modelManager = modelManager;
- this.decorationsController = decorationsController;
- this.dirtyDiffWorker = new DirtyDiffWorker();
- this.throttledComputeDiff = throttle(this.computeDiff, 250);
- this.decorate = this.decorate.bind(this);
-
- this.dirtyDiffWorker.addEventListener('message', this.decorate);
- }
-
- attachModel(model) {
- model.onChange(() => this.throttledComputeDiff(model));
- }
-
- computeDiff(model) {
- this.dirtyDiffWorker.postMessage({
- path: model.path,
- originalContent: model.getOriginalModel().getValue(),
- newContent: model.getModel().getValue(),
- });
- }
-
- reDecorate(model) {
- this.decorationsController.decorate(model);
- }
-
- decorate({ data }) {
- const decorations = data.changes.map(change => getDecorator(change));
- this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations);
- }
-
- dispose() {
- this.disposable.dispose();
-
- this.dirtyDiffWorker.removeEventListener('message', this.decorate);
- this.dirtyDiffWorker.terminate();
- }
-}
diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js
deleted file mode 100644
index 0e37f5c4704..00000000000
--- a/app/assets/javascripts/ide/lib/diff/diff.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { diffLines } from 'diff';
-
-// eslint-disable-next-line import/prefer-default-export
-export const computeDiff = (originalContent, newContent) => {
- const changes = diffLines(originalContent, newContent);
-
- let lineNumber = 1;
- return changes.reduce((acc, change) => {
- const findOnLine = acc.find(c => c.lineNumber === lineNumber);
-
- if (findOnLine) {
- Object.assign(findOnLine, change, {
- modified: true,
- endLineNumber: (lineNumber + change.count) - 1,
- });
- } else if ('added' in change || 'removed' in change) {
- acc.push(Object.assign({}, change, {
- lineNumber,
- modified: undefined,
- endLineNumber: (lineNumber + change.count) - 1,
- }));
- }
-
- if (!change.removed) {
- lineNumber += change.count;
- }
-
- return acc;
- }, []);
-};
diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
deleted file mode 100644
index e74c4046330..00000000000
--- a/app/assets/javascripts/ide/lib/diff/diff_worker.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { computeDiff } from './diff';
-
-self.addEventListener('message', (e) => {
- const data = e.data;
-
- self.postMessage({
- path: data.path,
- changes: computeDiff(data.originalContent, data.newContent),
- });
-});
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
deleted file mode 100644
index 51255f15658..00000000000
--- a/app/assets/javascripts/ide/lib/editor.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import _ from 'underscore';
-import DecorationsController from './decorations/controller';
-import DirtyDiffController from './diff/controller';
-import Disposable from './common/disposable';
-import ModelManager from './common/model_manager';
-import editorOptions from './editor_options';
-
-export default class Editor {
- static create(monaco) {
- this.editorInstance = new Editor(monaco);
-
- return this.editorInstance;
- }
-
- constructor(monaco) {
- this.monaco = monaco;
- this.currentModel = null;
- this.instance = null;
- this.dirtyDiffController = null;
- this.disposable = new Disposable();
-
- this.disposable.add(
- this.modelManager = new ModelManager(this.monaco),
- this.decorationsController = new DecorationsController(this),
- );
-
- this.debouncedUpdate = _.debounce(() => {
- this.updateDimensions();
- }, 200);
- window.addEventListener('resize', this.debouncedUpdate, false);
- }
-
- createInstance(domElement) {
- if (!this.instance) {
- this.disposable.add(
- this.instance = this.monaco.editor.create(domElement, {
- model: null,
- readOnly: false,
- contextmenu: true,
- scrollBeyondLastLine: false,
- minimap: {
- enabled: false,
- },
- }),
- this.dirtyDiffController = new DirtyDiffController(
- this.modelManager, this.decorationsController,
- ),
- );
- }
- }
-
- createModel(file) {
- return this.modelManager.addModel(file);
- }
-
- attachModel(model) {
- this.instance.setModel(model.getModel());
- if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
-
- this.currentModel = model;
-
- this.instance.updateOptions(editorOptions.reduce((acc, obj) => {
- Object.keys(obj).forEach((key) => {
- Object.assign(acc, {
- [key]: obj[key](model),
- });
- });
- return acc;
- }, {}));
-
- if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
- }
-
- clearEditor() {
- if (this.instance) {
- this.instance.setModel(null);
- }
- }
-
- dispose() {
- this.disposable.dispose();
- window.removeEventListener('resize', this.debouncedUpdate);
-
- // dispose main monaco instance
- if (this.instance) {
- this.instance = null;
- }
- }
-
- updateDimensions() {
- this.instance.layout();
- }
-
- setPosition({ lineNumber, column }) {
- this.instance.revealPositionInCenter({
- lineNumber,
- column,
- });
- this.instance.setPosition({
- lineNumber,
- column,
- });
- }
-
- onPositionChange(cb) {
- this.disposable.add(
- this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
- );
- }
-}
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
deleted file mode 100644
index 701affc466e..00000000000
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export default [{
-}];
diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js
deleted file mode 100644
index 142a220097b..00000000000
--- a/app/assets/javascripts/ide/monaco_loader.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import monacoContext from 'monaco-editor/dev/vs/loader';
-
-monacoContext.require.config({
- paths: {
- vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
- },
-});
-
-// ignore CDN config and use local assets path for service worker which cannot be cross-domain
-const relativeRootPath = (gon && gon.relative_url_root) || '';
-const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`;
-window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` };
-
-// eslint-disable-next-line no-underscore-dangle
-window.__monaco_context__ = monacoContext;
-export default monacoContext.require;
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
deleted file mode 100644
index 1fb24e93f2e..00000000000
--- a/app/assets/javascripts/ide/services/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-import Api from '../../api';
-
-Vue.use(VueResource);
-
-export default {
- getTreeData(endpoint) {
- return Vue.http.get(endpoint, { params: { format: 'json' } });
- },
- getFileData(endpoint) {
- return Vue.http.get(endpoint, { params: { format: 'json' } });
- },
- getRawFileData(file) {
- if (file.tempFile) {
- return Promise.resolve(file.content);
- }
-
- if (file.raw) {
- return Promise.resolve(file.raw);
- }
-
- return Vue.http.get(file.rawPath, { params: { format: 'json' } })
- .then(res => res.text());
- },
- getProjectData(namespace, project) {
- return Api.project(`${namespace}/${project}`);
- },
- getBranchData(projectId, currentBranchId) {
- return Api.branchSingle(projectId, currentBranchId);
- },
- createBranch(projectId, payload) {
- const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
-
- return Vue.http.post(url, payload);
- },
- commit(projectId, payload) {
- return Api.commitMultiple(projectId, payload);
- },
- getTreeLastCommit(endpoint) {
- return Vue.http.get(endpoint, {
- params: {
- format: 'json',
- },
- });
- },
-};
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
deleted file mode 100644
index d007d0ae78f..00000000000
--- a/app/assets/javascripts/ide/stores/actions.js
+++ /dev/null
@@ -1,196 +0,0 @@
-import Vue from 'vue';
-import { visitUrl } from '../../lib/utils/url_utility';
-import flash from '../../flash';
-import service from '../services';
-import * as types from './mutation_types';
-import { stripHtml } from '../../lib/utils/text_utility';
-
-export const redirectToUrl = (_, url) => visitUrl(url);
-
-export const setInitialData = ({ commit }, data) =>
- commit(types.SET_INITIAL_DATA, data);
-
-export const closeDiscardPopup = ({ commit }) =>
- commit(types.TOGGLE_DISCARD_POPUP, false);
-
-export const discardAllChanges = ({ commit, getters, dispatch }) => {
- const changedFiles = getters.changedFiles;
-
- changedFiles.forEach((file) => {
- commit(types.DISCARD_FILE_CHANGES, file);
-
- if (file.tempFile) {
- dispatch('closeFile', { file, force: true });
- }
- });
-};
-
-export const closeAllFiles = ({ state, dispatch }) => {
- state.openFiles.forEach(file => dispatch('closeFile', { file }));
-};
-
-export const toggleEditMode = (
- { state, commit, getters, dispatch },
- force = false,
-) => {
- const changedFiles = getters.changedFiles;
-
- if (changedFiles.length && !force) {
- commit(types.TOGGLE_DISCARD_POPUP, true);
- } else {
- commit(types.TOGGLE_EDIT_MODE);
- commit(types.TOGGLE_DISCARD_POPUP, false);
- dispatch('toggleBlobView');
-
- if (!state.editMode) {
- dispatch('discardAllChanges');
- }
- }
-};
-
-export const toggleBlobView = ({ commit, state }) => {
- if (state.editMode) {
- commit(types.SET_EDIT_MODE);
- } else {
- commit(types.SET_PREVIEW_MODE);
- }
-};
-
-export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
- if (side === 'left') {
- commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
- } else {
- commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
- }
-};
-
-export const setResizingStatus = ({ commit }, resizing) => {
- commit(types.SET_RESIZING_STATUS, resizing);
-};
-
-export const checkCommitStatus = ({ state }) =>
- service
- .getBranchData(state.currentProjectId, state.currentBranchId)
- .then(({ data }) => {
- const { id } = data.commit;
- const selectedBranch =
- state.projects[state.currentProjectId].branches[state.currentBranchId];
-
- if (selectedBranch.workingReference !== id) {
- return true;
- }
-
- return false;
- })
- .catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true));
-
-export const commitChanges = (
- { commit, state, dispatch, getters },
- { payload, newMr },
-) =>
- service
- .commit(state.currentProjectId, payload)
- .then(({ data }) => {
- const { branch } = payload;
- if (!data.short_id) {
- flash(data.message, 'alert', document, null, false, true);
- return;
- }
-
- const selectedProject = state.projects[state.currentProjectId];
- const lastCommit = {
- commit_path: `${selectedProject.web_url}/commit/${data.id}`,
- commit: {
- message: data.message,
- authored_date: data.committed_date,
- },
- };
-
- let commitMsg = `Your changes have been committed. Commit ${data.short_id}`;
- if (data.stats) {
- commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
- }
-
- flash(
- commitMsg,
- 'notice',
- document,
- null,
- false,
- true);
- window.dispatchEvent(new Event('resize'));
-
- if (newMr) {
- dispatch('discardAllChanges');
- dispatch(
- 'redirectToUrl',
- `${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
- );
- } else {
- commit(types.SET_BRANCH_WORKING_REFERENCE, {
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
- reference: data.id,
- });
-
- getters.changedFiles.forEach((entry) => {
- commit(types.SET_LAST_COMMIT_DATA, {
- entry,
- lastCommit,
- });
- });
-
- dispatch('discardAllChanges');
-
- window.scrollTo(0, 0);
- }
- })
- .catch((err) => {
- let errMsg = 'Error committing changes. Please try again.';
- if (err.response.data && err.response.data.message) {
- errMsg += ` (${stripHtml(err.response.data.message)})`;
- }
- flash(errMsg, 'alert', document, null, false, true);
- window.dispatchEvent(new Event('resize'));
- });
-
-export const createTempEntry = (
- { state, dispatch },
- { projectId, branchId, parent, name, type, content = '', base64 = false },
-) => {
- const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
- if (type === 'tree') {
- dispatch('createTempTree', {
- projectId,
- branchId,
- parent: selectedParent,
- name,
- });
- } else if (type === 'blob') {
- dispatch('createTempFile', {
- projectId,
- branchId,
- parent: selectedParent,
- name,
- base64,
- content,
- });
- }
-};
-
-export const scrollToTab = () => {
- Vue.nextTick(() => {
- const tabs = document.getElementById('tabs');
-
- if (tabs) {
- const tabEl = tabs.querySelector('.active .repo-tab');
-
- tabEl.focus();
- }
- });
-};
-
-export * from './actions/tree';
-export * from './actions/file';
-export * from './actions/project';
-export * from './actions/branch';
diff --git a/app/assets/javascripts/ide/stores/actions/branch.js b/app/assets/javascripts/ide/stores/actions/branch.js
deleted file mode 100644
index bc6fd2d4163..00000000000
--- a/app/assets/javascripts/ide/stores/actions/branch.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import service from '../../services';
-import flash from '../../../flash';
-import * as types from '../mutation_types';
-
-export const getBranchData = (
- { commit, state, dispatch },
- { projectId, branchId, force = false } = {},
-) => new Promise((resolve, reject) => {
- if ((typeof state.projects[`${projectId}`] === 'undefined' ||
- !state.projects[`${projectId}`].branches[branchId])
- || force) {
- service.getBranchData(`${projectId}`, branchId)
- .then(({ data }) => {
- const { id } = data.commit;
- commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
- commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- resolve(data);
- })
- .catch(() => {
- flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
- reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
- });
- } else {
- resolve(state.projects[`${projectId}`].branches[branchId]);
- }
-});
-
-export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
- state.currentProjectId,
- {
- branch,
- ref: state.currentBranchId,
- },
-)
-.then(res => res.json())
-.then((data) => {
- const branchName = data.name;
- const url = location.href.replace(state.currentBranchId, branchName);
-
- if (this.$router) this.$router.push(url);
-
- commit(types.SET_CURRENT_BRANCH, branchName);
-});
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
deleted file mode 100644
index 670af2fb89e..00000000000
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import { normalizeHeaders } from '../../../lib/utils/common_utils';
-import flash from '../../../flash';
-import service from '../../services';
-import * as types from '../mutation_types';
-import router from '../../ide_router';
-import {
- findEntry,
- setPageTitle,
- createTemp,
- findIndexOfFile,
-} from '../utils';
-
-export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => {
- if ((file.changed || file.tempFile) && !force) return;
-
- const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
- const fileWasActive = file.active;
-
- commit(types.TOGGLE_FILE_OPEN, file);
- commit(types.SET_FILE_ACTIVE, { file, active: false });
-
- if (state.openFiles.length > 0 && fileWasActive) {
- const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
- const nextFileToOpen = state.openFiles[nextIndexToOpen];
-
- dispatch('setFileActive', nextFileToOpen);
- } else if (!state.openFiles.length) {
- router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
- }
-
- dispatch('getLastCommitData');
-};
-
-export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
- const currentActiveFile = getters.activeFile;
-
- if (file.active) return;
-
- if (currentActiveFile) {
- commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
- }
-
- commit(types.SET_FILE_ACTIVE, { file, active: true });
- dispatch('scrollToTab');
-
- // reset hash for line highlighting
- location.hash = '';
-
- commit(types.SET_CURRENT_PROJECT, file.projectId);
- commit(types.SET_CURRENT_BRANCH, file.branchId);
-};
-
-export const getFileData = ({ state, commit, dispatch }, file) => {
- commit(types.TOGGLE_LOADING, file);
-
- service.getFileData(file.url)
- .then((res) => {
- const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
- setPageTitle(pageTitle);
-
- return res.json();
- })
- .then((data) => {
- commit(types.SET_FILE_DATA, { data, file });
- commit(types.TOGGLE_FILE_OPEN, file);
- dispatch('setFileActive', file);
- commit(types.TOGGLE_LOADING, file);
- })
- .catch(() => {
- commit(types.TOGGLE_LOADING, file);
- flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
- });
-};
-
-export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file)
- .then((raw) => {
- commit(types.SET_FILE_RAW_DATA, { file, raw });
- })
- .catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
-
-export const changeFileContent = ({ commit }, { file, content }) => {
- commit(types.UPDATE_FILE_CONTENT, { file, content });
-};
-
-export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
- if (state.selectedFile) {
- commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
- }
-};
-
-export const setFileEOL = ({ state, commit }, { eol }) => {
- if (state.selectedFile) {
- commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
- }
-};
-
-export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
- if (state.selectedFile) {
- commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
- }
-};
-
-export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
- const path = parent.path !== undefined ? parent.path : '';
- // We need to do the replacement otherwise the web_url + file.url duplicate
- const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
- const file = createTemp({
- projectId,
- branchId,
- name: name.replace(`${path}/`, ''),
- path,
- type: 'blob',
- level: parent.level !== undefined ? parent.level + 1 : 0,
- changed: true,
- content,
- base64,
- url: newUrl,
- });
-
- if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
-
- commit(types.CREATE_TMP_FILE, {
- parent,
- file,
- });
- commit(types.TOGGLE_FILE_OPEN, file);
- dispatch('setFileActive', file);
-
- if (!state.editMode && !file.base64) {
- dispatch('toggleEditMode', true);
- }
-
- router.push(`/project${file.url}`);
-
- return Promise.resolve(file);
-};
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
deleted file mode 100644
index faeceb430a2..00000000000
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import service from '../../services';
-import flash from '../../../flash';
-import * as types from '../mutation_types';
-
-// eslint-disable-next-line import/prefer-default-export
-export const getProjectData = (
- { commit, state, dispatch },
- { namespace, projectId, force = false } = {},
-) => new Promise((resolve, reject) => {
- if (!state.projects[`${namespace}/${projectId}`] || force) {
- commit(types.TOGGLE_LOADING, state);
- service.getProjectData(namespace, projectId)
- .then(res => res.data)
- .then((data) => {
- commit(types.TOGGLE_LOADING, state);
- commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
- if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
- resolve(data);
- })
- .catch(() => {
- flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
- reject(new Error(`Project not loaded ${namespace}/${projectId}`));
- });
- } else {
- resolve(state.projects[`${namespace}/${projectId}`]);
- }
-});
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
deleted file mode 100644
index 302ba45edee..00000000000
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ /dev/null
@@ -1,188 +0,0 @@
-import { visitUrl } from '../../../lib/utils/url_utility';
-import { normalizeHeaders } from '../../../lib/utils/common_utils';
-import flash from '../../../flash';
-import service from '../../services';
-import * as types from '../mutation_types';
-import router from '../../ide_router';
-import {
- setPageTitle,
- findEntry,
- createTemp,
- createOrMergeEntry,
-} from '../utils';
-
-export const getTreeData = (
- { commit, state, dispatch },
- { endpoint, tree = null, projectId, branch, force = false } = {},
-) => new Promise((resolve, reject) => {
- // We already have the base tree so we resolve immediately
- if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
- resolve();
- } else {
- if (tree) commit(types.TOGGLE_LOADING, tree);
- const selectedProject = state.projects[projectId];
- // We are merging the web_url that we got on the project info with the endpoint
- // we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
- const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
- if (completeEndpoint && (!tree || !tree.tempFile)) {
- service.getTreeData(completeEndpoint)
- .then((res) => {
- const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
- setPageTitle(pageTitle);
-
- return res.json();
- })
- .then((data) => {
- if (!state.isInitialRoot) {
- commit(types.SET_ROOT, data.path === '/');
- }
-
- dispatch('updateDirectoryData', { data, tree, projectId, branch });
- const selectedTree = tree || state.trees[`${projectId}/${branch}`];
-
- commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
- commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
- if (tree) commit(types.TOGGLE_LOADING, selectedTree);
-
- const prevLastCommitPath = selectedTree.lastCommitPath;
- if (prevLastCommitPath !== null) {
- dispatch('getLastCommitData', selectedTree);
- }
- resolve(data);
- })
- .catch((e) => {
- flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
- if (tree) commit(types.TOGGLE_LOADING, tree);
- reject(e);
- });
- } else {
- resolve();
- }
- }
-});
-
-export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
- if (tree.opened) {
- // send empty data to clear the tree
- const data = { trees: [], blobs: [], submodules: [] };
-
- dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId });
- } else {
- dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId });
- }
-
- commit(types.TOGGLE_TREE_OPEN, tree);
-};
-
-export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
- if (row.type === 'tree') {
- dispatch('toggleTreeOpen', {
- endpoint: row.url,
- tree: row,
- });
- } else if (row.type === 'submodule') {
- commit(types.TOGGLE_LOADING, row);
- visitUrl(row.url);
- } else if (row.type === 'blob' && row.opened) {
- dispatch('setFileActive', row);
- } else {
- dispatch('getFileData', row);
- }
-};
-
-export const createTempTree = (
- { state, commit, dispatch },
- { projectId, branchId, parent, name },
-) => {
- let selectedTree = parent;
- const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
-
- dirNames.forEach((dirName) => {
- const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
-
- if (!foundEntry) {
- const path = selectedTree.path !== undefined ? selectedTree.path : '';
- const tmpEntry = createTemp({
- projectId,
- branchId,
- name: dirName,
- path,
- type: 'tree',
- level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
- tree: [],
- url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
- });
-
- commit(types.CREATE_TMP_TREE, {
- parent: selectedTree,
- tmpEntry,
- });
- commit(types.TOGGLE_TREE_OPEN, tmpEntry);
-
- router.push(`/project${tmpEntry.url}`);
-
- selectedTree = tmpEntry;
- } else {
- selectedTree = foundEntry;
- }
- });
-};
-
-export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
- if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
-
- service.getTreeLastCommit(tree.lastCommitPath)
- .then((res) => {
- const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
-
- commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
-
- return res.json();
- })
- .then((data) => {
- data.forEach((lastCommit) => {
- const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
-
- if (entry) {
- commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
- }
- });
-
- dispatch('getLastCommitData', tree);
- })
- .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
-};
-
-export const updateDirectoryData = (
- { commit, state },
- { data, tree, projectId, branch },
-) => {
- if (!tree) {
- const existingTree = state.trees[`${projectId}/${branch}`];
- if (!existingTree) {
- commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
- }
- }
-
- const selectedTree = tree || state.trees[`${projectId}/${branch}`];
- const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
- const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
- const createEntry = (entry, type) => createOrMergeEntry({
- tree: selectedTree,
- projectId: `${projectId}`,
- branchId: branch,
- entry,
- level,
- type,
- parentTreeUrl,
- });
-
- const formattedData = [
- ...data.trees.map(t => createEntry(t, 'tree')),
- ...data.submodules.map(m => createEntry(m, 'submodule')),
- ...data.blobs.map(b => createEntry(b, 'blob')),
- ];
-
- commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
-};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
deleted file mode 100644
index 6b51ccff817..00000000000
--- a/app/assets/javascripts/ide/stores/getters.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export const changedFiles = state => state.openFiles.filter(file => file.changed);
-
-export const activeFile = state => state.openFiles.find(file => file.active) || null;
-
-export const activeFileExtension = (state) => {
- const file = activeFile(state);
- return file ? `.${file.path.split('.').pop()}` : '';
-};
-
-export const canEditFile = (state) => {
- const currentActiveFile = activeFile(state);
-
- return state.canCommit &&
- (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
-};
-
-export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
-
-export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
deleted file mode 100644
index 6ac9bfd8189..00000000000
--- a/app/assets/javascripts/ide/stores/index.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import state from './state';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-
-Vue.use(Vuex);
-
-export default new Vuex.Store({
- state: state(),
- actions,
- mutations,
- getters,
-});
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
deleted file mode 100644
index 69b218a5e7d..00000000000
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ /dev/null
@@ -1,46 +0,0 @@
-export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
-export const TOGGLE_LOADING = 'TOGGLE_LOADING';
-export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
-export const SET_ROOT = 'SET_ROOT';
-export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
-export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
-export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
-export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
-
-// Project Mutation Types
-export const SET_PROJECT = 'SET_PROJECT';
-export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
-export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
-
-// Branch Mutation Types
-export const SET_BRANCH = 'SET_BRANCH';
-export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
-export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
-
-// Tree mutation types
-export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
-export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
-export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
-export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
-export const CREATE_TREE = 'CREATE_TREE';
-
-// File mutation types
-export const SET_FILE_DATA = 'SET_FILE_DATA';
-export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
-export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
-export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
-export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
-export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
-export const SET_FILE_POSITION = 'SET_FILE_POSITION';
-export const SET_FILE_EOL = 'SET_FILE_EOL';
-export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
-export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
-
-// Viewer mutation types
-export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
-export const SET_EDIT_MODE = 'SET_EDIT_MODE';
-export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
-export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
-
-export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
-
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
deleted file mode 100644
index 03d81be10a1..00000000000
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import * as types from './mutation_types';
-import projectMutations from './mutations/project';
-import fileMutations from './mutations/file';
-import treeMutations from './mutations/tree';
-import branchMutations from './mutations/branch';
-
-export default {
- [types.SET_INITIAL_DATA](state, data) {
- Object.assign(state, data);
- },
- [types.SET_PREVIEW_MODE](state) {
- Object.assign(state, {
- currentBlobView: 'repo-preview',
- });
- },
- [types.SET_EDIT_MODE](state) {
- Object.assign(state, {
- currentBlobView: 'repo-editor',
- });
- },
- [types.TOGGLE_LOADING](state, entry) {
- Object.assign(entry, {
- loading: !entry.loading,
- });
- },
- [types.TOGGLE_EDIT_MODE](state) {
- Object.assign(state, {
- editMode: !state.editMode,
- });
- },
- [types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) {
- Object.assign(state, {
- discardPopupOpen,
- });
- },
- [types.SET_ROOT](state, isRoot) {
- Object.assign(state, {
- isRoot,
- isInitialRoot: isRoot,
- });
- },
- [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
- Object.assign(state, {
- leftPanelCollapsed: collapsed,
- });
- },
- [types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
- Object.assign(state, {
- rightPanelCollapsed: collapsed,
- });
- },
- [types.SET_RESIZING_STATUS](state, resizing) {
- Object.assign(state, {
- panelResizing: resizing,
- });
- },
- [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
- Object.assign(entry.lastCommit, {
- id: lastCommit.commit.id,
- url: lastCommit.commit_path,
- message: lastCommit.commit.message,
- author: lastCommit.commit.author_name,
- updatedAt: lastCommit.commit.authored_date,
- });
- },
- ...projectMutations,
- ...fileMutations,
- ...treeMutations,
- ...branchMutations,
-};
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
deleted file mode 100644
index 04b9582c5bb..00000000000
--- a/app/assets/javascripts/ide/stores/mutations/branch.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as types from '../mutation_types';
-
-export default {
- [types.SET_CURRENT_BRANCH](state, currentBranchId) {
- Object.assign(state, {
- currentBranchId,
- });
- },
- [types.SET_BRANCH](state, { projectPath, branchName, branch }) {
- // Add client side properties
- Object.assign(branch, {
- treeId: `${projectPath}/${branchName}`,
- active: true,
- workingReference: '',
- });
-
- Object.assign(state.projects[projectPath], {
- branches: {
- [branchName]: branch,
- },
- });
- },
- [types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
- Object.assign(state.projects[projectId].branches[branchId], {
- workingReference: reference,
- });
- },
-};
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
deleted file mode 100644
index 72db1c180c9..00000000000
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import * as types from '../mutation_types';
-import { findIndexOfFile } from '../utils';
-
-export default {
- [types.SET_FILE_ACTIVE](state, { file, active }) {
- Object.assign(file, {
- active,
- });
-
- Object.assign(state, {
- selectedFile: file,
- });
- },
- [types.TOGGLE_FILE_OPEN](state, file) {
- Object.assign(file, {
- opened: !file.opened,
- });
-
- if (file.opened) {
- state.openFiles.push(file);
- } else {
- state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
- }
- },
- [types.SET_FILE_DATA](state, { data, file }) {
- Object.assign(file, {
- blamePath: data.blame_path,
- commitsPath: data.commits_path,
- permalink: data.permalink,
- rawPath: data.raw_path,
- binary: data.binary,
- html: data.html,
- renderError: data.render_error,
- });
- },
- [types.SET_FILE_RAW_DATA](state, { file, raw }) {
- Object.assign(file, {
- raw,
- });
- },
- [types.UPDATE_FILE_CONTENT](state, { file, content }) {
- const changed = content !== file.raw;
-
- Object.assign(file, {
- content,
- changed,
- });
- },
- [types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
- Object.assign(file, {
- fileLanguage,
- });
- },
- [types.SET_FILE_EOL](state, { file, eol }) {
- Object.assign(file, {
- eol,
- });
- },
- [types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
- Object.assign(file, {
- editorRow,
- editorColumn,
- });
- },
- [types.DISCARD_FILE_CHANGES](state, file) {
- Object.assign(file, {
- content: file.raw,
- changed: false,
- });
- },
- [types.CREATE_TMP_FILE](state, { file, parent }) {
- parent.tree.push(file);
- },
-};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
deleted file mode 100644
index 2816562a919..00000000000
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as types from '../mutation_types';
-
-export default {
- [types.SET_CURRENT_PROJECT](state, currentProjectId) {
- Object.assign(state, {
- currentProjectId,
- });
- },
- [types.SET_PROJECT](state, { projectPath, project }) {
- // Add client side properties
- Object.assign(project, {
- tree: [],
- branches: {},
- active: true,
- });
-
- Object.assign(state, {
- projects: Object.assign({}, state.projects, {
- [projectPath]: project,
- }),
- });
- },
-};
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
deleted file mode 100644
index 4fe438ab465..00000000000
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as types from '../mutation_types';
-
-export default {
- [types.TOGGLE_TREE_OPEN](state, tree) {
- Object.assign(tree, {
- opened: !tree.opened,
- });
- },
- [types.CREATE_TREE](state, { treePath }) {
- Object.assign(state, {
- trees: Object.assign({}, state.trees, {
- [treePath]: {
- tree: [],
- },
- }),
- });
- },
- [types.SET_DIRECTORY_DATA](state, { data, tree }) {
- Object.assign(tree, {
- tree: data,
- });
- },
- [types.SET_PARENT_TREE_URL](state, url) {
- Object.assign(state, {
- parentTreeUrl: url,
- });
- },
- [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
- Object.assign(tree, {
- lastCommitPath: url,
- });
- },
- [types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
- parent.tree.push(tmpEntry);
- },
-};
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
deleted file mode 100644
index 61d12096946..00000000000
--- a/app/assets/javascripts/ide/stores/state.js
+++ /dev/null
@@ -1,23 +0,0 @@
-export default () => ({
- canCommit: false,
- currentProjectId: '',
- currentBranchId: '',
- currentBlobView: 'repo-editor',
- discardPopupOpen: false,
- editMode: true,
- endpoints: {},
- isRoot: false,
- isInitialRoot: false,
- lastCommitPath: '',
- loading: false,
- onTopOfBranch: false,
- openFiles: [],
- selectedFile: null,
- path: '',
- parentTreeUrl: '',
- trees: {},
- projects: {},
- leftPanelCollapsed: false,
- rightPanelCollapsed: true,
- panelResizing: false,
-});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
deleted file mode 100644
index d556404faa5..00000000000
--- a/app/assets/javascripts/ide/stores/utils.js
+++ /dev/null
@@ -1,177 +0,0 @@
-import _ from 'underscore';
-
-export const dataStructure = () => ({
- id: '',
- key: '',
- type: '',
- projectId: '',
- branchId: '',
- name: '',
- url: '',
- path: '',
- level: 0,
- tempFile: false,
- icon: '',
- tree: [],
- loading: false,
- opened: false,
- active: false,
- changed: false,
- lastCommitPath: '',
- lastCommit: {
- id: '',
- url: '',
- message: '',
- updatedAt: '',
- author: '',
- },
- tree_url: '',
- blamePath: '',
- commitsPath: '',
- permalink: '',
- rawPath: '',
- binary: false,
- html: '',
- raw: '',
- content: '',
- parentTreeUrl: '',
- renderError: false,
- base64: false,
- editorRow: 1,
- editorColumn: 1,
- fileLanguage: '',
- eol: '',
-});
-
-export const decorateData = (entity) => {
- const {
- id,
- projectId,
- branchId,
- type,
- url,
- name,
- icon,
- tree_url,
- path,
- renderError,
- content = '',
- tempFile = false,
- active = false,
- opened = false,
- changed = false,
- parentTreeUrl = '',
- level = 0,
- base64 = false,
- } = entity;
-
- return {
- ...dataStructure(),
- id,
- projectId,
- branchId,
- key: `${name}-${type}-${id}`,
- type,
- name,
- url,
- tree_url,
- path,
- level,
- tempFile,
- icon: `fa-${icon}`,
- opened,
- active,
- parentTreeUrl,
- changed,
- renderError,
- content,
- base64,
- };
-};
-
-/*
- Takes the multi-dimensional tree and returns a flattened array.
- This allows for the table to recursively render the table rows but keeps the data
- structure nested to make it easier to add new files/directories.
-*/
-export const treeList = (state, treeId) => {
- const baseTree = state.trees[treeId];
- if (baseTree) {
- const mapTree = arr => (!arr.tree || !arr.tree.length ?
- [] : _.map(arr.tree, a => [a, mapTree(a)]));
-
- return _.chain(baseTree.tree)
- .map(arr => [arr, mapTree(arr)])
- .flatten()
- .value();
- }
- return [];
-};
-
-export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
-
-export const getTreeEntry = (store, treeId, path) => {
- const fileList = treeList(store.state, treeId);
- return fileList ? fileList.find(file => file.path === path) : null;
-};
-
-export const findEntry = (tree, type, name) => tree.find(
- f => f.type === type && f.name === name,
-);
-
-export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
-
-export const setPageTitle = (title) => {
- document.title = title;
-};
-
-export const createTemp = ({
- projectId, branchId, name, path, type, level, changed, content, base64, url,
-}) => {
- const treePath = path ? `${path}/${name}` : name;
-
- return decorateData({
- id: new Date().getTime().toString(),
- projectId,
- branchId,
- name,
- type,
- tempFile: true,
- path: treePath,
- icon: type === 'tree' ? 'folder' : 'file-text-o',
- changed,
- content,
- parentTreeUrl: '',
- level,
- base64,
- renderError: base64,
- url,
- });
-};
-
-export const createOrMergeEntry = ({ tree,
- projectId,
- branchId,
- entry,
- type,
- parentTreeUrl,
- level }) => {
- const found = findEntry(tree.tree || tree, type, entry.name);
-
- if (found) {
- return Object.assign({}, found, {
- id: entry.id,
- url: entry.url,
- tempFile: false,
- });
- }
-
- return decorateData({
- ...entry,
- projectId,
- branchId,
- type,
- parentTreeUrl,
- level,
- });
-};
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 35094f8e73b..523bd2adb93 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,11 +1,14 @@
-import { __ } from './locale';
+import _ from 'underscore';
+import { __, sprintf } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+import { convertPermissionToBoolean } from './lib/utils/common_utils';
class ImporterStatus {
- constructor(jobsUrl, importUrl) {
+ constructor({ jobsUrl, importUrl, ciCdOnly }) {
this.jobsUrl = jobsUrl;
this.importUrl = importUrl;
+ this.ciCdOnly = ciCdOnly;
this.initStatusPage();
this.setAutoUpdate();
}
@@ -45,6 +48,7 @@ class ImporterStatus {
repo_id: id,
target_namespace: targetNamespace,
new_name: newName,
+ ci_cd_only: this.ciCdOnly,
})
.then(({ data }) => {
const job = $(`tr#repo_${id}`);
@@ -54,7 +58,13 @@ class ImporterStatus {
$('table.import-jobs tbody').prepend(job);
job.addClass('active');
- job.find('.import-actions').html('<i class="fa fa-spinner fa-spin" aria-label="importing"></i> started');
+ const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
+ job.find('.import-actions').html(sprintf(
+ _.escape(__('%{loadingIcon} Started')), {
+ loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(connectingVerb)}"></i>`,
+ },
+ false,
+ ));
})
.catch(() => flash(__('An error occurred while importing project')));
}
@@ -71,13 +81,16 @@ class ImporterStatus {
switch (job.import_status) {
case 'finished':
jobItem.removeClass('active').addClass('success');
- statusField.html('<span><i class="fa fa-check"></i> done</span>');
+ statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`);
break;
case 'scheduled':
- statusField.html(`${spinner} scheduled`);
+ statusField.html(`${spinner} ${__('Scheduled')}`);
break;
case 'started':
- statusField.html(`${spinner} started`);
+ statusField.html(`${spinner} ${__('Started')}`);
+ break;
+ case 'failed':
+ statusField.html(__('Failed'));
break;
default:
statusField.html(job.import_status);
@@ -98,7 +111,11 @@ function initImporterStatus() {
if (importerStatus) {
const data = importerStatus.dataset;
- return new ImporterStatus(data.jobsImportPath, data.importPath);
+ return new ImporterStatus({
+ jobsUrl: data.jobsImportPath,
+ importUrl: data.importPath,
+ ciCdOnly: convertPermissionToBoolean(data.ciCdOnly),
+ });
}
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 7151ac05a09..9b46bbf83da 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -21,7 +21,7 @@ export default class LabelsSelect {
}
$els.each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
$dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
@@ -53,13 +53,6 @@ export default class LabelsSelect {
.map(function () {
return this.value;
}).get();
- if (issueUpdateURL != null) {
- issueURLSplit = issueUpdateURL.split('/');
- }
- if (issueUpdateURL) {
- labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
- labelNoneHTMLTemplate = '<span class="no-value">None</span>';
- }
const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip();
@@ -91,14 +84,17 @@ export default class LabelsSelect {
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
- data.issueURLSplit = issueURLSplit;
+ data.issueUpdateURL = issueUpdateURL;
labelCount = 0;
- if (data.labels.length) {
- template = labelHTMLTemplate(data);
+ if (data.labels.length && issueUpdateURL) {
+ template = LabelsSelect.getLabelTemplate({
+ labels: data.labels,
+ issueUpdateURL,
+ });
labelCount = data.labels.length;
}
else {
- template = labelNoneHTMLTemplate;
+ template = '<span class="no-value">None</span>';
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
@@ -242,10 +238,16 @@ export default class LabelsSelect {
filterable: true,
selected: $dropdown.data('selected') || [],
toggleLabel: function(selected, el) {
+ var $dropdownParent = $dropdown.parent();
+ var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false;
var title = selected.title;
var selectedLabels = this.selected;
+ if ($dropdownInputField.length && $dropdownInputField.val().length) {
+ $dropdownParent.find('.dropdown-input-clear').trigger('click');
+ }
+
if (selected.id === 0) {
this.selected = [];
return 'No Label';
@@ -412,6 +414,26 @@ export default class LabelsSelect {
this.bindEvents();
}
+ static getLabelTemplate(tplData) {
+ // We could use ES6 template string here
+ // and properly indent markup for readability
+ // but that also introduces unintended white-space
+ // so best approach is to use traditional way of
+ // concatenation
+ // see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
+ const tpl = _.template([
+ '<% _.each(labels, function(label){ %>',
+ '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
+ '<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
+ '<%- label.title %>',
+ '</span>',
+ '</a>',
+ '<% }); %>',
+ ].join(''));
+
+ return tpl(tplData);
+ }
+
bindEvents() {
return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 017f3b986fd..ed90db317df 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,3 +1,5 @@
+import jQuery from 'jquery';
+import Cookies from 'js-cookie';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
@@ -22,13 +24,18 @@ export const getGroupSlug = () => {
return null;
};
-export const isInIssuePage = () => {
- const page = getPagePath(1);
- const action = getPagePath(2);
+export const checkPageAndAction = (page, action) => {
+ const pagePath = getPagePath(1);
+ const actionPath = getPagePath(2);
- return page === 'issues' && action === 'show';
+ return pagePath === page && actionPath === action;
};
+export const isInIssuePage = () => checkPageAndAction('issues', 'show');
+export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
+export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
+export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
+
export const ajaxGet = url => axios.get(url, {
params: { format: 'js' },
responseType: 'text',
@@ -133,7 +140,11 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
-export const scrollToElement = ($el) => {
+export const scrollToElement = (element) => {
+ let $el = element;
+ if (!(element instanceof jQuery)) {
+ $el = $(element);
+ }
const top = $el.offset().top;
const mrTabsHeight = $('.merge-request-tabs').height() || 0;
const headerHeight = $('.navbar-gitlab').height() || 0;
@@ -291,6 +302,14 @@ export const parseQueryStringIntoObject = (query = '') => {
}, {});
};
+/**
+ * Converts object with key-value pairs
+ * into query-param string
+ *
+ * @param {Object} params
+ */
+export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&');
+
export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
/**
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 94d03621bff..c0ce0786518 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -65,6 +65,20 @@ export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
+export function camelCase(str) {
+ return str.replace(/_+([a-z])/gi, ($1, $2) => $2.toUpperCase());
+}
+
+export function camelCaseKeys(obj = {}) {
+ return Object.keys(obj).reduce((acc, key) => {
+ const camelKey = camelCase(key);
+ return {
+ ...acc,
+ [camelKey]: obj[key],
+ };
+ }, {});
+}
+
/**
* Replaces all html tags from a string with the given replacement.
*
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 659dc9eaa1f..53b01cca1d3 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -36,8 +36,11 @@ import initBreadcrumbs from './breadcrumb';
import initDispatcher from './dispatcher';
-// eslint-disable-next-line global-require, import/no-commonjs
-if (process.env.NODE_ENV !== 'production') require('./test_utils/');
+// inject test utilities if necessary
+if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
+ $.fx.off = true;
+ import(/* webpackMode: "eager" */ './test_utils/');
+}
svg4everybody();
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index b4b3c15108d..66b258839ae 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -12,7 +12,7 @@ import './components/inline_conflict_lines';
import './components/parallel_conflict_lines';
import syntaxHighlight from '../syntax_highlight';
-$(() => {
+export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts');
const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
@@ -91,4 +91,4 @@ $(() => {
}
}
});
-});
+}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 41971e92ec0..46789e324c2 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -241,6 +241,10 @@ export default class MergeRequestTabs {
return newState;
}
+ getCurrentAction() {
+ return this.currentAction;
+ }
+
loadCommits(source) {
if (this.commitsLoaded) {
return;
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 2841ecb558b..c259d5405bd 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -216,6 +216,9 @@ export default class MilestoneSelect {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
+ })
+ .catch(() => {
+ $loading.fadeOut();
});
}
}
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 031badc7026..8ca94ef3e2a 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -7,34 +7,82 @@
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
- import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
export default {
-
components: {
Graph,
GraphGroup,
EmptyState,
},
- data() {
- const metricsData = document.querySelector('#prometheus-graphs').dataset;
- const store = new MonitoringStore();
+ props: {
+ hasMetrics: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showLegend: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showPanels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ forceSmallGraph: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ documentationPath: {
+ type: String,
+ required: true,
+ },
+ settingsPath: {
+ type: String,
+ required: true,
+ },
+ clustersPath: {
+ type: String,
+ required: true,
+ },
+ tagsPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ metricsEndpoint: {
+ type: String,
+ required: true,
+ },
+ deploymentEndpoint: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ emptyGettingStartedSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyLoadingSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyUnableToConnectSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
return {
- store,
+ store: new MonitoringStore(),
state: 'gettingStarted',
- hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
- documentationPath: metricsData.documentationPath,
- settingsPath: metricsData.settingsPath,
- clustersPath: metricsData.clustersPath,
- tagsPath: metricsData.tagsPath,
- projectPath: metricsData.projectPath,
- metricsEndpoint: metricsData.additionalMetrics,
- deploymentEndpoint: metricsData.deploymentEndpoint,
- emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath,
- emptyLoadingSvgPath: metricsData.emptyLoadingSvgPath,
- emptyUnableToConnectSvgPath: metricsData.emptyUnableToConnectSvgPath,
showEmptyState: true,
updateAspectRatio: false,
updatedAspectRatios: 0,
@@ -67,6 +115,7 @@
window.addEventListener('resize', this.resizeThrottled, false);
}
},
+
methods: {
getGraphsData() {
this.state = 'loading';
@@ -115,6 +164,7 @@
v-for="(groupData, index) in store.groups"
:key="index"
:name="groupData.group"
+ :show-panels="showPanels"
>
<graph
v-for="(graphData, index) in groupData.metrics"
@@ -125,6 +175,8 @@
:deployment-data="store.deploymentData"
:project-path="projectPath"
:tags-path="tagsPath"
+ :show-legend="showLegend"
+ :small-graph="forceSmallGraph"
/>
</graph-group>
</div>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index ea5c24efaf9..9e67a6f2146 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -52,6 +52,16 @@
type: String,
required: true,
},
+ showLegend: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ smallGraph: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
@@ -130,7 +140,7 @@
const breakpointSize = bp.getBreakpointSize();
const query = this.graphData.queries[0];
this.margin = measurements.large.margin;
- if (breakpointSize === 'xs' || breakpointSize === 'sm') {
+ if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300;
this.margin = measurements.small.margin;
this.measurements = measurements.small;
@@ -182,7 +192,9 @@
this.graphHeightOffset,
);
- if (this.timeSeries.length > 3) {
+ if (!this.showLegend) {
+ this.baseGraphHeight -= 50;
+ } else if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
@@ -255,6 +267,7 @@
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
+ :show-legend-group="showLegend"
/>
<svg
class="graph-data"
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index c6e8d726ffc..3149397b61f 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -39,6 +39,11 @@
type: Number,
required: true,
},
+ showLegendGroup: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -57,8 +62,9 @@
},
rectTransform() {
- const yCoordinate = ((this.graphHeight - this.margin.top) / 2)
- + (this.yLabelWidth / 2) + 10 || 0;
+ const yCoordinate = (((this.graphHeight - this.margin.top)
+ + this.measurements.axisLabelLineOffset) / 2)
+ + (this.yLabelWidth / 2) || 0;
return `translate(0, ${yCoordinate}) rotate(-90)`;
},
@@ -166,39 +172,41 @@
>
Time
</text>
- <g
- class="legend-group"
- v-for="(series, index) in timeSeries"
- :key="index"
- :transform="translateLegendGroup(index)"
- >
- <line
- :stroke="series.lineColor"
- :stroke-width="measurements.legends.height"
- :stroke-dasharray="strokeDashArray(series.lineStyle)"
- :x1="measurements.legends.offsetX"
- :x2="measurements.legends.offsetX + measurements.legends.width"
- :y1="graphHeight - measurements.legends.offsetY"
- :y2="graphHeight - measurements.legends.offsetY"
- />
- <text
- v-if="timeSeries.length > 1"
- class="legend-metric-title"
- ref="legendTitleSvg"
- x="38"
- :y="graphHeight - 30"
- >
- {{ createSeriesString(index, series) }}
- </text>
- <text
- v-else
- class="legend-metric-title"
- ref="legendTitleSvg"
- x="38"
- :y="graphHeight - 30"
+ <template v-if="showLegendGroup">
+ <g
+ class="legend-group"
+ v-for="(series, index) in timeSeries"
+ :key="index"
+ :transform="translateLegendGroup(index)"
>
- {{ legendTitle }} {{ formatMetricUsage(series) }}
- </text>
- </g>
+ <line
+ :stroke="series.lineColor"
+ :stroke-width="measurements.legends.height"
+ :stroke-dasharray="strokeDashArray(series.lineStyle)"
+ :x1="measurements.legends.offsetX"
+ :x2="measurements.legends.offsetX + measurements.legends.width"
+ :y1="graphHeight - measurements.legends.offsetY"
+ :y2="graphHeight - measurements.legends.offsetY"
+ />
+ <text
+ v-if="timeSeries.length > 1"
+ class="legend-metric-title"
+ ref="legendTitleSvg"
+ x="38"
+ :y="graphHeight - 30"
+ >
+ {{ createSeriesString(index, series) }}
+ </text>
+ <text
+ v-else
+ class="legend-metric-title"
+ ref="legendTitleSvg"
+ x="38"
+ :y="graphHeight - 30"
+ >
+ {{ legendTitle }} {{ formatMetricUsage(series) }}
+ </text>
+ </g>
+ </template>
</g>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index 079351a69af..f71cf614552 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -5,12 +5,20 @@
type: String,
required: true,
},
+ showPanels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
<template>
- <div class="panel panel-default prometheus-panel">
+ <div
+ v-if="showPanels"
+ class="panel panel-default prometheus-panel"
+ >
<div class="panel-heading">
<h4>{{ name }}</h4>
</div>
@@ -18,4 +26,10 @@
<slot></slot>
</div>
</div>
+ <div
+ v-else
+ class="prometheus-graph-group"
+ >
+ <slot></slot>
+ </div>
</template>
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index c3b0ef7e9ca..41270e015d4 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,7 +1,22 @@
import Vue from 'vue';
+import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import Dashboard from './components/dashboard.vue';
-export default () => new Vue({
- el: '#prometheus-graphs',
- render: createElement => createElement(Dashboard),
-});
+export default () => {
+ const el = document.getElementById('prometheus-graphs');
+
+ if (el && el.dataset) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ render(createElement) {
+ return createElement(Dashboard, {
+ props: {
+ ...el.dataset,
+ hasMetrics: convertPermissionToBoolean(el.dataset.hasMetrics),
+ },
+ });
+ },
+ });
+ }
+};
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
index e230a06cd8c..6fcca36d2fa 100644
--- a/app/assets/javascripts/monitoring/services/monitoring_service.js
+++ b/app/assets/javascripts/monitoring/services/monitoring_service.js
@@ -40,6 +40,9 @@ export default class MonitoringService {
}
getDeploymentData() {
+ if (!this.deploymentEndpoint) {
+ return Promise.resolve([]);
+ }
return backOffRequest(() => axios.get(this.deploymentEndpoint))
.then(resp => resp.data)
.then((response) => {
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 4ce3dad440c..b5b8e3c255d 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -76,7 +76,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else {
- metricTag = timeSeriesMetricLabel || `series ${timeSeriesNumber + 1}`;
+ metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
[lineColor, areaColor] = pickColor();
}
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
new file mode 100644
index 00000000000..972fdb2b791
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import notesApp from '../notes/components/notes_app.vue';
+import discussionCounter from '../notes/components/discussion_counter.vue';
+import store from '../notes/stores';
+
+export default function initMrNotes() {
+ new Vue({ // eslint-disable-line
+ el: '#js-vue-mr-discussions',
+ components: {
+ notesApp,
+ },
+ data() {
+ const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
+ return {
+ noteableData: JSON.parse(notesDataset.noteableData),
+ currentUserData: JSON.parse(notesDataset.currentUserData),
+ notesData: JSON.parse(notesDataset.notesData),
+ };
+ },
+ render(createElement) {
+ return createElement('notes-app', {
+ props: {
+ noteableData: this.noteableData,
+ notesData: this.notesData,
+ userData: this.currentUserData,
+ },
+ });
+ },
+ });
+
+ new Vue({ // eslint-disable-line
+ el: '#js-vue-discussion-counter',
+ components: {
+ discussionCounter,
+ },
+ store,
+ render(createElement) {
+ return createElement('discussion-counter');
+ },
+ });
+}
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
deleted file mode 100644
index 129f1724cb8..00000000000
--- a/app/assets/javascripts/network/network_bundle.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
-
-import ShortcutsNetwork from '../shortcuts_network';
-import Network from './network';
-
-$(function() {
- if (!$(".network-graph").length) return;
-
- var network_graph;
- network_graph = new Network({
- url: $(".network-graph").attr('data-url'),
- commit_url: $(".network-graph").attr('data-commit-url'),
- ref: $(".network-graph").attr('data-ref'),
- commit_id: $(".network-graph").attr('data-commit-id')
- });
- return new ShortcutsNetwork(network_graph.branch_graph);
-});
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index f17b432cffd..c640003d958 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -24,7 +24,7 @@ import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import Autosave from './autosave';
import TaskList from './task_list';
-import { isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
+import { isInViewport, getPagePath, scrollToElement, isMetaKey, hasVueMRDiscussionsCookie } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
@@ -44,6 +44,10 @@ export default class Notes {
}
}
+ static getInstance() {
+ return this.instance;
+ }
+
constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this);
@@ -102,67 +106,77 @@ export default class Notes {
}
addBinding() {
+ this.$wrapperEl = hasVueMRDiscussionsCookie() ? $(document).find('.diffs') : $(document);
+
// Edit note link
- $(document).on('click', '.js-note-edit', this.showEditForm.bind(this));
- $(document).on('click', '.note-edit-cancel', this.cancelEdit);
+ this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
+ this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit);
// Reopen and close actions for Issue/MR combined with note form submit
- $(document).on('click', '.js-comment-submit-button', this.postComment);
- $(document).on('click', '.js-comment-save-button', this.updateComment);
- $(document).on('keyup input', '.js-note-text', this.updateTargetButtons);
+ this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment);
+ this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment);
+ this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons);
// resolve a discussion
- $(document).on('click', '.js-comment-resolve-button', this.postComment);
+ this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
- $(document).on('click', '.js-note-delete', this.removeNote);
+ this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
// delete note attachment
- $(document).on('click', '.js-note-attachment-delete', this.removeAttachment);
+ this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
// reset main target form when clicking discard
- $(document).on('click', '.js-note-discard', this.resetMainTargetForm);
+ this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
- $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment);
+ this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
+ this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
- $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
+ this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
- $(document).on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
+ this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form
- $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
+ this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
- $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
+ this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
// fetch notes when tab becomes visible
- $(document).on('visibilitychange', this.visibilityChange);
+ this.$wrapperEl.on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data
- $(document).on('issuable:change', this.refresh);
+ this.$wrapperEl.on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
- $(document).on('ajax:success', '.js-main-target-form', this.addNote);
- $(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
- $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
- $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
+ this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote);
+ this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
+ this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
+ this.$wrapperEl.on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
// when a key is clicked on the notes
- $(document).on('keydown', '.js-note-text', this.keydownNoteText);
+ this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText);
// When the URL fragment/hash has changed, `#note_xxx`
- return $(window).on('hashchange', this.onHashChange);
+ $(window).on('hashchange', this.onHashChange);
+ this.boundGetContent = this.getContent.bind(this);
+ document.addEventListener('refreshLegacyNotes', this.boundGetContent);
+ this.eventsBound = true;
}
cleanBinding() {
- $(document).off('click', '.js-note-edit');
- $(document).off('click', '.note-edit-cancel');
- $(document).off('click', '.js-note-delete');
- $(document).off('click', '.js-note-attachment-delete');
- $(document).off('click', '.js-discussion-reply-button');
- $(document).off('click', '.js-add-diff-note-button');
- $(document).off('click', '.js-add-image-diff-note-button');
- $(document).off('visibilitychange');
- $(document).off('keyup input', '.js-note-text');
- $(document).off('click', '.js-note-target-reopen');
- $(document).off('click', '.js-note-target-close');
- $(document).off('click', '.js-note-discard');
- $(document).off('keydown', '.js-note-text');
- $(document).off('click', '.js-comment-resolve-button');
- $(document).off('click', '.system-note-commit-list-toggler');
- $(document).off('ajax:success', '.js-main-target-form');
- $(document).off('ajax:success', '.js-discussion-note-form');
- $(document).off('ajax:complete', '.js-main-target-form');
+ if (!this.eventsBound) {
+ return;
+ }
+
+ this.$wrapperEl.off('click', '.js-note-edit');
+ this.$wrapperEl.off('click', '.note-edit-cancel');
+ this.$wrapperEl.off('click', '.js-note-delete');
+ this.$wrapperEl.off('click', '.js-note-attachment-delete');
+ this.$wrapperEl.off('click', '.js-discussion-reply-button');
+ this.$wrapperEl.off('click', '.js-add-diff-note-button');
+ this.$wrapperEl.off('click', '.js-add-image-diff-note-button');
+ this.$wrapperEl.off('visibilitychange');
+ this.$wrapperEl.off('keyup input', '.js-note-text');
+ this.$wrapperEl.off('click', '.js-note-target-reopen');
+ this.$wrapperEl.off('click', '.js-note-target-close');
+ this.$wrapperEl.off('click', '.js-note-discard');
+ this.$wrapperEl.off('keydown', '.js-note-text');
+ this.$wrapperEl.off('click', '.js-comment-resolve-button');
+ this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
+ this.$wrapperEl.off('ajax:success', '.js-main-target-form');
+ this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
+ this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
+ document.removeEventListener('refreshLegacyNotes', this.boundGetContent);
$(window).off('hashchange', this.onHashChange);
}
@@ -252,8 +266,10 @@ export default class Notes {
if (this.refreshing) {
return;
}
+
this.refreshing = true;
- axios.get(this.notes_url, {
+
+ axios.get(`${this.notes_url}?html=true`, {
headers: {
'X-Last-Fetched-At': this.last_fetched_at,
},
@@ -350,7 +366,7 @@ export default class Notes {
}
if (!noteEntity.valid) {
- if (noteEntity.errors.commands_only) {
+ if (noteEntity.errors && noteEntity.errors.commands_only) {
if (noteEntity.commands_changes &&
Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
@@ -363,6 +379,10 @@ export default class Notes {
const $note = $notesList.find(`#note_${noteEntity.id}`);
if (Notes.isNewNote(noteEntity, this.note_ids)) {
+ if (hasVueMRDiscussionsCookie()) {
+ return;
+ }
+
this.note_ids.push(noteEntity.id);
if ($notesList.length) {
@@ -399,6 +419,8 @@ export default class Notes {
this.setupNewNote($updatedNote);
}
}
+
+ Notes.refreshVueNotes();
}
isParallelView() {
@@ -406,12 +428,11 @@ export default class Notes {
}
/**
- * Render note in discussion area.
- *
- * Note: for rendering inline notes use renderDiscussionNote
+ * Render note in discussion area. To render inline notes use renderDiscussionNote.
*/
renderDiscussionNote(noteEntity, $form) {
var discussionContainer, form, row, lineType, diffAvatarContainer;
+
if (!Notes.isNewNote(noteEntity, this.note_ids)) {
return;
}
@@ -452,7 +473,9 @@ export default class Notes {
// Init discussion on 'Discussion' page if it is merge request page
const page = $('body').attr('data-page');
if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) {
- Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
+ if (!hasVueMRDiscussionsCookie()) {
+ Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
+ }
}
} else {
// append new note to all matching discussions
@@ -634,7 +657,6 @@ export default class Notes {
var $noteEntityEl, $note_li;
// Convert returned HTML to a jQuery object so we can modify it further
$noteEntityEl = $(noteEntity.html);
- $noteEntityEl.addClass('fade-in-full');
this.revertNoteEditForm($targetNote);
$noteEntityEl.renderGFM();
// Find the note's `li` element by ID and replace it with the updated HTML
@@ -730,7 +752,7 @@ export default class Notes {
var selector = this.getEditFormSelector($target);
var $editForm = $(selector);
- $editForm.insertBefore('.notes-form');
+ $editForm.insertBefore('.diffs');
$editForm.find('.js-comment-save-button').enable();
$editForm.find('.js-finish-edit-warning').hide();
}
@@ -746,7 +768,8 @@ export default class Notes {
}
removeNoteEditForm($note) {
- var form = $note.find('.current-note-edit-form');
+ var form = $note.find('.diffs .current-note-edit-form');
+
$note.removeClass('is-editing');
form.removeClass('current-note-edit-form');
form.find('.js-finish-edit-warning').hide();
@@ -818,6 +841,7 @@ export default class Notes {
};
})(this));
+ Notes.refreshVueNotes();
Notes.checkMergeRequestStatus();
return this.updateNotesCount(-1);
}
@@ -1157,7 +1181,7 @@ export default class Notes {
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm.find('form')
- .attr('action', postUrl)
+ .attr('action', `${postUrl}?html=true`)
.attr('data-remote', 'true');
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
@@ -1280,6 +1304,10 @@ export default class Notes {
return $updatedNote;
}
+ static refreshVueNotes() {
+ document.dispatchEvent(new CustomEvent('refreshVueNotes'));
+ }
+
/**
* Get data from Form attributes to use for saving/submitting comment.
*/
@@ -1481,7 +1509,7 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
- axios.post(formAction, formData)
+ axios.post(`${formAction}?html=true`, formData)
.then((res) => {
const note = res.data;
@@ -1546,6 +1574,8 @@ export default class Notes {
if ($notesContainer.length) {
$notesContainer.append('<div class="flash-container" style="display: none;"></div>');
}
+
+ Notes.refreshVueNotes();
} else if (isMainForm) { // Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
this.addNote($form, note);
@@ -1627,7 +1657,7 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
- axios.post(formAction, formData)
+ axios.post(`${formAction}?html=true`, formData)
.then(({ data }) => {
// Submission successful! render final note element
this.updateNote(data, $editingNote);
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index df796050e0d..b85c1a6ad72 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -2,10 +2,11 @@
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
- import { __ } from '~/locale';
+ import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
+ import { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
@@ -29,6 +30,12 @@
mixins: [
issuableStateMixin,
],
+ props: {
+ noteableType: {
+ type: String,
+ required: true,
+ },
+ },
data() {
return {
note: '',
@@ -43,37 +50,51 @@
'getUserData',
'getNoteableData',
'getNotesData',
- 'issueState',
+ 'openState',
]),
+ noteableDisplayName() {
+ return this.noteableType.replace(/_/g, ' ');
+ },
isLoggedIn() {
return this.getUserData.id;
},
commentButtonTitle() {
return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
},
- isIssueOpen() {
- return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
+ isOpen() {
+ return this.openState === constants.OPENED || this.openState === constants.REOPENED;
},
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
},
issueActionButtonTitle() {
- if (this.note.length) {
- const actionText = this.isIssueOpen ? 'close' : 'reopen';
+ const openOrClose = this.isOpen ? 'close' : 'reopen';
- return this.noteType === constants.COMMENT ?
- `Comment & ${actionText} issue` :
- `Start discussion & ${actionText} issue`;
+ if (this.note.length) {
+ return sprintf(
+ __('%{actionText} & %{openOrClose} %{noteable}'),
+ {
+ actionText: this.commentButtonTitle,
+ openOrClose,
+ noteable: this.noteableDisplayName,
+ },
+ );
}
- return this.isIssueOpen ? 'Close issue' : 'Reopen issue';
+ return sprintf(
+ __('%{openOrClose} %{noteable}'),
+ {
+ openOrClose: capitalizeFirstCharacter(openOrClose),
+ noteable: this.noteableDisplayName,
+ },
+ );
},
actionButtonClassNames() {
return {
- 'btn-reopen': !this.isIssueOpen,
- 'btn-close': this.isIssueOpen,
- 'js-note-target-close': this.isIssueOpen,
- 'js-note-target-reopen': !this.isIssueOpen,
+ 'btn-reopen': !this.isOpen,
+ 'btn-close': this.isOpen,
+ 'js-note-target-close': this.isOpen,
+ 'js-note-target-reopen': !this.isOpen,
};
},
markdownDocsPath() {
@@ -138,7 +159,7 @@
flashContainer: this.$el,
data: {
note: {
- noteable_type: constants.NOTEABLE_TYPE,
+ noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
note: this.note,
},
@@ -193,19 +214,29 @@ Please check your network connection and try again.`;
this.isSubmitting = false;
},
toggleIssueState() {
- if (this.isIssueOpen) {
+ if (this.isOpen) {
this.closeIssue()
.then(() => this.enableButton())
.catch(() => {
this.enableButton();
- Flash(__('Something went wrong while closing the issue. Please try again later'));
+ Flash(
+ sprintf(
+ __('Something went wrong while closing the %{issuable}. Please try again later'),
+ { issuable: this.noteableDisplayName },
+ ),
+ );
});
} else {
this.reopenIssue()
.then(() => this.enableButton())
.catch(() => {
this.enableButton();
- Flash(__('Something went wrong while reopening the issue. Please try again later'));
+ Flash(
+ sprintf(
+ __('Something went wrong while reopening the %{issuable}. Please try again later'),
+ { issuable: this.noteableDisplayName },
+ ),
+ );
});
}
},
@@ -221,7 +252,6 @@ Please check your network connection and try again.`;
this.$refs.markdownField.previewMarkdown = false;
}
- // reset autostave
this.autosave.reset();
},
setNoteType(type) {
@@ -240,10 +270,11 @@ Please check your network connection and try again.`;
},
initAutoSave() {
if (this.isLoggedIn) {
+ const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
+
this.autosave = new Autosave(
$(this.$refs.textarea),
- ['Note', 'Issue', this.getNoteableData.id],
- 'issue',
+ ['Note', noteableType, this.getNoteableData.id],
);
}
},
@@ -331,7 +362,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled="isSubmitButtonDisabled"
class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
type="submit">
- {{ commentButtonTitle }}
+ {{ __(commentButtonTitle) }}
</button>
<button
:disabled="isSubmitButtonDisabled"
@@ -359,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<div class="description">
<strong>Comment</strong>
<p>
- Add a general comment to this issue.
+ Add a general comment to this {{ noteableDisplayName }}.
</p>
</div>
</button>
diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue
new file mode 100644
index 00000000000..3bcde17f07c
--- /dev/null
+++ b/app/assets/javascripts/notes/components/diff_file_header.vue
@@ -0,0 +1,94 @@
+<script>
+ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+ import Icon from '~/vue_shared/components/icon.vue';
+
+ export default {
+ components: {
+ ClipboardButton,
+ Icon,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ titleTag() {
+ return this.diffFile.discussionPath ? 'a' : 'span';
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="file-header-content">
+ <div
+ v-if="diffFile.submodule"
+ >
+ <span>
+ <icon name="archive" />
+ <strong
+ v-html="diffFile.submoduleLink"
+ class="file-title-name"
+ ></strong>
+ <clipboard-button
+ title="Copy file path to clipboard"
+ :text="diffFile.submoduleLink"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+ </span>
+ </div>
+ <template v-else>
+ <component
+ ref="titleWrapper"
+ :is="titleTag"
+ :href="diffFile.discussionPath"
+ >
+ <span v-html="diffFile.blobIcon"></span>
+ <span v-if="diffFile.renamedFile">
+ <strong
+ class="file-title-name has-tooltip"
+ :title="diffFile.oldPath"
+ data-container="body"
+ >
+ {{ diffFile.oldPath }}
+ </strong>
+ &rarr;
+ <strong
+ class="file-title-name has-tooltip"
+ :title="diffFile.newPath"
+ data-container="body"
+ >
+ {{ diffFile.newPath }}
+ </strong>
+ </span>
+
+ <strong
+ v-else
+ class="file-title-name has-tooltip"
+ :title="diffFile.oldPath"
+ data-container="body"
+ >
+ {{ diffFile.filePath }}
+ <span v-if="diffFile.deletedFile">
+ deleted
+ </span>
+ </strong>
+ </component>
+
+ <clipboard-button
+ title="Copy file path to clipboard"
+ :text="diffFile.filePath"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+
+ <small
+ v-if="diffFile.modeChanged"
+ ref="fileMode"
+ >
+ {{ diffFile.aMode }} → {{ diffFile.bMode }}
+ </small>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
new file mode 100644
index 00000000000..75a32709ad5
--- /dev/null
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -0,0 +1,96 @@
+<script>
+ import syntaxHighlight from '~/syntax_highlight';
+ import imageDiffHelper from '~/image_diff/helpers/index';
+ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+ import DiffFileHeader from './diff_file_header.vue';
+
+ export default {
+ components: {
+ DiffFileHeader,
+ },
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ isImageDiff() {
+ return !this.diffFile.text;
+ },
+ diffFileClass() {
+ const { text } = this.diffFile;
+ return text ? 'text-file' : 'js-image-file';
+ },
+ diffRows() {
+ return $(this.discussion.truncatedDiffLines);
+ },
+ diffFile() {
+ return convertObjectPropsToCamelCase(this.discussion.diffFile);
+ },
+ imageDiffHtml() {
+ return this.discussion.imageDiffHtml;
+ },
+ },
+ mounted() {
+ if (this.isImageDiff) {
+ const canCreateNote = false;
+ const renderCommentBadge = true;
+ imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
+ } else {
+ const fileHolder = $(this.$refs.fileHolder);
+ this.$nextTick(() => {
+ syntaxHighlight(fileHolder);
+ });
+ }
+ },
+ methods: {
+ rowTag(html) {
+ return html.outerHTML ? 'tr' : 'template';
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ ref="fileHolder"
+ class="diff-file file-holder"
+ :class="diffFileClass"
+ >
+ <div class="js-file-title file-title file-title-flex-parent">
+ <diff-file-header
+ :diff-file="diffFile"
+ />
+ </div>
+ <div
+ v-if="diffFile.text"
+ class="diff-content code js-syntax-highlight"
+ >
+ <table>
+ <component
+ :is="rowTag(html)"
+ :class="html.className"
+ v-for="(html, index) in diffRows"
+ v-html="html.outerHTML"
+ :key="index"
+ />
+ <tr class="notes_holder">
+ <td
+ class="notes_line"
+ colspan="2"
+ ></td>
+ <td class="notes_content">
+ <slot></slot>
+ </td>
+ </tr>
+ </table>
+ </div>
+ <div
+ v-else
+ >
+ <div v-html="imageDiffHtml"></div>
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
new file mode 100644
index 00000000000..0158f58b569
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -0,0 +1,119 @@
+<script>
+ import { mapGetters } from 'vuex';
+ import resolveSvg from 'icons/_icon_resolve_discussion.svg';
+ import resolvedSvg from 'icons/_icon_status_success_solid.svg';
+ import mrIssueSvg from 'icons/_icon_mr_issue.svg';
+ import nextDiscussionSvg from 'icons/_next_discussion.svg';
+ import { pluralize } from '../../lib/utils/text_utility';
+ import { scrollToElement } from '../../lib/utils/common_utils';
+ import tooltip from '../../vue_shared/directives/tooltip';
+
+ export default {
+ directives: {
+ tooltip,
+ },
+ computed: {
+ ...mapGetters([
+ 'getUserData',
+ 'getNoteableData',
+ 'discussionCount',
+ 'unresolvedDiscussions',
+ 'resolvedDiscussionCount',
+ ]),
+ isLoggedIn() {
+ return this.getUserData.id;
+ },
+ hasNextButton() {
+ return this.isLoggedIn && !this.allResolved;
+ },
+ countText() {
+ return pluralize('discussion', this.discussionCount);
+ },
+ allResolved() {
+ return this.resolvedDiscussionCount === this.discussionCount;
+ },
+ resolveAllDiscussionsIssuePath() {
+ return this.getNoteableData.create_issue_to_resolve_discussions_path;
+ },
+ firstUnresolvedDiscussionId() {
+ const item = this.unresolvedDiscussions[0] || {};
+
+ return item.id;
+ },
+ },
+ created() {
+ this.resolveSvg = resolveSvg;
+ this.resolvedSvg = resolvedSvg;
+ this.mrIssueSvg = mrIssueSvg;
+ this.nextDiscussionSvg = nextDiscussionSvg;
+ },
+ methods: {
+ jumpToFirstDiscussion() {
+ const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`);
+ const activeTab = window.mrTabs.currentAction;
+
+ if (activeTab === 'commits' || activeTab === 'pipelines') {
+ window.mrTabs.activateTab('show');
+ }
+
+ if (el) {
+ scrollToElement(el);
+ }
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="line-resolve-all-container prepend-top-10">
+ <div>
+ <div
+ v-if="discussionCount > 0"
+ :class="{ 'has-next-btn': hasNextButton }"
+ class="line-resolve-all">
+ <span
+ :class="{ 'is-active': allResolved }"
+ class="line-resolve-btn is-disabled"
+ type="button">
+ <span
+ v-if="allResolved"
+ v-html="resolvedSvg"
+ ></span>
+ <span
+ v-else
+ v-html="resolveSvg"
+ ></span>
+ </span>
+ <span class=".line-resolve-text">
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
+ </span>
+ </div>
+ <div
+ v-if="resolveAllDiscussionsIssuePath && !allResolved"
+ class="btn-group"
+ role="group">
+ <a
+ :href="resolveAllDiscussionsIssuePath"
+ v-tooltip
+ title="Resolve all discussions in new issue"
+ data-container="body"
+ class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
+ <span v-html="mrIssueSvg"></span>
+ </a>
+ </div>
+ <div
+ v-if="isLoggedIn && !allResolved"
+ class="btn-group"
+ role="group">
+ <button
+ @click="jumpToFirstDiscussion"
+ v-tooltip
+ title="Jump to first unresolved discussion"
+ data-container="body"
+ class="btn btn-default discussion-next-btn">
+ <span v-html="nextDiscussionSvg"></span>
+ </button>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 46ffb60aa60..c26aa6fa15d 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -4,6 +4,8 @@
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg';
+ import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
+ import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
@@ -42,6 +44,26 @@
type: Boolean,
required: true,
},
+ resolvable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isResolved: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isResolving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ resolvedBy: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
canReportAsAbuse: {
type: Boolean,
required: true,
@@ -63,6 +85,15 @@
currentUserId() {
return this.getUserDataByProp('id');
},
+ resolveButtonTitle() {
+ let title = 'Mark as resolved';
+
+ if (this.resolvedBy) {
+ title = `Resolved by ${this.resolvedBy.name}`;
+ }
+
+ return title;
+ },
},
created() {
this.emojiSmiling = emojiSmiling;
@@ -70,6 +101,8 @@
this.emojiSmiley = emojiSmiley;
this.editSvg = editSvg;
this.ellipsisSvg = ellipsisSvg;
+ this.resolveDiscussionSvg = resolveDiscussionSvg;
+ this.resolvedDiscussionSvg = resolvedDiscussionSvg;
},
methods: {
onEdit() {
@@ -78,6 +111,9 @@
onDelete() {
this.$emit('handleDelete');
},
+ onResolve() {
+ this.$emit('handleResolve');
+ },
},
};
</script>
@@ -90,6 +126,31 @@
{{ accessLevel }}
</span>
<div
+ v-if="resolvable"
+ class="note-actions-item">
+ <button
+ v-tooltip
+ @click="onResolve"
+ :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
+ :title="resolveButtonTitle"
+ :aria-label="resolveButtonTitle"
+ type="button"
+ class="line-resolve-btn note-action-button">
+ <template v-if="!isResolving">
+ <div
+ v-if="isResolved"
+ v-html="resolvedDiscussionSvg"></div>
+ <div
+ v-else
+ v-html="resolveDiscussionSvg"></div>
+ </template>
+ <loading-icon
+ v-else
+ :inline="true"
+ />
+ </button>
+ </div>
+ <div
v-if="canAddAwardEmoji"
class="note-actions-item">
<a
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 2d7cd30115d..ca12df9db64 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -41,7 +41,7 @@
this.initTaskList();
if (this.isEditing) {
- this.initAutoSave();
+ this.initAutoSave(this.note.noteable_type);
}
},
updated() {
@@ -50,7 +50,7 @@
if (this.isEditing) {
if (!this.autosave) {
- this.initAutoSave();
+ this.initAutoSave(this.note.noteable_type);
} else {
this.setAutoSave();
}
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index d382a9bb642..1a13fdbeb7c 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,9 +1,10 @@
<script>
- import { mapGetters } from 'vuex';
+ import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
+ import resolvable from '../mixins/resolvable';
export default {
name: 'IssueNoteForm',
@@ -13,6 +14,7 @@
},
mixins: [
issuableStateMixin,
+ resolvable,
],
props: {
noteBody: {
@@ -30,7 +32,7 @@
required: false,
default: 'Save comment',
},
- discussion: {
+ note: {
type: Object,
required: false,
default: () => ({}),
@@ -42,9 +44,11 @@
},
data() {
return {
- note: this.noteBody,
+ updatedNoteBody: this.noteBody,
conflictWhileEditing: false,
isSubmitting: false,
+ isResolving: false,
+ resolveAsThread: true,
};
},
computed: {
@@ -71,13 +75,13 @@
return this.getUserDataByProp('id');
},
isDisabled() {
- return !this.note.length || this.isSubmitting;
+ return !this.updatedNoteBody.length || this.isSubmitting;
},
},
watch: {
noteBody() {
- if (this.note === this.noteBody) {
- this.note = this.noteBody;
+ if (this.updatedNoteBody === this.noteBody) {
+ this.updatedNoteBody = this.noteBody;
} else {
this.conflictWhileEditing = true;
}
@@ -87,16 +91,24 @@
this.$refs.textarea.focus();
},
methods: {
- handleUpdate() {
+ ...mapActions([
+ 'toggleResolveNote',
+ ]),
+ handleUpdate(shouldResolve) {
+ const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true;
- this.$emit('handleFormUpdate', this.note, this.$refs.editNoteForm, () => {
+ this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
this.isSubmitting = false;
+
+ if (shouldResolve) {
+ this.resolveHandler(beforeSubmitDiscussionState);
+ }
});
},
editMyLastNote() {
- if (this.note === '') {
- const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion);
+ if (this.updatedNoteBody === '') {
+ const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody);
if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', {
@@ -107,7 +119,7 @@
},
cancelHandler(shouldConfirm = false) {
// Sends information about confirm message and if the textarea has changed
- this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note);
+ this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody);
},
},
};
@@ -150,7 +162,7 @@
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
:data-supports-quick-actions="!isEditing"
aria-label="Description"
- v-model="note"
+ v-model="updatedNoteBody"
ref="textarea"
slot="textarea"
placeholder="Write a comment or drag your files here..."
@@ -169,6 +181,13 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
{{ saveButtonTitle }}
</button>
<button
+ v-if="note.resolvable"
+ @click.prevent="handleUpdate(true)"
+ class="btn btn-nr btn-default append-right-10 js-comment-resolve-button"
+ >
+ {{ resolveButtonTitle }}
+ </button>
+ <button
@click="cancelHandler()"
class="btn btn-cancel note-edit-cancel"
type="button">
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 5b255d4a710..4743d95b951 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -34,15 +34,15 @@
required: false,
default: false,
},
- },
- data() {
- return {
- isExpanded: true,
- };
+ expanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
toggleChevronClass() {
- return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down';
+ return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down';
},
noteTimestampLink() {
return `#note_${this.noteId}`;
@@ -53,7 +53,6 @@
'setTargetNoteHash',
]),
handleToggle() {
- this.isExpanded = !this.isExpanded;
this.$emit('toggleHandler');
},
updateTargetNoteHash() {
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 98a06c5fc71..76bb53eaf2f 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,5 +1,7 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+ import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
+ import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -8,13 +10,19 @@
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
+ import diffWithNote from './diff_with_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
+ import noteable from '../mixins/noteable';
+ import resolvable from '../mixins/resolvable';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import { scrollToElement } from '../../lib/utils/common_utils';
export default {
components: {
noteableNote,
+ diffWithNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
@@ -23,8 +31,13 @@
placeholderNote,
placeholderSystemNote,
},
+ directives: {
+ tooltip,
+ },
mixins: [
autosave,
+ noteable,
+ resolvable,
],
props: {
note: {
@@ -35,14 +48,25 @@
data() {
return {
isReplying: false,
+ isResolving: false,
+ resolveAsThread: true,
};
},
computed: {
...mapGetters([
'getNoteableData',
+ 'discussionCount',
+ 'resolvedDiscussionCount',
+ 'unresolvedDiscussions',
]),
discussion() {
- return this.note.notes[0];
+ return {
+ ...this.note.notes[0],
+ truncatedDiffLines: this.note.truncated_diff_lines,
+ diffFile: this.note.diff_file,
+ diffDiscussion: this.note.diff_discussion,
+ imageDiffHtml: this.note.image_diff_html,
+ };
},
author() {
return this.discussion.author;
@@ -71,26 +95,40 @@
return null;
},
+ hasUnresolvedDiscussion() {
+ return this.unresolvedDiscussions.length > 0;
+ },
+ wrapperComponent() {
+ return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div';
+ },
+ wrapperClass() {
+ return this.isDiffDiscussion ? '' : 'panel panel-default';
+ },
},
mounted() {
if (this.isReplying) {
- this.initAutoSave();
+ this.initAutoSave(this.discussion.noteable_type);
}
},
updated() {
if (this.isReplying) {
if (!this.autosave) {
- this.initAutoSave();
+ this.initAutoSave(this.discussion.noteable_type);
} else {
this.setAutoSave();
}
}
},
+ created() {
+ this.resolveDiscussionsSvg = resolveDiscussionsSvg;
+ this.nextDiscussionsSvg = nextDiscussionsSvg;
+ },
methods: {
...mapActions([
'saveNote',
'toggleDiscussion',
'removePlaceholderNotes',
+ 'toggleResolveNote',
]),
componentName(note) {
if (note.isPlaceholderNote) {
@@ -103,7 +141,7 @@
return noteableNote;
},
componentData(note) {
- return note.isPlaceholderNote ? note.notes[0] : note;
+ return note.isPlaceholderNote ? this.note.notes[0] : note;
},
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.note.id });
@@ -128,7 +166,7 @@
flashContainer: this.$el,
data: {
in_reply_to_discussion_id: this.note.reply_id,
- target_type: 'issue',
+ target_type: this.noteableType,
target_id: this.discussion.noteable_id,
note: { note: noteText },
},
@@ -152,12 +190,27 @@ Please check your network connection and try again.`;
});
});
},
+ jumpToDiscussion() {
+ const unresolvedIds = this.unresolvedDiscussions.map(d => d.id);
+ const index = unresolvedIds.indexOf(this.note.id);
+
+ if (index >= 0 && index !== unresolvedIds.length) {
+ const nextId = unresolvedIds[index + 1];
+ const el = document.querySelector(`[data-discussion-id="${nextId}"]`);
+
+ if (el) {
+ scrollToElement(el);
+ }
+ }
+ },
},
};
</script>
<template>
- <li class="note note-discussion timeline-entry">
+ <li
+ :data-discussion-id="note.id"
+ class="note note-discussion timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
@@ -175,6 +228,7 @@ Please check your network connection and try again.`;
:created-at="discussion.created_at"
:note-id="discussion.id"
:include-toggle="true"
+ :expanded="note.expanded"
@toggleHandler="toggleDiscussionHandler"
action-text="started a discussion"
class="discussion"
@@ -187,43 +241,103 @@ Please check your network connection and try again.`;
class-name="discussion-headline-light js-discussion-headline"
/>
</div>
- </div>
- <div
- v-if="note.expanded"
- class="discussion-body">
- <div class="panel panel-default">
- <div class="discussion-notes">
- <ul class="notes">
- <component
- v-for="note in note.notes"
- :is="componentName(note)"
- :note="componentData(note)"
- :key="note.id"
- />
- </ul>
- <div
- :class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder">
- <button
- v-if="canReply && !isReplying"
- @click="showReplyForm"
- type="button"
- class="js-vue-discussion-reply btn btn-text-field"
- title="Add a reply">
- Reply...
- </button>
- <note-form
- v-if="isReplying"
- save-button-title="Comment"
- :discussion="note"
- :is-editing="false"
- @handleFormUpdate="saveReply"
- @cancelFormEdition="cancelReplyForm"
- ref="noteForm"
- />
- <note-signed-out-widget v-if="!canReply" />
+ <div
+ v-if="note.expanded"
+ class="discussion-body">
+ <component
+ :is="wrapperComponent"
+ :discussion="discussion"
+ :class="wrapperClass"
+ >
+ <div class="discussion-notes">
+ <ul class="notes">
+ <component
+ v-for="note in note.notes"
+ :is="componentName(note)"
+ :note="componentData(note)"
+ :key="note.id"
+ />
+ </ul>
+ <div
+ :class="{ 'is-replying': isReplying }"
+ class="discussion-reply-holder">
+ <template v-if="!isReplying && canReply">
+ <div
+ class="btn-group-justified discussion-with-resolve-btn"
+ role="group">
+ <div
+ class="btn-group"
+ role="group">
+ <button
+ @click="showReplyForm"
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field"
+ title="Add a reply">Reply...</button>
+ </div>
+ <div
+ v-if="note.resolvable"
+ class="btn-group"
+ role="group">
+ <button
+ @click="resolveHandler()"
+ type="button"
+ class="btn btn-default"
+ >
+ <i
+ v-if="isResolving"
+ aria-hidden="true"
+ class="fa fa-spinner fa-spin"
+ ></i>
+ {{ resolveButtonTitle }}
+ </button>
+ </div>
+ <div
+ class="btn-group discussion-actions"
+ role="group">
+ <div
+ v-if="note.resolvable && !discussionResolved"
+ class="btn-group"
+ role="group">
+ <a
+ :href="note.resolve_with_issue_path"
+ v-tooltip
+ class="new-issue-for-discussion btn
+ btn-default discussion-create-issue-btn"
+ title="Resolve this discussion in a new issue"
+ data-container="body"
+ >
+ <span v-html="resolveDiscussionsSvg"></span>
+ </a>
+ </div>
+ <div
+ v-if="hasUnresolvedDiscussion"
+ class="btn-group"
+ role="group">
+ <button
+ @click="jumpToDiscussion"
+ v-tooltip
+ class="btn btn-default discussion-next-btn"
+ title="Jump to next unresolved discussion"
+ data-container="body"
+ >
+ <span v-html="nextDiscussionsSvg"></span>
+ </button>
+ </div>
+ </div>
+ </div>
+ </template>
+ <note-form
+ v-if="isReplying"
+ save-button-title="Comment"
+ :note="note"
+ :is-editing="false"
+ @handleFormUpdate="saveReply"
+ @cancelFormEdition="cancelReplyForm"
+ ref="noteForm" />
+ <note-signed-out-widget v-if="!canReply" />
+ </div>
</div>
- </div>
+ </component>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 045077de383..4d17bd5acc2 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -7,6 +7,8 @@
import noteActions from './note_actions.vue';
import noteBody from './note_body.vue';
import eventHub from '../event_hub';
+ import noteable from '../mixins/noteable';
+ import resolvable from '../mixins/resolvable';
export default {
components: {
@@ -15,6 +17,10 @@
noteActions,
noteBody,
},
+ mixins: [
+ noteable,
+ resolvable,
+ ],
props: {
note: {
type: Object,
@@ -26,6 +32,7 @@
isEditing: false,
isDeleting: false,
isRequesting: false,
+ isResolving: false,
};
},
computed: {
@@ -65,6 +72,7 @@
...mapActions([
'deleteNote',
'updateNote',
+ 'toggleResolveNote',
'scrollToNoteIfNeeded',
]),
editHandler() {
@@ -89,7 +97,7 @@
const data = {
endpoint: this.note.path,
note: {
- target_type: 'issue',
+ target_type: this.noteableType,
target_id: this.note.noteable_id,
note: { note: noteText },
},
@@ -134,7 +142,7 @@
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
- this.$refs.noteBody.$refs.noteForm.note = noteText;
+ this.$refs.noteBody.$refs.noteForm.note.note = noteText;
},
},
};
@@ -171,8 +179,13 @@
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
:report-abuse-path="note.report_abuse_path"
+ :resolvable="note.resolvable"
+ :is-resolved="note.resolved"
+ :is-resolving="isResolving"
+ :resolved-by="note.resolved_by"
@handleEdit="editHandler"
@handleDelete="deleteHandler"
+ @handleResolve="resolveHandler"
/>
</div>
<note-body
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 92db4830704..74afed5560b 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -11,6 +11,7 @@
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
export default {
name: 'NotesApp',
@@ -48,7 +49,24 @@
...mapGetters([
'notes',
'getNotesDataByProp',
+ 'discussionCount',
]),
+ noteableType() {
+ // FIXME -- @fatihacet Get this from JSON data.
+ const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
+
+ return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE;
+ },
+ allNotes() {
+ if (this.isLoading) {
+ const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
+
+ return new Array(totalNotes).fill({
+ isSkeletonNote: true,
+ });
+ }
+ return this.notes;
+ },
},
created() {
this.setNotesData(this.notesData);
@@ -67,6 +85,10 @@
this.actionToggleAward({ awardName, noteId });
});
}
+ document.addEventListener('refreshVueNotes', this.fetchNotes);
+ },
+ beforeDestroy() {
+ document.removeEventListener('refreshVueNotes', this.fetchNotes);
},
methods: {
...mapActions({
@@ -81,6 +103,9 @@
setTargetNoteHash: 'setTargetNoteHash',
}),
getComponentName(note) {
+ if (note.isSkeletonNote) {
+ return skeletonLoadingContainer;
+ }
if (note.isPlaceholderNote) {
if (note.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote;
@@ -109,9 +134,14 @@
});
},
initPolling() {
+ if (this.isPollingInitialized) {
+ return;
+ }
+
this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
this.poll();
+ this.isPollingInitialized = true;
},
checkLocationHash() {
const hash = getLocationHash();
@@ -128,25 +158,20 @@
<template>
<div id="notes">
- <div
- v-if="isLoading"
- class="js-loading loading">
- <loading-icon />
- </div>
-
<ul
- v-if="!isLoading"
id="notes-list"
class="notes main-notes-list timeline">
<component
- v-for="note in notes"
+ v-for="note in allNotes"
:is="getComponentName(note)"
:note="getComponentData(note)"
:key="note.id"
/>
</ul>
- <comment-form />
+ <comment-form
+ :noteable-type="noteableType"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index a6961063c01..f4f407ffd8a 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -1,4 +1,5 @@
export const DISCUSSION_NOTE = 'DiscussionNote';
+export const DIFF_NOTE = 'DiffNote';
export const DISCUSSION = 'discussion';
export const NOTE = 'note';
export const SYSTEM_NOTE = 'systemNote';
@@ -8,4 +9,7 @@ export const REOPENED = 'reopened';
export const CLOSED = 'closed';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
-export const NOTEABLE_TYPE = 'Issue';
+export const ISSUE_NOTEABLE_TYPE = 'issue';
+export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
+export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
+export const RESOLVE_NOTE_METHOD_NAME = 'post';
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 48e7cfddb63..545bf2c99a7 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -20,17 +20,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
return {
noteableData: JSON.parse(notesDataset.noteableData),
currentUserData,
- notesData: {
- lastFetchedAt: notesDataset.lastFetchedAt,
- discussionsPath: notesDataset.discussionsPath,
- newSessionPath: notesDataset.newSessionPath,
- registerPath: notesDataset.registerPath,
- notesPath: notesDataset.notesPath,
- markdownDocsPath: notesDataset.markdownDocsPath,
- quickActionsDocsPath: notesDataset.quickActionsDocsPath,
- closeIssuePath: notesDataset.closeIssuePath,
- reopenIssuePath: notesDataset.reopenIssuePath,
- },
+ notesData: JSON.parse(notesDataset.notesData),
};
},
render(createElement) {
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index a008171beda..a3d897f2f12 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -1,9 +1,10 @@
import Autosave from '../../autosave';
+import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default {
methods: {
- initAutoSave() {
- this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id], 'issue');
+ initAutoSave(noteableType) {
+ this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]);
},
resetAutoSave() {
this.autosave.reset();
diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js
new file mode 100644
index 00000000000..0da4ff49f08
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/noteable.js
@@ -0,0 +1,22 @@
+import * as constants from '../constants';
+
+export default {
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ noteableType() {
+ switch (this.note.noteable_type) {
+ case 'MergeRequest':
+ return constants.MERGE_REQUEST_NOTEABLE_TYPE;
+ case 'Issue':
+ return constants.ISSUE_NOTEABLE_TYPE;
+ default:
+ return '';
+ }
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
new file mode 100644
index 00000000000..ab1ae115e52
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -0,0 +1,50 @@
+import Flash from '~/flash';
+import { __ } from '~/locale';
+
+export default {
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ discussionResolved() {
+ const { notes, resolved } = this.note;
+
+ if (notes) { // Decide resolved state using store. Only valid for discussions.
+ return notes.every(note => note.resolved && !note.system);
+ }
+
+ return resolved;
+ },
+ resolveButtonTitle() {
+ if (this.updatedNoteBody) {
+ if (this.discussionResolved) {
+ return __('Comment and unresolve discussion');
+ }
+
+ return __('Comment and resolve discussion');
+ }
+ return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion');
+ },
+ },
+ methods: {
+ resolveHandler(resolvedState = false) {
+ this.isResolving = true;
+ const endpoint = this.note.resolve_path || `${this.note.path}/resolve`;
+ const isResolved = this.discussionResolved || resolvedState;
+ const discussion = this.resolveAsThread;
+
+ this.toggleResolveNote({ endpoint, isResolved, discussion })
+ .then(() => {
+ this.isResolving = false;
+ })
+ .catch(() => {
+ this.isResolving = false;
+ const msg = __('Something went wrong while resolving this discussion. Please try again.');
+ Flash(msg, 'alert', this.$el);
+ });
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index b8e7ffc8c46..4766351dfc5 100644
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueResource from 'vue-resource';
+import * as constants from '../constants';
Vue.use(VueResource);
@@ -19,6 +20,12 @@ export default {
createNewNote(endpoint, data) {
return Vue.http.post(endpoint, data, { emulateJSON: true });
},
+ toggleResolveNote(endpoint, isResolved) {
+ const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
+ const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
+
+ return Vue.http[method](endpoint);
+ },
poll(data = {}) {
const { endpoint, lastFetchedAt } = data;
const options = {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 4c846d69b86..42fc2a131b8 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -61,8 +61,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export const removePlaceholderNotes = ({ commit }) =>
commit(types.REMOVE_PLACEHOLDER_NOTES);
+export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service
+ .toggleResolveNote(endpoint, isResolved)
+ .then(res => res.json())
+ .then((res) => {
+ const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
+
+ commit(mutationType, res);
+ });
+
export const closeIssue = ({ commit, dispatch, state }) => service
- .toggleIssueState(state.notesData.closeIssuePath)
+ .toggleIssueState(state.notesData.closePath)
.then(res => res.json())
.then((data) => {
commit(types.CLOSE_ISSUE);
@@ -70,7 +79,7 @@ export const closeIssue = ({ commit, dispatch, state }) => service
});
export const reopenIssue = ({ commit, dispatch, state }) => service
- .toggleIssueState(state.notesData.reopenIssuePath)
+ .toggleIssueState(state.notesData.reopenPath)
.then(res => res.json())
.then((data) => {
commit(types.REOPEN_ISSUE);
@@ -80,7 +89,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => service
export const emitStateChangedEvent = ({ commit, getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { detail: {
data,
- isClosed: getters.issueState === constants.CLOSED,
+ isClosed: getters.openState === constants.CLOSED,
} });
document.dispatchEvent(event);
@@ -174,7 +183,7 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
resp.notes.forEach((note) => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
- } else if (note.type === constants.DISCUSSION_NOTE) {
+ } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
if (discussion) {
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 82024104d73..e6180101c58 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -8,7 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
-export const issueState = state => state.noteableData.state;
+export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
@@ -30,3 +30,37 @@ export const getCurrentUserLastNote = state => _.flatten(
export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes)
.find(el => isLastNote(el, state));
+
+export const discussionCount = (state) => {
+ const discussions = state.notes.filter(n => !n.individual_note);
+
+ return discussions.length;
+};
+
+export const unresolvedDiscussions = (state, getters) => {
+ const resolvedMap = getters.resolvedDiscussionsById;
+
+ return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
+};
+
+export const resolvedDiscussionsById = (state) => {
+ const map = {};
+
+ state.notes.forEach((n) => {
+ if (n.notes) {
+ const resolved = n.notes.every(note => note.resolved && !note.system);
+
+ if (resolved) {
+ map[n.id] = n;
+ }
+ }
+ });
+
+ return map;
+};
+
+export const resolvedDiscussionCount = (state, getters) => {
+ const resolvedMap = getters.resolvedDiscussionsById;
+
+ return Object.keys(resolvedMap).length;
+};
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 6d7c3bbae0f..da1b5a9e51a 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -12,6 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export const TOGGLE_AWARD = 'TOGGLE_AWARD';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE';
+export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index b3f66578c9a..963b40be3fd 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -1,22 +1,32 @@
import * as utils from './utils';
import * as types from './mutation_types';
import * as constants from '../constants';
+import { isInMRPage } from '../../lib/utils/common_utils';
export default {
[types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note;
const [exists] = state.notes.filter(n => n.id === note.discussion_id);
+ const isDiscussion = (type === constants.DISCUSSION_NOTE);
if (!exists) {
const noteData = {
expanded: true,
id: discussion_id,
- individual_note: !(type === constants.DISCUSSION_NOTE),
+ individual_note: !isDiscussion,
notes: [note],
reply_id: discussion_id,
};
+ if (isDiscussion && isInMRPage()) {
+ noteData.resolvable = note.resolvable;
+ noteData.resolved = false;
+ noteData.resolve_path = note.resolve_path;
+ noteData.resolve_with_issue_path = note.resolve_with_issue_path;
+ }
+
state.notes.push(noteData);
+ document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
}
},
@@ -25,6 +35,7 @@ export default {
if (noteObj) {
noteObj.notes.push(note);
+ document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
}
},
@@ -41,6 +52,8 @@ export default {
state.notes.splice(state.notes.indexOf(noteObj), 1);
}
}
+
+ document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.REMOVE_PLACEHOLDER_NOTES](state) {
@@ -77,15 +90,19 @@ export default {
const notes = [];
notesData.forEach((note) => {
+ const nn = Object.assign({}, note);
+
// To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => {
- const nn = Object.assign({}, note);
nn.notes = [n]; // override notes array to only have one item to mimick individual_note
notes.push(nn);
});
} else {
- notes.push(note);
+ const oldNote = utils.findNoteObjectById(state.notes, note.id);
+ nn.expanded = oldNote ? oldNote.expanded : note.expanded;
+
+ notes.push(nn);
}
});
@@ -134,6 +151,8 @@ export default {
user: { id, name, username },
});
}
+
+ document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.TOGGLE_DISCUSSION](state, { discussionId }) {
@@ -151,6 +170,24 @@ export default {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
}
+
+ // document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ },
+
+ [types.UPDATE_DISCUSSION](state, noteData) {
+ const note = noteData;
+ let index = 0;
+
+ state.notes.forEach((n, i) => {
+ if (n.id === note.id) {
+ index = i;
+ }
+ });
+
+ note.expanded = true; // override expand flag to prevent collapse
+ state.notes.splice(index, 1, note);
+
+ document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.CLOSE_ISSUE](state) {
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index 6074115e855..275263a2aaa 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -28,4 +28,3 @@ export const getQuickActionText = (note) => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
-
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js
index c0b6e8d4095..d76b1f174fc 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/index.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js
@@ -1,3 +1,3 @@
import AbuseReports from './abuse_reports';
-export default () => new AbuseReports();
+document.addEventListener('DOMContentLoaded', () => new AbuseReports());
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
index b68ce5d32d8..f92450cbaa7 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
-export default function initBroadcastMessagesForm() {
+export default () => {
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
const previewColor = $(this).val();
$('div.broadcast-message-preview').css('background-color', previewColor);
@@ -32,4 +32,4 @@ export default function initBroadcastMessagesForm() {
.catch(() => flash(__('An error occurred while rendering preview broadcast message')));
}
}, 250));
-}
+};
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
index b548c48282a..d6cc6a850eb 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
@@ -1,3 +1,3 @@
import initBroadcastMessagesForm from './broadcast_message';
-export default () => initBroadcastMessagesForm();
+document.addEventListener('DOMContentLoaded', initBroadcastMessagesForm);
diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js
index 42ef9d38ef7..2d5020dbef4 100644
--- a/app/assets/javascripts/pages/admin/cohorts/index.js
+++ b/app/assets/javascripts/pages/admin/cohorts/index.js
@@ -1,3 +1,3 @@
import initUsagePing from './usage_ping';
-export default () => initUsagePing();
+document.addEventListener('DOMContentLoaded', initUsagePing);
diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js
index 5defea104d4..b0cdad627a6 100644
--- a/app/assets/javascripts/pages/admin/groups/show/index.js
+++ b/app/assets/javascripts/pages/admin/groups/show/index.js
@@ -1,3 +1,3 @@
import UsersSelect from '../../../../users_select';
-export default () => new UsersSelect();
+document.addEventListener('DOMContentLoaded', () => new UsersSelect());
diff --git a/app/assets/javascripts/pages/admin/labels/edit/index.js b/app/assets/javascripts/pages/admin/labels/edit/index.js
index d7ec6e47f67..5de1d4d6344 100644
--- a/app/assets/javascripts/pages/admin/labels/edit/index.js
+++ b/app/assets/javascripts/pages/admin/labels/edit/index.js
@@ -1,3 +1,3 @@
import Labels from '../../../../labels';
-export default () => new Labels();
+document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pages/admin/labels/new/index.js b/app/assets/javascripts/pages/admin/labels/new/index.js
index d7ec6e47f67..5de1d4d6344 100644
--- a/app/assets/javascripts/pages/admin/labels/new/index.js
+++ b/app/assets/javascripts/pages/admin/labels/new/index.js
@@ -1,3 +1,3 @@
import Labels from '../../../../labels';
-export default () => new Labels();
+document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js
index 71e0ddcd7b6..31c96eb87af 100644
--- a/app/assets/javascripts/pages/admin/projects/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index.js
@@ -1,9 +1,9 @@
import ProjectsList from '../../../projects_list';
import NamespaceSelect from '../../../namespace_select';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
document.querySelectorAll('.js-namespace-select')
.forEach(dropdown => new NamespaceSelect({ dropdown }));
-};
+});
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index b3f6a72fdcb..42f7460ad55 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -2,9 +2,9 @@
import { visitUrl } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select';
import { isMetaClick } from '~/lib/utils/common_utils';
-import { __ } from '../../../../locale';
-import flash from '../../../../flash';
-import axios from '../../../../lib/utils/axios_utils';
+import { __ } from '~/locale';
+import flash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
export default class Todos {
constructor() {
diff --git a/app/assets/javascripts/pages/groups/boards/index.js b/app/assets/javascripts/pages/groups/boards/index.js
new file mode 100644
index 00000000000..5cfe8723204
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/boards/index.js
@@ -0,0 +1,9 @@
+import UsersSelect from '~/users_select';
+import ShortcutsNavigation from '~/shortcuts_navigation';
+import initBoards from '~/boards';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new UsersSelect(); // eslint-disable-line no-new
+ new ShortcutsNavigation(); // eslint-disable-line no-new
+ initBoards();
+});
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
new file mode 100644
index 00000000000..22248418c41
--- /dev/null
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -0,0 +1,64 @@
+<script>
+ import axios from '~/lib/utils/axios_utils';
+ import createFlash from '~/flash';
+ import GlModal from '~/vue_shared/components/gl_modal.vue';
+ import { s__, sprintf } from '~/locale';
+ import { visitUrl } from '~/lib/utils/url_utility';
+ import eventHub from '../event_hub';
+
+ export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ milestoneTitle: {
+ type: String,
+ required: true,
+ },
+ url: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
+ },
+ text() {
+ return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group.
+ Existing project milestones with the same title will be merged.
+ This action cannot be reversed.`);
+ },
+ },
+ methods: {
+ onSubmit() {
+ eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
+ return axios.post(this.url, { params: { format: 'json' } })
+ .then((response) => {
+ eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true });
+ visitUrl(response.data.url);
+ })
+ .catch((error) => {
+ eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false });
+ createFlash(error);
+ });
+ },
+ },
+ };
+</script>
+<template>
+ <gl-modal
+ id="promote-milestone-modal"
+ footer-primary-button-variant="warning"
+ :footer-primary-button-text="s__('Milestones|Promote Milestone')"
+ @submit="onSubmit"
+ >
+ <template
+ slot="title"
+ >
+ {{ title }}
+ </template>
+ {{ text }}
+ </gl-modal>
+</template>
+
diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
new file mode 100644
index 00000000000..d51b5c221e3
--- /dev/null
+++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import deleteMilestoneModal from './components/delete_milestone_modal.vue';
+import eventHub from './event_hub';
+
+export default () => {
+ Vue.use(Translate);
+
+ const onRequestFinished = ({ milestoneUrl, successful }) => {
+ const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
+
+ if (!successful) {
+ button.removeAttribute('disabled');
+ }
+
+ button.querySelector('.js-loading-icon').classList.add('hidden');
+ };
+
+ const onRequestStarted = (milestoneUrl) => {
+ const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
+ button.setAttribute('disabled', '');
+ button.querySelector('.js-loading-icon').classList.remove('hidden');
+ eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
+ };
+
+ const onDeleteButtonClick = (event) => {
+ const button = event.currentTarget;
+ const modalProps = {
+ milestoneId: parseInt(button.dataset.milestoneId, 10),
+ milestoneTitle: button.dataset.milestoneTitle,
+ milestoneUrl: button.dataset.milestoneUrl,
+ issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
+ mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
+ };
+ eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
+ eventHub.$emit('deleteMilestoneModal.props', modalProps);
+ };
+
+ const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
+ deleteMilestoneButtons.forEach((button) => {
+ button.addEventListener('click', onDeleteButtonClick);
+ });
+
+ eventHub.$once('deleteMilestoneModal.mounted', () => {
+ deleteMilestoneButtons.forEach((button) => {
+ button.removeAttribute('disabled');
+ });
+ });
+
+ return new Vue({
+ el: '#delete-milestone-modal',
+ components: {
+ deleteMilestoneModal,
+ },
+ data() {
+ return {
+ modalProps: {
+ milestoneId: -1,
+ milestoneTitle: '',
+ milestoneUrl: '',
+ issueCount: -1,
+ mergeRequestCount: -1,
+ },
+ };
+ },
+ mounted() {
+ eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
+ eventHub.$emit('deleteMilestoneModal.mounted');
+ },
+ beforeDestroy() {
+ eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
+ },
+ methods: {
+ setModalProps(modalProps) {
+ this.modalProps = modalProps;
+ },
+ },
+ render(createElement) {
+ return createElement(deleteMilestoneModal, {
+ props: this.modalProps,
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/milestones/shared/index.js b/app/assets/javascripts/pages/milestones/shared/index.js
index 327e2cf569c..dabfe32848b 100644
--- a/app/assets/javascripts/pages/milestones/shared/index.js
+++ b/app/assets/javascripts/pages/milestones/shared/index.js
@@ -1,88 +1,7 @@
-import Vue from 'vue';
-
-import Translate from '~/vue_shared/translate';
-
-import deleteMilestoneModal from './components/delete_milestone_modal.vue';
-import eventHub from './event_hub';
+import initDeleteMilestoneModal from './delete_milestone_modal_init';
+import initPromoteMilestoneModal from './promote_milestone_modal_init';
export default () => {
- Vue.use(Translate);
-
- const onRequestFinished = ({ milestoneUrl, successful }) => {
- const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
-
- if (!successful) {
- button.removeAttribute('disabled');
- }
-
- button.querySelector('.js-loading-icon').classList.add('hidden');
- };
-
- const onRequestStarted = (milestoneUrl) => {
- const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
- button.setAttribute('disabled', '');
- button.querySelector('.js-loading-icon').classList.remove('hidden');
- eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
- };
-
- const onDeleteButtonClick = (event) => {
- const button = event.currentTarget;
- const modalProps = {
- milestoneId: parseInt(button.dataset.milestoneId, 10),
- milestoneTitle: button.dataset.milestoneTitle,
- milestoneUrl: button.dataset.milestoneUrl,
- issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
- mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
- };
- eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
- eventHub.$emit('deleteMilestoneModal.props', modalProps);
- };
-
- const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
- for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
- const button = deleteMilestoneButtons[i];
- button.addEventListener('click', onDeleteButtonClick);
- }
-
- eventHub.$once('deleteMilestoneModal.mounted', () => {
- for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
- const button = deleteMilestoneButtons[i];
- button.removeAttribute('disabled');
- }
- });
-
- return new Vue({
- el: '#delete-milestone-modal',
- components: {
- deleteMilestoneModal,
- },
- data() {
- return {
- modalProps: {
- milestoneId: -1,
- milestoneTitle: '',
- milestoneUrl: '',
- issueCount: -1,
- mergeRequestCount: -1,
- },
- };
- },
- mounted() {
- eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
- eventHub.$emit('deleteMilestoneModal.mounted');
- },
- beforeDestroy() {
- eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
- },
- methods: {
- setModalProps(modalProps) {
- this.modalProps = modalProps;
- },
- },
- render(createElement) {
- return createElement(deleteMilestoneModal, {
- props: this.modalProps,
- });
- },
- });
+ initDeleteMilestoneModal();
+ initPromoteMilestoneModal();
};
diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
new file mode 100644
index 00000000000..d00f81c9094
--- /dev/null
+++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
+import eventHub from './event_hub';
+
+Vue.use(Translate);
+
+export default () => {
+ const onRequestFinished = ({ milestoneUrl, successful }) => {
+ const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`);
+
+ if (!successful) {
+ button.removeAttribute('disabled');
+ }
+ };
+
+ const onRequestStarted = (milestoneUrl) => {
+ const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`);
+ button.setAttribute('disabled', '');
+ eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished);
+ };
+
+ const onDeleteButtonClick = (event) => {
+ const button = event.currentTarget;
+ const modalProps = {
+ milestoneTitle: button.dataset.milestoneTitle,
+ url: button.dataset.url,
+ };
+ eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
+ eventHub.$emit('promoteMilestoneModal.props', modalProps);
+ };
+
+ const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button');
+ promoteMilestoneButtons.forEach((button) => {
+ button.addEventListener('click', onDeleteButtonClick);
+ });
+
+ eventHub.$once('promoteMilestoneModal.mounted', () => {
+ promoteMilestoneButtons.forEach((button) => {
+ button.removeAttribute('disabled');
+ });
+ });
+
+ const promoteMilestoneModal = document.getElementById('promote-milestone-modal');
+ let promoteMilestoneComponent;
+
+ if (promoteMilestoneModal) {
+ promoteMilestoneComponent = new Vue({
+ el: promoteMilestoneModal,
+ components: {
+ PromoteMilestoneModal,
+ },
+ data() {
+ return {
+ modalProps: {
+ milestoneTitle: '',
+ url: '',
+ },
+ };
+ },
+ mounted() {
+ eventHub.$on('promoteMilestoneModal.props', this.setModalProps);
+ eventHub.$emit('promoteMilestoneModal.mounted');
+ },
+ beforeDestroy() {
+ eventHub.$off('promoteMilestoneModal.props', this.setModalProps);
+ },
+ methods: {
+ setModalProps(modalProps) {
+ this.modalProps = modalProps;
+ },
+ },
+ render(createElement) {
+ return createElement('promote-milestone-modal', {
+ props: this.modalProps,
+ });
+ },
+ });
+ }
+
+ return promoteMilestoneComponent;
+};
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
new file mode 100644
index 00000000000..c52ad7bc335
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -0,0 +1,16 @@
+import '~/profile/gl_crop';
+import Profile from '~/profile/profile';
+
+document.addEventListener('DOMContentLoaded', () => {
+ $(document).on('input.ssh_key', '#key_key', function () { // eslint-disable-line func-names
+ const $title = $('#key_title');
+ const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
+
+ // Extract the SSH Key title from its comment
+ if (comment && comment.length > 1) {
+ $title.val(comment[1]).change();
+ }
+ });
+
+ new Profile(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/profiles/index/index.js b/app/assets/javascripts/pages/profiles/index/index.js
index 90eed38777a..9bd430f4f11 100644
--- a/app/assets/javascripts/pages/profiles/index/index.js
+++ b/app/assets/javascripts/pages/profiles/index/index.js
@@ -1,7 +1,7 @@
import NotificationsForm from '../../../notifications_form';
import notificationsDropdown from '../../../notifications_dropdown';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
-};
+});
diff --git a/app/assets/javascripts/two_factor_auth.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index e3414d9afff..5b2473e0989 100644
--- a/app/assets/javascripts/two_factor_auth.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -1,4 +1,4 @@
-import U2FRegister from './u2f/register';
+import U2FRegister from '~/u2f/register';
document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
index 7889704a324..cd923f13ce8 100644
--- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js
+++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
@@ -1,8 +1,10 @@
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
+import initPipelines from '~/commit/pipelines/pipelines_bundle';
document.addEventListener('DOMContentLoaded', () => {
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
+ initPipelines();
});
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 460a54ab504..1aeed197385 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -5,6 +5,7 @@ import ShortcutsNavigation from '~/shortcuts_navigation';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown';
+import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
document.addEventListener('DOMContentLoaded', () => {
@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop);
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
fetchCommitMergeRequests();
+ initDiffNotes();
});
diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js
index 890062eeee6..d1c78bd61db 100644
--- a/app/assets/javascripts/pages/projects/compare/index.js
+++ b/app/assets/javascripts/pages/projects/compare/index.js
@@ -1,5 +1,3 @@
import initCompareAutocomplete from '~/compare_autocomplete';
-export default () => {
- initCompareAutocomplete();
-};
+document.addEventListener('DOMContentLoaded', initCompareAutocomplete);
diff --git a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
new file mode 100644
index 00000000000..df58e9dd072
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
@@ -0,0 +1,3 @@
+import initCycleAnalytics from '~/cycle_analytics/cycle_analytics_bundle';
+
+document.addEventListener('DOMContentLoaded', initCycleAnalytics);
diff --git a/app/assets/javascripts/pages/projects/environments/folder/index.js b/app/assets/javascripts/pages/projects/environments/folder/index.js
new file mode 100644
index 00000000000..5feaf944038
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/folder/index.js
@@ -0,0 +1,3 @@
+import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle';
+
+document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle);
diff --git a/app/assets/javascripts/pages/projects/environments/index/index.js b/app/assets/javascripts/pages/projects/environments/index/index.js
new file mode 100644
index 00000000000..ace8af00ece
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/index/index.js
@@ -0,0 +1,3 @@
+import initEnviroments from '~/environments/';
+
+document.addEventListener('DOMContentLoaded', initEnviroments);
diff --git a/app/assets/javascripts/pages/projects/environments/terminal/index.js b/app/assets/javascripts/pages/projects/environments/terminal/index.js
new file mode 100644
index 00000000000..7129e24cee1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/terminal/index.js
@@ -0,0 +1,3 @@
+import initTerminal from '~/terminal/';
+
+document.addEventListener('DOMContentLoaded', initTerminal);
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 9b1d52692a3..de1e13de7e9 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,7 +1,7 @@
import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
-};
+});
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
new file mode 100644
index 00000000000..500fbd27340
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -0,0 +1,13 @@
+import initIssuableSidebar from '~/init_issuable_sidebar';
+import Issue from '~/issue';
+import ShortcutsIssuable from '~/shortcuts_issuable';
+import ZenMode from '~/zen_mode';
+import '~/notes/index';
+import '~/issue_show/index';
+
+export default function () {
+ new Issue(); // eslint-disable-line no-new
+ new ShortcutsIssuable(); // eslint-disable-line no-new
+ new ZenMode(); // eslint-disable-line no-new
+ initIssuableSidebar();
+}
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index 1e56aa58da2..7968dfd7a12 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,13 +1,7 @@
-import initIssuableSidebar from '~/init_issuable_sidebar';
-import Issue from '~/issue';
-import ShortcutsIssuable from '~/shortcuts_issuable';
-import ZenMode from '~/zen_mode';
-import '~/notes/index';
-import '~/issue_show/index';
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initShow from '../show';
document.addEventListener('DOMContentLoaded', () => {
- new Issue(); // eslint-disable-line no-new
- new ShortcutsIssuable(); // eslint-disable-line no-new
- new ZenMode(); // eslint-disable-line no-new
- initIssuableSidebar();
+ initShow();
+ initSidebarBundle();
});
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
new file mode 100644
index 00000000000..54695dfeb99
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -0,0 +1,79 @@
+<script>
+ import axios from '~/lib/utils/axios_utils';
+ import createFlash from '~/flash';
+ import GlModal from '~/vue_shared/components/gl_modal.vue';
+ import { s__, sprintf } from '~/locale';
+ import { visitUrl } from '~/lib/utils/url_utility';
+ import eventHub from '../event_hub';
+
+ export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ labelTitle: {
+ type: String,
+ required: true,
+ },
+ labelColor: {
+ type: String,
+ required: true,
+ },
+ labelTextColor: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ text() {
+ return s__(`Milestones|Promoting this label will make it available for all projects inside the group.
+ Existing project labels with the same title will be merged. This action cannot be reversed.`);
+ },
+ title() {
+ const label = `<span
+ class="label color-label"
+ style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
+ >${this.labelTitle}</span>`;
+
+ return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), {
+ labelTitle: label,
+ }, false);
+ },
+ },
+ methods: {
+ onSubmit() {
+ eventHub.$emit('promoteLabelModal.requestStarted', this.url);
+ return axios.post(this.url, { params: { format: 'json' } })
+ .then((response) => {
+ eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true });
+ visitUrl(response.data.url);
+ })
+ .catch((error) => {
+ eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false });
+ createFlash(error);
+ });
+ },
+ },
+ };
+</script>
+<template>
+ <gl-modal
+ id="promote-label-modal"
+ footer-primary-button-variant="warning"
+ :footer-primary-button-text="s__('Labels|Promote Label')"
+ @submit="onSubmit"
+ >
+ <div
+ slot="title"
+ v-html="title"
+ >
+ {{ title }}
+ </div>
+
+ {{ text }}
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/pages/projects/labels/event_hub.js b/app/assets/javascripts/pages/projects/labels/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/labels/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 6e45de2a724..2abcbfab1ed 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -1,3 +1,91 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
import initLabels from '~/init_labels';
+import eventHub from '../event_hub';
+import PromoteLabelModal from '../components/promote_label_modal.vue';
-document.addEventListener('DOMContentLoaded', initLabels);
+Vue.use(Translate);
+
+const initLabelIndex = () => {
+ initLabels();
+
+ const onRequestFinished = ({ labelUrl, successful }) => {
+ const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
+
+ if (!successful) {
+ button.removeAttribute('disabled');
+ }
+ };
+
+ const onRequestStarted = (labelUrl) => {
+ const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
+ button.setAttribute('disabled', '');
+ eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished);
+ };
+
+ const onDeleteButtonClick = (event) => {
+ const button = event.currentTarget;
+ const modalProps = {
+ labelTitle: button.dataset.labelTitle,
+ labelColor: button.dataset.labelColor,
+ labelTextColor: button.dataset.labelTextColor,
+ url: button.dataset.url,
+ };
+ eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
+ eventHub.$emit('promoteLabelModal.props', modalProps);
+ };
+
+ const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button');
+ promoteLabelButtons.forEach((button) => {
+ button.addEventListener('click', onDeleteButtonClick);
+ });
+
+ eventHub.$once('promoteLabelModal.mounted', () => {
+ promoteLabelButtons.forEach((button) => {
+ button.removeAttribute('disabled');
+ });
+ });
+
+ const promoteLabelModal = document.getElementById('promote-label-modal');
+ let promoteLabelModalComponent;
+
+ if (promoteLabelModal) {
+ promoteLabelModalComponent = new Vue({
+ el: promoteLabelModal,
+ components: {
+ PromoteLabelModal,
+ },
+ data() {
+ return {
+ modalProps: {
+ labelTitle: '',
+ labelColor: '',
+ labelTextColor: '',
+ url: '',
+ },
+ };
+ },
+ mounted() {
+ eventHub.$on('promoteLabelModal.props', this.setModalProps);
+ eventHub.$emit('promoteLabelModal.mounted');
+ },
+ beforeDestroy() {
+ eventHub.$off('promoteLabelModal.props', this.setModalProps);
+ },
+ methods: {
+ setModalProps(modalProps) {
+ this.modalProps = modalProps;
+ },
+ },
+ render(createElement) {
+ return createElement('promote-label-modal', {
+ props: this.modalProps,
+ });
+ },
+ });
+ }
+
+ return promoteLabelModalComponent;
+};
+
+document.addEventListener('DOMContentLoaded', initLabelIndex);
diff --git a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
new file mode 100644
index 00000000000..28641104c58
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
@@ -0,0 +1,7 @@
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initMergeConflicts from '~/merge_conflicts/merge_conflicts_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initSidebarBundle();
+ initMergeConflicts();
+});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index 1d5aec4001d..6c9afddefac 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -1,5 +1,6 @@
import Compare from '~/compare';
import MergeRequest from '~/merge_request';
+import initPipelines from '~/commit/pipelines/pipelines_bundle';
document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
@@ -14,5 +15,6 @@ document.addEventListener('DOMContentLoaded', () => {
new MergeRequest({ // eslint-disable-line no-new
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
+ initPipelines();
}
});
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
new file mode 100644
index 00000000000..28d8761b502
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -0,0 +1,32 @@
+import MergeRequest from '~/merge_request';
+import ZenMode from '~/zen_mode';
+import initNotes from '~/init_notes';
+import initIssuableSidebar from '~/init_issuable_sidebar';
+import initDiffNotes from '~/diff_notes/diff_notes_bundle';
+import ShortcutsIssuable from '~/shortcuts_issuable';
+import Diff from '~/diff';
+import { handleLocationHash } from '~/lib/utils/common_utils';
+import howToMerge from '~/how_to_merge';
+import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import initWidget from '../../../vue_merge_request_widget';
+
+export default function () {
+ new Diff(); // eslint-disable-line no-new
+ new ZenMode(); // eslint-disable-line no-new
+
+ initIssuableSidebar();
+ initNotes();
+ initDiffNotes();
+ initPipelines();
+
+ const mrShowNode = document.querySelector('.merge-request');
+
+ window.mergeRequest = new MergeRequest({
+ action: mrShowNode.dataset.mrAction,
+ });
+
+ new ShortcutsIssuable(true); // eslint-disable-line no-new
+ handleLocationHash();
+ howToMerge();
+ initWidget();
+}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index 07f3e579c97..e5b2827b50c 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,28 +1,13 @@
-import MergeRequest from '~/merge_request';
-import ZenMode from '~/zen_mode';
-import initNotes from '~/init_notes';
-import initIssuableSidebar from '~/init_issuable_sidebar';
-import initDiffNotes from '~/diff_notes/diff_notes_bundle';
-import ShortcutsIssuable from '~/shortcuts_issuable';
-import Diff from '~/diff';
-import { handleLocationHash } from '~/lib/utils/common_utils';
-import howToMerge from '~/how_to_merge';
+import { hasVueMRDiscussionsCookie } from '~/lib/utils/common_utils';
+import initMrNotes from '~/mr_notes';
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initShow from '../init_merge_request_show';
document.addEventListener('DOMContentLoaded', () => {
- new Diff(); // eslint-disable-line no-new
- new ZenMode(); // eslint-disable-line no-new
+ initShow();
+ initSidebarBundle();
- initIssuableSidebar();
- initNotes();
- initDiffNotes();
-
- const mrShowNode = document.querySelector('.merge-request');
-
- window.mergeRequest = new MergeRequest({
- action: mrShowNode.dataset.mrAction,
- });
-
- new ShortcutsIssuable(true); // eslint-disable-line no-new
- handleLocationHash();
- howToMerge();
+ if (hasVueMRDiscussionsCookie()) {
+ initMrNotes();
+ }
});
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/pages/projects/network/network.js
index a3fd22aff2a..7354243e4c8 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/pages/projects/network/network.js
@@ -1,6 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
-import BranchGraph from './branch_graph';
+import BranchGraph from '../../../network/branch_graph';
export default (function() {
function Network(opts) {
diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js
new file mode 100644
index 00000000000..e7dfd2d0128
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/network/show/index.js
@@ -0,0 +1,16 @@
+import ShortcutsNetwork from '../../../../shortcuts_network';
+import Network from '../network';
+
+document.addEventListener('DOMContentLoaded', () => {
+ if (!$('.network-graph').length) return;
+
+ const networkGraph = new Network({
+ url: $('.network-graph').attr('data-url'),
+ commit_url: $('.network-graph').attr('data-commit-url'),
+ ref: $('.network-graph').attr('data-ref'),
+ commit_id: $('.network-graph').attr('data-commit-id'),
+ });
+
+ // eslint-disable-next-line no-new
+ new ShortcutsNetwork(networkGraph.branch_graph);
+});
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index 71c49deb9d0..ea6fd961393 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -2,8 +2,8 @@ import ProjectNew from '../shared/project_new';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new ProjectNew(); // eslint-disable-line no-new
initProjectVisibilitySelector();
initProjectNew.bindEvents();
-};
+});
diff --git a/app/assets/javascripts/pages/projects/pipelines/builds/index.js b/app/assets/javascripts/pages/projects/pipelines/builds/index.js
index fbe9824c34b..7a57e417b41 100644
--- a/app/assets/javascripts/pages/projects/pipelines/builds/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/builds/index.js
@@ -1,3 +1,7 @@
+import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines';
-document.addEventListener('DOMContentLoaded', initPipelines);
+document.addEventListener('DOMContentLoaded', () => {
+ initPipelines();
+ initPipelineDetails();
+});
diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js
new file mode 100644
index 00000000000..a84e2790680
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
+import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
+import Translate from '../../../../vue_shared/translate';
+import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils';
+
+Vue.use(Translate);
+
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#pipelines-list-vue',
+ components: {
+ pipelinesComponent,
+ },
+ data() {
+ return {
+ store: new PipelinesStore(),
+ };
+ },
+ created() {
+ this.dataset = document.querySelector(this.$options.el).dataset;
+ },
+ render(createElement) {
+ return createElement('pipelines-component', {
+ props: {
+ store: this.store,
+ endpoint: this.dataset.endpoint,
+ helpPagePath: this.dataset.helpPagePath,
+ emptyStateSvgPath: this.dataset.emptyStateSvgPath,
+ errorStateSvgPath: this.dataset.errorStateSvgPath,
+ noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
+ autoDevopsPath: this.dataset.helpAutoDevopsPath,
+ newPipelinePath: this.dataset.newPipelinePath,
+ canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
+ hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
+ ciLintPath: this.dataset.ciLintPath,
+ resetCachePath: this.dataset.resetCachePath,
+ },
+ });
+ },
+}));
diff --git a/app/assets/javascripts/pages/projects/pipelines/show/index.js b/app/assets/javascripts/pages/projects/pipelines/show/index.js
index fbe9824c34b..7a57e417b41 100644
--- a/app/assets/javascripts/pages/projects/pipelines/show/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/show/index.js
@@ -1,3 +1,7 @@
+import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines';
-document.addEventListener('DOMContentLoaded', initPipelines);
+document.addEventListener('DOMContentLoaded', () => {
+ initPipelines();
+ initPipelineDetails();
+});
diff --git a/app/assets/javascripts/pages/projects/registry/repositories/index.js b/app/assets/javascripts/pages/projects/registry/repositories/index.js
new file mode 100644
index 00000000000..35564754ee0
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/registry/repositories/index.js
@@ -0,0 +1,3 @@
+import initRegistryImages from '~/registry/index';
+
+document.addEventListener('DOMContentLoaded', initRegistryImages);
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index 5a6f4138b10..788d86d1192 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,7 +1,17 @@
+/* eslint-disable no-new */
+
+import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
+import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
+import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
+import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
document.addEventListener('DOMContentLoaded', () => {
+ new ProtectedTagCreate();
+ new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
+ new ProtectedBranchCreate(); // eslint-disable-line no-new
+ new ProtectedBranchEditList(); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/projects/snippets/edit/index.js b/app/assets/javascripts/pages/projects/snippets/edit/index.js
index caf9ee9b398..c15f798b630 100644
--- a/app/assets/javascripts/pages/projects/snippets/edit/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/edit/index.js
@@ -1,3 +1,7 @@
+import initSnippet from '~/snippet/snippet_bundle';
import initForm from '~/pages/projects/init_form';
-document.addEventListener('DOMContentLoaded', () => initForm($('.snippet-form')));
+document.addEventListener('DOMContentLoaded', () => {
+ initSnippet();
+ initForm($('.snippet-form'));
+});
diff --git a/app/assets/javascripts/pages/projects/snippets/new/index.js b/app/assets/javascripts/pages/projects/snippets/new/index.js
index caf9ee9b398..c15f798b630 100644
--- a/app/assets/javascripts/pages/projects/snippets/new/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/new/index.js
@@ -1,3 +1,7 @@
+import initSnippet from '~/snippet/snippet_bundle';
import initForm from '~/pages/projects/init_form';
-document.addEventListener('DOMContentLoaded', () => initForm($('.snippet-form')));
+document.addEventListener('DOMContentLoaded', () => {
+ initSnippet();
+ initForm($('.snippet-form'));
+});
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index eb14c7a0e78..b9f8707fd6e 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -3,9 +3,9 @@ import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
-};
+});
diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js
index de8d4168d71..7fdf4ee0bf3 100644
--- a/app/assets/javascripts/pages/search/init_filtered_search.js
+++ b/app/assets/javascripts/pages/search/init_filtered_search.js
@@ -1,9 +1,23 @@
import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
-export default ({ page }) => {
+export default ({
+ page,
+ filteredSearchTokenKeys,
+ isGroup,
+ isGroupAncestor,
+ isGroupDecendent,
+ stateFiltersSelector,
+}) => {
const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search');
if (filteredSearchEnabled) {
- const filteredSearchManager = new FilteredSearchManager({ page });
+ const filteredSearchManager = new FilteredSearchManager({
+ page,
+ isGroup,
+ isGroupAncestor,
+ isGroupDecendent,
+ filteredSearchTokenKeys,
+ stateFiltersSelector,
+ });
filteredSearchManager.setup();
}
};
diff --git a/app/assets/javascripts/pages/snippets/edit/index.js b/app/assets/javascripts/pages/snippets/edit/index.js
index 2ee38b64ca1..d86e1632ae5 100644
--- a/app/assets/javascripts/pages/snippets/edit/index.js
+++ b/app/assets/javascripts/pages/snippets/edit/index.js
@@ -1,3 +1,7 @@
+import initSnippet from '~/snippet/snippet_bundle';
import form from '../form';
-document.addEventListener('DOMContentLoaded', form);
+document.addEventListener('DOMContentLoaded', () => {
+ initSnippet();
+ form();
+});
diff --git a/app/assets/javascripts/pages/snippets/new/index.js b/app/assets/javascripts/pages/snippets/new/index.js
index 2ee38b64ca1..d86e1632ae5 100644
--- a/app/assets/javascripts/pages/snippets/new/index.js
+++ b/app/assets/javascripts/pages/snippets/new/index.js
@@ -1,3 +1,7 @@
+import initSnippet from '~/snippet/snippet_bundle';
import form from '../form';
-document.addEventListener('DOMContentLoaded', form);
+document.addEventListener('DOMContentLoaded', () => {
+ initSnippet();
+ form();
+});
diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue
new file mode 100644
index 00000000000..8d3d6223d7b
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/blank_state.vue
@@ -0,0 +1,32 @@
+<script>
+ export default {
+ name: 'PipelinesSvgState',
+ props: {
+ svgPath: {
+ type: String,
+ required: true,
+ },
+
+ message: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="row empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ <img :src="svgPath" />
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>{{ message }}</h4>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index dfaa2574091..10ac8c08bed 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -1,5 +1,6 @@
<script>
export default {
+ name: 'PipelinesEmptyState',
props: {
helpPagePath: {
type: String,
@@ -9,6 +10,10 @@
type: String,
required: true,
},
+ canSetCi: {
+ type: Boolean,
+ required: true,
+ },
},
};
</script>
@@ -22,22 +27,36 @@
<div class="col-xs-12">
<div class="text-content">
- <h4 class="text-center">
- {{ s__("Pipelines|Build with confidence") }}
- </h4>
- <p>
- {{ s__(`Pipelines|Continous Integration can help
-catch bugs by running your tests automatically,
-while Continuous Deployment can help you deliver code to your product environment.`) }}
+
+ <template v-if="canSetCi">
+ <h4 class="text-center">
+ {{ s__('Pipelines|Build with confidence') }}
+ </h4>
+
+ <p>
+ {{ s__(`Pipelines|Continous Integration can help
+ catch bugs by running your tests automatically,
+ while Continuous Deployment can help you deliver
+ code to your product environment.`) }}
+ </p>
+
+ <div class="text-center">
+ <a
+ :href="helpPagePath"
+ class="btn btn-primary js-get-started-pipelines"
+ >
+ {{ s__('Pipelines|Get started with Pipelines') }}
+ </a>
+ </div>
+ </template>
+
+ <p
+ v-else
+ class="text-center"
+ >
+ {{ s__('Pipelines|This project is not currently set up to run pipelines.') }}
</p>
- <div class="text-center">
- <a
- :href="helpPagePath"
- class="btn btn-info"
- >
- {{ s__("Pipelines|Get started with Pipelines") }}
- </a>
- </div>
+
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue
deleted file mode 100644
index 012853b201d..00000000000
--- a/app/assets/javascripts/pipelines/components/error_state.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-export default {
- props: {
- errorStateSvgPath: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="row empty-state js-pipelines-error-state">
- <div class="col-xs-12">
- <div class="svg-content">
- <img :src="errorStateSvgPath"/>
- </div>
- </div>
-
- <div class="col-xs-12 text-center">
- <div class="text-content">
- <h4>The API failed to fetch the pipelines.</h4>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
index f31a91c3403..eba5678e3e5 100644
--- a/app/assets/javascripts/pipelines/components/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/nav_controls.vue
@@ -1,67 +1,67 @@
<script>
-export default {
- name: 'PipelineNavControls',
- props: {
- newPipelinePath: {
- type: String,
- required: true,
- },
+ import LoadingButton from '../../vue_shared/components/loading_button.vue';
- hasCiEnabled: {
- type: Boolean,
- required: true,
+ export default {
+ name: 'PipelineNavControls',
+ components: {
+ LoadingButton,
},
+ props: {
+ newPipelinePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
- helpPagePath: {
- type: String,
- required: true,
- },
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
- resetCachePath: {
- type: String,
- required: true,
- },
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
- ciLintPath: {
- type: String,
- required: true,
+ isResetCacheButtonLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
-
- canCreatePipeline: {
- type: Boolean,
- required: true,
+ methods: {
+ onClickResetCache() {
+ this.$emit('resetRunnersCache', this.resetCachePath);
+ },
},
- },
-};
+ };
</script>
<template>
<div class="nav-controls">
<a
- v-if="canCreatePipeline"
+ v-if="newPipelinePath"
:href="newPipelinePath"
- class="btn btn-create">
- Run Pipeline
+ class="btn btn-create js-run-pipeline"
+ >
+ {{ s__('Pipelines|Run Pipeline') }}
</a>
- <a
- v-if="!hasCiEnabled"
- :href="helpPagePath"
- class="btn btn-info">
- Get started with Pipelines
- </a>
-
- <a
- data-method="post"
- rel="nofollow"
- :href="resetCachePath"
- class="btn btn-default">
- Clear runner caches
- </a>
+ <loading-button
+ v-if="resetCachePath"
+ @click="onClickResetCache"
+ :loading="isResetCacheButtonLoading"
+ class="btn btn-default js-clear-cache"
+ :label="s__('Pipelines|Clear Runner Caches')"
+ />
<a
+ v-if="ciLintPath"
:href="ciLintPath"
- class="btn btn-default">
- CI Lint
+ class="btn btn-default js-ci-lint"
+ >
+ {{ s__('Pipelines|CI Lint') }}
</a>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 90930d5ff44..e0a7284124d 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -1,12 +1,13 @@
<script>
import _ from 'underscore';
+ import { __, sprintf, s__ } from '../../locale';
+ import createFlash from '../../flash';
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
- import tablePagination from '../../vue_shared/components/table_pagination.vue';
- import navigationTabs from '../../vue_shared/components/navigation_tabs.vue';
- import navigationControls from './nav_controls.vue';
+ import TablePagination from '../../vue_shared/components/table_pagination.vue';
+ import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
+ import NavigationControls from './nav_controls.vue';
import {
- convertPermissionToBoolean,
getParameterByName,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
@@ -14,9 +15,9 @@
export default {
components: {
- tablePagination,
- navigationTabs,
- navigationControls,
+ TablePagination,
+ NavigationTabs,
+ NavigationControls,
},
mixins: [
pipelinesMixin,
@@ -36,111 +37,187 @@
required: false,
default: 'root',
},
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ errorStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ noPipelinesSvgPath: {
+ type: String,
+ required: true,
+ },
+ autoDevopsPath: {
+ type: String,
+ required: true,
+ },
+ hasGitlabCi: {
+ type: Boolean,
+ required: true,
+ },
+ canCreatePipeline: {
+ type: Boolean,
+ required: true,
+ },
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ newPipelinePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
- const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
-
return {
- endpoint: pipelinesData.endpoint,
- helpPagePath: pipelinesData.helpPagePath,
- emptyStateSvgPath: pipelinesData.emptyStateSvgPath,
- errorStateSvgPath: pipelinesData.errorStateSvgPath,
- autoDevopsPath: pipelinesData.helpAutoDevopsPath,
- newPipelinePath: pipelinesData.newPipelinePath,
- canCreatePipeline: pipelinesData.canCreatePipeline,
- hasCi: pipelinesData.hasCi,
- ciLintPath: pipelinesData.ciLintPath,
- resetCachePath: pipelinesData.resetCachePath,
+ // Start with loading state to avoid a glitch when the empty state will be rendered
+ isLoading: true,
state: this.store.state,
scope: getParameterByName('scope') || 'all',
page: getParameterByName('page') || '1',
requestData: {},
+ isResetCacheButtonLoading: false,
};
},
- computed: {
- canCreatePipelineParsed() {
- return convertPermissionToBoolean(this.canCreatePipeline);
- },
+ stateMap: {
+ // with tabs
+ loading: 'loading',
+ tableList: 'tableList',
+ error: 'error',
+ emptyTab: 'emptyTab',
+ // without tabs
+ emptyState: 'emptyState',
+ },
+ scopes: {
+ all: 'all',
+ pending: 'pending',
+ running: 'running',
+ finished: 'finished',
+ branches: 'branches',
+ tags: 'tags',
+ },
+ computed: {
/**
- * The empty state should only be rendered when the request is made to fetch all pipelines
- * and none is returned.
- *
- * @return {Boolean}
- */
- shouldRenderEmptyState() {
- return !this.isLoading &&
- !this.hasError &&
- this.hasMadeRequest &&
- !this.state.pipelines.length &&
- (this.scope === 'all' || this.scope === null);
+ * `hasGitlabCi` handles both internal and external CI.
+ * The order on which the checks are made in this method is
+ * important to guarantee we handle all the corner cases.
+ */
+ stateToRender() {
+ const { stateMap } = this.$options;
+
+ if (this.isLoading) {
+ return stateMap.loading;
+ }
+
+ if (this.hasError) {
+ return stateMap.error;
+ }
+
+ if (this.state.pipelines.length) {
+ return stateMap.tableList;
+ }
+
+ if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
+ return stateMap.emptyTab;
+ }
+
+ return stateMap.emptyState;
},
/**
- * When a specific scope does not have pipelines we render a message.
- *
- * @return {Boolean}
+ * Tabs are rendered in all states except empty state.
+ * They are not rendered before the first request to avoid a flicker on first load.
*/
- shouldRenderNoPipelinesMessage() {
- return !this.isLoading &&
- !this.hasError &&
- !this.state.pipelines.length &&
- this.scope !== 'all' &&
- this.scope !== null;
+ shouldRenderTabs() {
+ const { stateMap } = this.$options;
+ return this.hasMadeRequest &&
+ [
+ stateMap.loading,
+ stateMap.tableList,
+ stateMap.error,
+ stateMap.emptyTab,
+ ].includes(this.stateToRender);
},
- shouldRenderTable() {
- return !this.hasError &&
- !this.isLoading && this.state.pipelines.length;
+ shouldRenderButtons() {
+ return (this.newPipelinePath ||
+ this.resetCachePath ||
+ this.ciLintPath) && this.shouldRenderTabs;
},
- /**
- * Pagination should only be rendered when there is more than one page.
- *
- * @return {Boolean}
- */
+
shouldRenderPagination() {
return !this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage;
},
- hasCiEnabled() {
- return this.hasCi !== undefined;
+
+ emptyTabMessage() {
+ const { scopes } = this.$options;
+ const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
+
+ if (possibleScopes.includes(this.scope)) {
+ return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), {
+ scope: this.scope,
+ });
+ }
+
+ return s__('Pipelines|There are currently no pipelines.');
},
tabs() {
const { count } = this.state;
+ const { scopes } = this.$options;
+
return [
{
- name: 'All',
- scope: 'all',
+ name: __('All'),
+ scope: scopes.all,
count: count.all,
isActive: this.scope === 'all',
},
{
- name: 'Pending',
- scope: 'pending',
+ name: __('Pending'),
+ scope: scopes.pending,
count: count.pending,
isActive: this.scope === 'pending',
},
{
- name: 'Running',
- scope: 'running',
+ name: __('Running'),
+ scope: scopes.running,
count: count.running,
isActive: this.scope === 'running',
},
{
- name: 'Finished',
- scope: 'finished',
+ name: __('Finished'),
+ scope: scopes.finished,
count: count.finished,
isActive: this.scope === 'finished',
},
{
- name: 'Branches',
- scope: 'branches',
+ name: __('Branches'),
+ scope: scopes.branches,
isActive: this.scope === 'branches',
},
{
- name: 'Tags',
- scope: 'tags',
+ name: __('Tags'),
+ scope: scopes.tags,
isActive: this.scope === 'tags',
},
];
@@ -187,7 +264,24 @@
this.errorCallback();
// restart polling
- this.poll.restart();
+ this.poll.restart({ data: this.requestData });
+ });
+ },
+
+ handleResetRunnersCache(endpoint) {
+ this.isResetCacheButtonLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => {
+ this.isResetCacheButtonLoading = false;
+ createFlash(
+ s__('Pipelines|Project cache successfully reset.'),
+ 'notice',
+ );
+ })
+ .catch(() => {
+ this.isResetCacheButtonLoading = false;
+ createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.'));
});
},
},
@@ -197,69 +291,72 @@
<div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
- v-if="!shouldRenderEmptyState"
+ v-if="shouldRenderTabs || shouldRenderButtons"
>
<div class="fade-left">
<i
class="fa fa-angle-left"
- aria-hidden="true">
+ aria-hidden="true"
+ >
</i>
</div>
<div class="fade-right">
<i
class="fa fa-angle-right"
- aria-hidden="true">
+ aria-hidden="true"
+ >
</i>
</div>
<navigation-tabs
+ v-if="shouldRenderTabs"
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="pipelines"
/>
<navigation-controls
+ v-if="shouldRenderButtons"
:new-pipeline-path="newPipelinePath"
- :has-ci-enabled="hasCiEnabled"
- :help-page-path="helpPagePath"
:reset-cache-path="resetCachePath"
:ci-lint-path="ciLintPath"
- :can-create-pipeline="canCreatePipelineParsed "
+ @resetRunnersCache="handleResetRunnersCache"
+ :is-reset-cache-button-loading="isResetCacheButtonLoading"
/>
</div>
<div class="content-list pipelines">
<loading-icon
- label="Loading Pipelines"
+ v-if="stateToRender === $options.stateMap.loading"
+ :label="s__('Pipelines|Loading Pipelines')"
size="3"
- v-if="isLoading"
class="prepend-top-20"
/>
<empty-state
- v-if="shouldRenderEmptyState"
+ v-else-if="stateToRender === $options.stateMap.emptyState"
:help-page-path="helpPagePath"
:empty-state-svg-path="emptyStateSvgPath"
+ :can-set-ci="canCreatePipeline"
/>
- <error-state
- v-if="shouldRenderErrorState"
- :error-state-svg-path="errorStateSvgPath"
+ <svg-blank-state
+ v-else-if="stateToRender === $options.stateMap.error"
+ :svg-path="errorStateSvgPath"
+ :message="s__(`Pipelines|There was an error fetching the pipelines.
+ Try again in a few moments or contact your support team.`)"
/>
- <div
- class="blank-state-row"
- v-if="shouldRenderNoPipelinesMessage"
- >
- <div class="blank-state-center">
- <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
- </div>
- </div>
+ <svg-blank-state
+ v-else-if="stateToRender === $options.stateMap.emptyTab"
+ :svg-path="noPipelinesSvgPath"
+ :message="emptyTabMessage"
+ />
<div
class="table-holder"
- v-if="shouldRenderTable"
+ v-else-if="stateToRender === $options.stateMap.tableList"
>
<pipelines-table-component
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 2ba59051773..4cbd67e0372 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -316,7 +316,7 @@
v-if="pipeline.flags.cancelable"
:endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove"
- title="Cancel"
+ title="Stop"
icon="close"
:pipeline-id="pipeline.id"
data-toggle="modal"
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 50bdf80c3e3..522a4277bd7 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,23 +1,19 @@
import Visibility from 'visibilityjs';
+import { __ } from '../../locale';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
-import emptyState from '../components/empty_state.vue';
-import errorState from '../components/error_state.vue';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-import pipelinesTableComponent from '../components/pipelines_table.vue';
+import EmptyState from '../components/empty_state.vue';
+import SvgBlankState from '../components/blank_state.vue';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub';
export default {
components: {
- pipelinesTableComponent,
- errorState,
- emptyState,
- loadingIcon,
- },
- computed: {
- shouldRenderErrorState() {
- return this.hasError && !this.isLoading;
- },
+ PipelinesTableComponent,
+ SvgBlankState,
+ EmptyState,
+ LoadingIcon,
},
data() {
return {
@@ -55,12 +51,10 @@ export default {
}
});
- eventHub.$on('refreshPipelines', this.fetchPipelines);
eventHub.$on('postAction', this.postAction);
},
beforeDestroy() {
- eventHub.$off('refreshPipelines');
- eventHub.$on('postAction', this.postAction);
+ eventHub.$off('postAction', this.postAction);
},
destroyed() {
this.poll.stop();
@@ -85,6 +79,7 @@ export default {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
+ this.hasMadeRequest = true;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
@@ -95,8 +90,8 @@ export default {
},
postAction(endpoint) {
this.service.postAction(endpoint)
- .then(() => eventHub.$emit('refreshPipelines'))
- .catch(() => new Flash('An error occurred while making the request.'));
+ .then(() => this.fetchPipelines())
+ .catch(() => Flash(__('An error occurred while making the request.')));
},
},
};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 705a60b3ba2..6b26708148c 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -9,7 +9,7 @@ import eventHub from './event_hub';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
@@ -70,4 +70,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
-});
+};
diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js
deleted file mode 100644
index ab5596e70f0..00000000000
--- a/app/assets/javascripts/pipelines/pipelines_bundle.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Vue from 'vue';
-import PipelinesStore from './stores/pipelines_store';
-import pipelinesComponent from './components/pipelines.vue';
-import Translate from '../vue_shared/translate';
-
-Vue.use(Translate);
-
-document.addEventListener('DOMContentLoaded', () => new Vue({
- el: '#pipelines-list-vue',
- components: {
- pipelinesComponent,
- },
- data() {
- const store = new PipelinesStore();
-
- return {
- store,
- };
- },
- render(createElement) {
- return createElement('pipelines-component', {
- props: {
- store: this.store,
- },
- });
- },
-}));
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index e2285494e62..47736fc5f42 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
import VueResource from 'vue-resource';
+import '../../vue_shared/vue_resource_interceptor';
Vue.use(VueResource);
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 930f0fb381e..a811781853b 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,103 +1,85 @@
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
import Cookies from 'js-cookie';
-import { getPagePath } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import flash from '../flash';
-((global) => {
- class Profile {
- constructor({ form } = {}) {
- this.onSubmitForm = this.onSubmitForm.bind(this);
- this.form = form || $('.edit-user');
- this.newRepoActivated = Cookies.get('new_repo');
- this.setRepoRadio();
- this.bindEvents();
- this.initAvatarGlCrop();
- }
-
- initAvatarGlCrop() {
- const cropOpts = {
- filename: '.js-avatar-filename',
- previewImage: '.avatar-image .avatar',
- modalCrop: '.modal-profile-crop',
- pickImageEl: '.js-choose-user-avatar-button',
- uploadImageBtn: '.js-upload-user-avatar',
- modalCropImg: '.modal-profile-crop-image'
- };
- this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
- }
+export default class Profile {
+ constructor({ form } = {}) {
+ this.onSubmitForm = this.onSubmitForm.bind(this);
+ this.form = form || $('.edit-user');
+ this.newRepoActivated = Cookies.get('new_repo');
+ this.setRepoRadio();
+ this.bindEvents();
+ this.initAvatarGlCrop();
+ }
- bindEvents() {
- $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
- $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
- $('#user_notification_email').on('change', this.submitForm);
- $('#user_notified_of_own_activity').on('change', this.submitForm);
- this.form.on('submit', this.onSubmitForm);
- }
+ initAvatarGlCrop() {
+ const cropOpts = {
+ filename: '.js-avatar-filename',
+ previewImage: '.avatar-image .avatar',
+ modalCrop: '.modal-profile-crop',
+ pickImageEl: '.js-choose-user-avatar-button',
+ uploadImageBtn: '.js-upload-user-avatar',
+ modalCropImg: '.modal-profile-crop-image'
+ };
+ this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
+ }
- submitForm() {
- return $(this).parents('form').submit();
- }
+ bindEvents() {
+ $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
+ $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
+ $('#user_notification_email').on('change', this.submitForm);
+ $('#user_notified_of_own_activity').on('change', this.submitForm);
+ this.form.on('submit', this.onSubmitForm);
+ }
- onSubmitForm(e) {
- e.preventDefault();
- return this.saveForm();
- }
+ submitForm() {
+ return $(this).parents('form').submit();
+ }
- saveForm() {
- const self = this;
- const formData = new FormData(this.form[0]);
- const avatarBlob = this.avatarGlCrop.getBlob();
+ onSubmitForm(e) {
+ e.preventDefault();
+ return this.saveForm();
+ }
- if (avatarBlob != null) {
- formData.append('user[avatar]', avatarBlob, 'avatar.png');
- }
+ saveForm() {
+ const self = this;
+ const formData = new FormData(this.form[0]);
+ const avatarBlob = this.avatarGlCrop.getBlob();
- axios({
- method: this.form.attr('method'),
- url: this.form.attr('action'),
- data: formData,
- })
- .then(({ data }) => flash(data.message, 'notice'))
- .then(() => {
- window.scrollTo(0, 0);
- // Enable submit button after requests ends
- self.form.find(':input[disabled]').enable();
- })
- .catch(error => flash(error.message));
+ if (avatarBlob != null) {
+ formData.append('user[avatar]', avatarBlob, 'avatar.png');
}
- setNewRepoCookie() {
- if (this.value === 'off') {
- Cookies.remove('new_repo');
- } else {
- Cookies.set('new_repo', true, { expires_in: 365 });
- }
- }
+ axios({
+ method: this.form.attr('method'),
+ url: this.form.attr('action'),
+ data: formData,
+ })
+ .then(({ data }) => flash(data.message, 'notice'))
+ .then(() => {
+ window.scrollTo(0, 0);
+ // Enable submit button after requests ends
+ self.form.find(':input[disabled]').enable();
+ })
+ .catch(error => flash(error.message));
+ }
- setRepoRadio() {
- const multiEditRadios = $('input[name="user[multi_file]"]');
- if (this.newRepoActivated || this.newRepoActivated === 'true') {
- multiEditRadios.filter('[value=on]').prop('checked', true);
- } else {
- multiEditRadios.filter('[value=off]').prop('checked', true);
- }
+ setNewRepoCookie() {
+ if (this.value === 'off') {
+ Cookies.remove('new_repo');
+ } else {
+ Cookies.set('new_repo', true, { expires_in: 365 });
}
}
- $(function() {
- $(document).on('input.ssh_key', '#key_key', function() {
- const $title = $('#key_title');
- const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
-
- // Extract the SSH Key title from its comment
- if (comment && comment.length > 1) {
- return $title.val(comment[1]).change();
- }
- });
- if (getPagePath() === 'profiles') {
- return new Profile();
+ setRepoRadio() {
+ const multiEditRadios = $('input[name="user[multi_file]"]');
+ if (this.newRepoActivated || this.newRepoActivated === 'true') {
+ multiEditRadios.filter('[value=on]').prop('checked', true);
+ } else {
+ multiEditRadios.filter('[value=off]').prop('checked', true);
}
- });
-})(window.gl || (window.gl = {}));
+ }
+}
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
deleted file mode 100644
index ff35a9bcb83..00000000000
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import './gl_crop';
-import './profile';
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index e8126ac573d..03bb281395a 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -1,3 +1,5 @@
+import _ from 'underscore';
+import { s__, n__, sprintf } from '~/locale';
import axios from '../lib/utils/axios_utils';
import PANEL_STATE from './constants';
import { backOff } from '../lib/utils/common_utils';
@@ -20,6 +22,7 @@ export default class PrometheusMetrics {
this.$missingEnvVarMetricsList = this.$missingEnvVarPanel.find('.js-missing-var-metrics-list');
this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('activeMetrics');
+ this.helpMetricsPath = this.$monitoredMetricsPanel.data('metrics-help-path');
this.$panelToggle.on('click', e => this.handlePanelToggle(e));
}
@@ -59,23 +62,39 @@ export default class PrometheusMetrics {
populateActiveMetrics(metrics) {
let totalMonitoredMetrics = 0;
let totalMissingEnvVarMetrics = 0;
+ let totalExporters = 0;
metrics.forEach((metric) => {
- this.$monitoredMetricsList.append(`<li>${metric.group}<span class="badge">${metric.active_metrics}</span></li>`);
- totalMonitoredMetrics += metric.active_metrics;
- if (metric.metrics_missing_requirements > 0) {
- this.$missingEnvVarMetricsList.append(`<li>${metric.group}</li>`);
- totalMissingEnvVarMetrics += 1;
+ if (metric.active_metrics > 0) {
+ totalExporters += 1;
+ this.$monitoredMetricsList.append(`<li>${_.escape(metric.group)}<span class="badge">${_.escape(metric.active_metrics)}</span></li>`);
+ totalMonitoredMetrics += metric.active_metrics;
+ if (metric.metrics_missing_requirements > 0) {
+ this.$missingEnvVarMetricsList.append(`<li>${_.escape(metric.group)}</li>`);
+ totalMissingEnvVarMetrics += 1;
+ }
}
});
- this.$monitoredMetricsCount.text(totalMonitoredMetrics);
- this.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
+ if (totalMonitoredMetrics === 0) {
+ const emptyCommonMetricsText = sprintf(s__('PrometheusService|<p class="text-tertiary">No <a href="%{docsUrl}">common metrics</a> were found</p>'), {
+ docsUrl: this.helpMetricsPath,
+ }, false);
+ this.$monitoredMetricsEmpty.empty();
+ this.$monitoredMetricsEmpty.append(emptyCommonMetricsText);
+ this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
+ } else {
+ const metricsCountText = sprintf(s__('PrometheusService|%{exporters} with %{metrics} were found'), {
+ exporters: n__('%d exporter', '%d exporters', totalExporters),
+ metrics: n__('%d metric', '%d metrics', totalMonitoredMetrics),
+ });
+ this.$monitoredMetricsCount.text(metricsCountText);
+ this.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
- if (totalMissingEnvVarMetrics > 0) {
- this.$missingEnvVarPanel.removeClass('hidden');
- this.$missingEnvVarPanel.find('.flash-container').off('click');
- this.$missingEnvVarMetricCount.text(totalMissingEnvVarMetrics);
+ if (totalMissingEnvVarMetrics > 0) {
+ this.$missingEnvVarPanel.removeClass('hidden');
+ this.$missingEnvVarMetricCount.text(totalMissingEnvVarMetrics);
+ }
}
}
@@ -97,15 +116,15 @@ export default class PrometheusMetrics {
})
.catch(stop);
})
- .then((res) => {
- if (res && res.data && res.data.length) {
- this.populateActiveMetrics(res.data);
- } else {
+ .then((res) => {
+ if (res && res.data && res.data.length) {
+ this.populateActiveMetrics(res.data);
+ } else {
+ this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
+ }
+ })
+ .catch(() => {
this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
- }
- })
- .catch(() => {
- this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
- });
+ });
}
}
diff --git a/app/assets/javascripts/protected_branches/index.js b/app/assets/javascripts/protected_branches/index.js
deleted file mode 100644
index c9e7af127d2..00000000000
--- a/app/assets/javascripts/protected_branches/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-import ProtectedBranchCreate from './protected_branch_create';
-import ProtectedBranchEditList from './protected_branch_edit_list';
-
-$(() => {
- const protectedBranchCreate = new ProtectedBranchCreate();
- const protectedBranchEditList = new ProtectedBranchEditList();
-});
diff --git a/app/assets/javascripts/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js
deleted file mode 100644
index b1618e24e49..00000000000
--- a/app/assets/javascripts/protected_tags/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-import ProtectedTagCreate from './protected_tag_create';
-import ProtectedTagEditList from './protected_tag_edit_list';
-
-$(() => {
- const protectedtTagCreate = new ProtectedTagCreate();
- const protectedtTagEditList = new ProtectedTagEditList();
-});
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index b4906ba4ee5..a03180e80e6 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -86,6 +86,7 @@
v-if="repo.location"
:text="clipboardText"
:title="repo.location"
+ css-class="btn-default btn-transparent btn-clipboard"
/>
<div class="controls hidden-xs pull-right">
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index bef850eddc0..ee4eb3581f3 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -90,6 +90,7 @@
v-if="item.location"
:title="item.location"
:text="clipboardText(item.location)"
+ css-class="btn-default btn-transparent btn-clipboard"
/>
</td>
<td>
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
index d8edff73f72..6fb125192b2 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/index.js
@@ -4,7 +4,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => new Vue({
+export default () => new Vue({
el: '#js-vue-registry-images',
components: {
registryApp,
@@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
-}));
+});
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.js b/app/assets/javascripts/sidebar/components/assignees/assignees.js
deleted file mode 100644
index 643877b9d47..00000000000
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.js
+++ /dev/null
@@ -1,224 +0,0 @@
-export default {
- name: 'Assignees',
- data() {
- return {
- defaultRenderCount: 5,
- defaultMaxCounter: 99,
- showLess: true,
- };
- },
- props: {
- rootPath: {
- type: String,
- required: true,
- },
- users: {
- type: Array,
- required: true,
- },
- editable: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- firstUser() {
- return this.users[0];
- },
- hasMoreThanTwoAssignees() {
- return this.users.length > 2;
- },
- hasMoreThanOneAssignee() {
- return this.users.length > 1;
- },
- hasAssignees() {
- return this.users.length > 0;
- },
- hasNoUsers() {
- return !this.users.length;
- },
- hasOneUser() {
- return this.users.length === 1;
- },
- renderShowMoreSection() {
- return this.users.length > this.defaultRenderCount;
- },
- numberOfHiddenAssignees() {
- return this.users.length - this.defaultRenderCount;
- },
- isHiddenAssignees() {
- return this.numberOfHiddenAssignees > 0;
- },
- hiddenAssigneesLabel() {
- return `+ ${this.numberOfHiddenAssignees} more`;
- },
- collapsedTooltipTitle() {
- const maxRender = Math.min(this.defaultRenderCount, this.users.length);
- const renderUsers = this.users.slice(0, maxRender);
- const names = renderUsers.map(u => u.name);
-
- if (this.users.length > maxRender) {
- names.push(`+ ${this.users.length - maxRender} more`);
- }
-
- return names.join(', ');
- },
- sidebarAvatarCounter() {
- let counter = `+${this.users.length - 1}`;
-
- if (this.users.length > this.defaultMaxCounter) {
- counter = `${this.defaultMaxCounter}+`;
- }
-
- return counter;
- },
- },
- methods: {
- assignSelf() {
- this.$emit('assign-self');
- },
- toggleShowLess() {
- this.showLess = !this.showLess;
- },
- renderAssignee(index) {
- return !this.showLess || (index < this.defaultRenderCount && this.showLess);
- },
- avatarUrl(user) {
- return user.avatar || user.avatar_url || gon.default_avatar_url;
- },
- assigneeUrl(user) {
- return `${this.rootPath}${user.username}`;
- },
- assigneeAlt(user) {
- return `${user.name}'s avatar`;
- },
- assigneeUsername(user) {
- return `@${user.username}`;
- },
- shouldRenderCollapsedAssignee(index) {
- const firstTwo = this.users.length <= 2 && index <= 2;
-
- return index === 0 || firstTwo;
- },
- },
- template: `
- <div>
- <div
- class="sidebar-collapsed-icon sidebar-collapsed-user"
- :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
- data-container="body"
- data-placement="left"
- :title="collapsedTooltipTitle"
- >
- <i
- v-if="hasNoUsers"
- aria-label="No Assignee"
- class="fa fa-user"
- />
- <button
- type="button"
- class="btn-link"
- v-for="(user, index) in users"
- v-if="shouldRenderCollapsedAssignee(index)"
- >
- <img
- width="24"
- class="avatar avatar-inline s24"
- :alt="assigneeAlt(user)"
- :src="avatarUrl(user)"
- />
- <span class="author">
- {{ user.name }}
- </span>
- </button>
- <button
- v-if="hasMoreThanTwoAssignees"
- class="btn-link"
- type="button"
- >
- <span
- class="avatar-counter sidebar-avatar-counter"
- >
- {{ sidebarAvatarCounter }}
- </span>
- </button>
- </div>
- <div class="value hide-collapsed">
- <template v-if="hasNoUsers">
- <span class="assign-yourself no-value">
- No assignee
- <template v-if="editable">
- -
- <button
- type="button"
- class="btn-link"
- @click="assignSelf"
- >
- assign yourself
- </button>
- </template>
- </span>
- </template>
- <template v-else-if="hasOneUser">
- <a
- class="author_link bold"
- :href="assigneeUrl(firstUser)"
- >
- <img
- width="32"
- class="avatar avatar-inline s32"
- :alt="assigneeAlt(firstUser)"
- :src="avatarUrl(firstUser)"
- />
- <span class="author">
- {{ firstUser.name }}
- </span>
- <span class="username">
- {{ assigneeUsername(firstUser) }}
- </span>
- </a>
- </template>
- <template v-else>
- <div class="user-list">
- <div
- class="user-item"
- v-for="(user, index) in users"
- v-if="renderAssignee(index)"
- >
- <a
- class="user-link has-tooltip"
- data-placement="bottom"
- :href="assigneeUrl(user)"
- :data-title="user.name"
- >
- <img
- width="32"
- class="avatar avatar-inline s32"
- :alt="assigneeAlt(user)"
- :src="avatarUrl(user)"
- />
- </a>
- </div>
- </div>
- <div
- v-if="renderShowMoreSection"
- class="user-list-more"
- >
- <button
- type="button"
- class="btn-link"
- @click="toggleShowLess"
- >
- <template v-if="showLess">
- {{ hiddenAssigneesLabel }}
- </template>
- <template v-else>
- - show less
- </template>
- </button>
- </div>
- </template>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
new file mode 100644
index 00000000000..1e7f46454bf
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -0,0 +1,232 @@
+<script>
+export default {
+ name: 'Assignees',
+ props: {
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ users: {
+ type: Array,
+ required: true,
+ },
+ editable: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ defaultRenderCount: 5,
+ defaultMaxCounter: 99,
+ showLess: true,
+ };
+ },
+ computed: {
+ firstUser() {
+ return this.users[0];
+ },
+ hasMoreThanTwoAssignees() {
+ return this.users.length > 2;
+ },
+ hasMoreThanOneAssignee() {
+ return this.users.length > 1;
+ },
+ hasAssignees() {
+ return this.users.length > 0;
+ },
+ hasNoUsers() {
+ return !this.users.length;
+ },
+ hasOneUser() {
+ return this.users.length === 1;
+ },
+ renderShowMoreSection() {
+ return this.users.length > this.defaultRenderCount;
+ },
+ numberOfHiddenAssignees() {
+ return this.users.length - this.defaultRenderCount;
+ },
+ isHiddenAssignees() {
+ return this.numberOfHiddenAssignees > 0;
+ },
+ hiddenAssigneesLabel() {
+ return `+ ${this.numberOfHiddenAssignees} more`;
+ },
+ collapsedTooltipTitle() {
+ const maxRender = Math.min(this.defaultRenderCount, this.users.length);
+ const renderUsers = this.users.slice(0, maxRender);
+ const names = renderUsers.map(u => u.name);
+
+ if (this.users.length > maxRender) {
+ names.push(`+ ${this.users.length - maxRender} more`);
+ }
+
+ return names.join(', ');
+ },
+ sidebarAvatarCounter() {
+ let counter = `+${this.users.length - 1}`;
+
+ if (this.users.length > this.defaultMaxCounter) {
+ counter = `${this.defaultMaxCounter}+`;
+ }
+
+ return counter;
+ },
+ },
+ methods: {
+ assignSelf() {
+ this.$emit('assign-self');
+ },
+ toggleShowLess() {
+ this.showLess = !this.showLess;
+ },
+ renderAssignee(index) {
+ return !this.showLess || (index < this.defaultRenderCount && this.showLess);
+ },
+ avatarUrl(user) {
+ return user.avatar || user.avatar_url || gon.default_avatar_url;
+ },
+ assigneeUrl(user) {
+ return `${this.rootPath}${user.username}`;
+ },
+ assigneeAlt(user) {
+ return `${user.name}'s avatar`;
+ },
+ assigneeUsername(user) {
+ return `@${user.username}`;
+ },
+ shouldRenderCollapsedAssignee(index) {
+ const firstTwo = this.users.length <= 2 && index <= 2;
+
+ return index === 0 || firstTwo;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="sidebar-collapsed-icon sidebar-collapsed-user"
+ :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
+ data-container="body"
+ data-placement="left"
+ :title="collapsedTooltipTitle"
+ >
+ <i
+ v-if="hasNoUsers"
+ aria-label="No Assignee"
+ class="fa fa-user"
+ >
+ </i>
+ <button
+ type="button"
+ class="btn-link"
+ v-for="(user, index) in users"
+ v-if="shouldRenderCollapsedAssignee(index)"
+ :key="user.id"
+ >
+ <img
+ width="24"
+ class="avatar avatar-inline s24"
+ :alt="assigneeAlt(user)"
+ :src="avatarUrl(user)"
+ />
+ <span class="author">
+ {{ user.name }}
+ </span>
+ </button>
+ <button
+ v-if="hasMoreThanTwoAssignees"
+ class="btn-link"
+ type="button"
+ >
+ <span
+ class="avatar-counter sidebar-avatar-counter"
+ >
+ {{ sidebarAvatarCounter }}
+ </span>
+ </button>
+ </div>
+ <div class="value hide-collapsed">
+ <template v-if="hasNoUsers">
+ <span class="assign-yourself no-value">
+ No assignee
+ <template v-if="editable">
+ -
+ <button
+ type="button"
+ class="btn-link"
+ @click="assignSelf"
+ >
+ assign yourself
+ </button>
+ </template>
+ </span>
+ </template>
+ <template v-else-if="hasOneUser">
+ <a
+ class="author_link bold"
+ :href="assigneeUrl(firstUser)"
+ >
+ <img
+ width="32"
+ class="avatar avatar-inline s32"
+ :alt="assigneeAlt(firstUser)"
+ :src="avatarUrl(firstUser)"
+ />
+ <span class="author">
+ {{ firstUser.name }}
+ </span>
+ <span class="username">
+ {{ assigneeUsername(firstUser) }}
+ </span>
+ </a>
+ </template>
+ <template v-else>
+ <div class="user-list">
+ <div
+ class="user-item"
+ v-for="(user, index) in users"
+ v-if="renderAssignee(index)"
+ :key="user.id"
+ >
+ <a
+ class="user-link has-tooltip"
+ data-container="body"
+ data-placement="bottom"
+ :href="assigneeUrl(user)"
+ :data-title="user.name"
+ >
+ <img
+ width="32"
+ class="avatar avatar-inline s32"
+ :alt="assigneeAlt(user)"
+ :src="avatarUrl(user)"
+ />
+ </a>
+ </div>
+ </div>
+ <div
+ v-if="renderShowMoreSection"
+ class="user-list-more"
+ >
+ <button
+ type="button"
+ class="btn-link"
+ @click="toggleShowLess"
+ >
+ <template v-if="showLess">
+ {{ hiddenAssigneesLabel }}
+ </template>
+ <template v-else>
+ - show less
+ </template>
+ </button>
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
+
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 9e47039d920..b58e04b5e60 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,16 +1,15 @@
+<script>
import Flash from '../../../flash';
import AssigneeTitle from './assignee_title';
-import Assignees from './assignees';
+import Assignees from './assignees.vue';
import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
- data() {
- return {
- store: new Store(),
- loading: false,
- };
+ components: {
+ AssigneeTitle,
+ Assignees,
},
props: {
mediator: {
@@ -27,9 +26,28 @@ export default {
default: false,
},
},
- components: {
- 'assignee-title': AssigneeTitle,
- assignees: Assignees,
+ data() {
+ return {
+ store: new Store(),
+ loading: false,
+ };
+ },
+ created() {
+ this.removeAssignee = this.store.removeAssignee.bind(this.store);
+ this.addAssignee = this.store.addAssignee.bind(this.store);
+ this.removeAllAssignees = this.store.removeAllAssignees.bind(this.store);
+
+ // Get events from glDropdown
+ eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
+ eventHub.$on('sidebar.addAssignee', this.addAssignee);
+ eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
+ eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
+ },
+ beforeDestroy() {
+ eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
+ eventHub.$off('sidebar.addAssignee', this.addAssignee);
+ eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
+ eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
},
methods: {
assignSelf() {
@@ -54,39 +72,24 @@ export default {
});
},
},
- created() {
- this.removeAssignee = this.store.removeAssignee.bind(this.store);
- this.addAssignee = this.store.addAssignee.bind(this.store);
- this.removeAllAssignees = this.store.removeAllAssignees.bind(this.store);
-
- // Get events from glDropdown
- eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
- eventHub.$on('sidebar.addAssignee', this.addAssignee);
- eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
- eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
- },
- beforeDestroy() {
- eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
- eventHub.$off('sidebar.addAssignee', this.addAssignee);
- eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
- eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
- },
- template: `
- <div>
- <assignee-title
- :number-of-assignees="store.assignees.length"
- :loading="loading || store.isFetching.assignees"
- :editable="store.editable"
- :show-toggle="!signedIn"
- />
- <assignees
- v-if="!store.isFetching.assignees"
- class="value"
- :root-path="store.rootPath"
- :users="store.assignees"
- :editable="store.editable"
- @assign-self="assignSelf"
- />
- </div>
- `,
};
+</script>
+
+<template>
+ <div>
+ <assignee-title
+ :number-of-assignees="store.assignees.length"
+ :loading="loading || store.isFetching.assignees"
+ :editable="store.editable"
+ :show-toggle="!signedIn"
+ />
+ <assignees
+ v-if="!store.isFetching.assignees"
+ class="value"
+ :root-path="store.rootPath"
+ :users="store.assignees"
+ :editable="store.editable"
+ @assign-self="assignSelf"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 56cc78ca0ca..ef748f18301 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
-import SidebarAssignees from './components/assignees/sidebar_assignees';
+import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 04c39d7b6b5..377846db70e 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -1,13 +1,9 @@
import Mediator from './sidebar_mediator';
import { mountSidebar, getSidebarOptions } from './mount_sidebar';
-function domContentLoaded() {
+export default () => {
const mediator = new Mediator(getSidebarOptions());
mediator.fetch();
mountSidebar(mediator);
-}
-
-document.addEventListener('DOMContentLoaded', domContentLoaded);
-
-export default domContentLoaded;
+};
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index a98403f4cf2..ce0fd3f6ff8 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,12 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */
-(function() {
- $(function() {
- var editor = ace.edit("editor");
+export default () => {
+ const editor = ace.edit('editor');
- $(".snippet-form-holder form").on('submit', function() {
- $(".snippet-file-content").val(editor.getValue());
- });
+ $('.snippet-form-holder form').on('submit', () => {
+ $('.snippet-file-content').val(editor.getValue());
});
-}).call(window);
+};
diff --git a/app/assets/javascripts/sortable/sortable_config.js b/app/assets/javascripts/sortable/sortable_config.js
new file mode 100644
index 00000000000..43ef5d66422
--- /dev/null
+++ b/app/assets/javascripts/sortable/sortable_config.js
@@ -0,0 +1,7 @@
+export default {
+ animation: 200,
+ forceFallback: true,
+ fallbackClass: 'is-dragging',
+ fallbackOnBody: true,
+ ghostClass: 'is-ghost',
+};
diff --git a/app/assets/javascripts/terminal/terminal_bundle.js b/app/assets/javascripts/terminal/index.js
index 134522ef961..1a75e072c4e 100644
--- a/app/assets/javascripts/terminal/terminal_bundle.js
+++ b/app/assets/javascripts/terminal/index.js
@@ -6,4 +6,4 @@ import './terminal';
window.Terminal = Terminal;
-$(() => new gl.Terminal({ selector: '#terminal' }));
+export default () => new gl.Terminal({ selector: '#terminal' });
diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js
index 6b9422b1816..904b0093f7b 100644
--- a/app/assets/javascripts/terminal/terminal.js
+++ b/app/assets/javascripts/terminal/terminal.js
@@ -6,8 +6,14 @@
constructor(options) {
this.options = options || {};
- this.options.cursorBlink = options.cursorBlink || true;
- this.options.screenKeys = options.screenKeys || true;
+ if (!Object.prototype.hasOwnProperty.call(this.options, 'cursorBlink')) {
+ this.options.cursorBlink = true;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(this.options, 'screenKeys')) {
+ this.options.screenKeys = true;
+ }
+
this.container = document.querySelector(options.selector);
this.setSocketUrl();
diff --git a/app/assets/javascripts/test.js b/app/assets/javascripts/test.js
deleted file mode 100644
index c4c7918a68f..00000000000
--- a/app/assets/javascripts/test.js
+++ /dev/null
@@ -1 +0,0 @@
-$.fx.off = true;
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index a3cc04e35fe..fd42f9c3baa 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,7 +1,5 @@
-/* eslint-disable func-names, wrap-iife */
-/* global u2f */
import _ from 'underscore';
-import isU2FSupported from './util';
+import importU2FLibrary from './util';
import U2FError from './error';
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
@@ -10,6 +8,7 @@ import U2FError from './error';
// State Flow #2: setup -> in_progress -> error -> setup
export default class U2FAuthenticate {
constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
+ this.u2fUtils = null;
this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderAuthenticated = this.renderAuthenticated.bind(this);
@@ -50,22 +49,23 @@ export default class U2FAuthenticate {
}
start() {
- if (isU2FSupported()) {
- return this.renderInProgress();
- }
- return this.renderNotSupported();
+ return importU2FLibrary()
+ .then((utils) => {
+ this.u2fUtils = utils;
+ this.renderInProgress();
+ })
+ .catch(() => this.renderNotSupported());
}
authenticate() {
- return u2f.sign(this.appId, this.challenge, this.signRequests, (function (_this) {
- return function (response) {
+ return this.u2fUtils.sign(this.appId, this.challenge, this.signRequests,
+ (response) => {
if (response.errorCode) {
const error = new U2FError(response.errorCode, 'authenticate');
- return _this.renderError(error);
+ return this.renderError(error);
}
- return _this.renderAuthenticated(JSON.stringify(response));
- };
- })(this), 10);
+ return this.renderAuthenticated(JSON.stringify(response));
+ }, 10);
}
renderTemplate(name, params) {
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index cc3f02e75f6..869fac658e8 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,8 +1,5 @@
-/* eslint-disable func-names, wrap-iife */
-/* global u2f */
-
import _ from 'underscore';
-import isU2FSupported from './util';
+import importU2FLibrary from './util';
import U2FError from './error';
// Register U2F (universal 2nd factor) devices for users to authenticate with.
@@ -11,6 +8,7 @@ import U2FError from './error';
// State Flow #2: setup -> in_progress -> error -> setup
export default class U2FRegister {
constructor(container, u2fParams) {
+ this.u2fUtils = null;
this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderRegistered = this.renderRegistered.bind(this);
@@ -34,22 +32,23 @@ export default class U2FRegister {
}
start() {
- if (isU2FSupported()) {
- return this.renderSetup();
- }
- return this.renderNotSupported();
+ return importU2FLibrary()
+ .then((utils) => {
+ this.u2fUtils = utils;
+ this.renderSetup();
+ })
+ .catch(() => this.renderNotSupported());
}
register() {
- return u2f.register(this.appId, this.registerRequests, this.signRequests, (function (_this) {
- return function (response) {
+ return this.u2fUtils.register(this.appId, this.registerRequests, this.signRequests,
+ (response) => {
if (response.errorCode) {
const error = new U2FError(response.errorCode, 'register');
- return _this.renderError(error);
+ return this.renderError(error);
}
- return _this.renderRegistered(JSON.stringify(response));
- };
- })(this), 10);
+ return this.renderRegistered(JSON.stringify(response));
+ }, 10);
}
renderTemplate(name, params) {
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 9771ff935c2..5778f00332d 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,3 +1,41 @@
-export default function isU2FSupported() {
- return window.u2f;
+function isOpera(userAgent) {
+ return userAgent.indexOf('Opera') >= 0 || userAgent.indexOf('OPR') >= 0;
+}
+
+function getOperaVersion(userAgent) {
+ const match = userAgent.match(/OPR[^0-9]*([0-9]+)[^0-9]+/);
+ return match ? parseInt(match[1], 10) : false;
+}
+
+function isChrome(userAgent) {
+ return userAgent.indexOf('Chrom') >= 0 && !isOpera(userAgent);
+}
+
+function getChromeVersion(userAgent) {
+ const match = userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
+ return match ? parseInt(match[1], 10) : false;
+}
+
+export function canInjectU2fApi(userAgent) {
+ const isSupportedChrome = isChrome(userAgent) && getChromeVersion(userAgent) >= 41;
+ const isSupportedOpera = isOpera(userAgent) && getOperaVersion(userAgent) >= 40;
+ const isMobile = (
+ userAgent.indexOf('droid') >= 0 ||
+ userAgent.indexOf('CriOS') >= 0 ||
+ /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent)
+ );
+ return (isSupportedChrome || isSupportedOpera) && !isMobile;
+}
+
+export default function importU2FLibrary() {
+ if (window.u2f) {
+ return Promise.resolve(window.u2f);
+ }
+
+ const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
+ if (canInjectU2fApi(userAgent) || (gon && gon.test_env)) {
+ return import(/* webpackMode: "eager" */ 'vendor/u2f').then(() => window.u2f);
+ }
+
+ return Promise.reject();
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
index 69e70ba1dd6..a16f9055a6d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
@@ -1,11 +1,15 @@
+<script>
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';
+import MemoryGraph from '../../vue_shared/components/memory_graph.vue';
import MRWidgetService from '../services/mr_widget_service';
export default {
name: 'MemoryUsage',
+ components: {
+ MemoryGraph,
+ },
props: {
metricsUrl: {
type: String,
@@ -28,9 +32,6 @@ export default {
backOffRequestCounter: 0,
};
},
- components: {
- 'mr-memory-graph': MemoryGraph,
- },
computed: {
shouldShowLoading() {
return this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
@@ -57,6 +58,10 @@ export default {
return 'unchanged';
},
},
+ mounted() {
+ this.loadingMetrics = true;
+ this.loadMetrics();
+ },
methods: {
getMegabytes(bytesString) {
const valueInBytes = Number(bytesString).toFixed(2);
@@ -114,40 +119,42 @@ export default {
});
},
},
- mounted() {
- this.loadingMetrics = true;
- this.loadMetrics();
- },
- template: `
- <div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
- <p
- v-if="shouldShowLoading"
- class="usage-info js-usage-info usage-info-loading">
- <i
- class="fa fa-spinner fa-spin usage-info-load-spinner"
- aria-hidden="true" />Loading deployment statistics
- </p>
- <p
- v-if="shouldShowMemoryGraph"
- class="usage-info js-usage-info">
- <a :href="metricsMonitoringUrl">Memory</a> usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
- </p>
- <p
- v-if="shouldShowLoadFailure"
- class="usage-info js-usage-info usage-info-failed">
- Failed to load deployment statistics
- </p>
- <p
- v-if="shouldShowMetricsUnavailable"
- class="usage-info js-usage-info usage-info-unavailable">
- Deployment statistics are not available currently
- </p>
- <mr-memory-graph
- v-if="shouldShowMemoryGraph"
- :metrics="memoryMetrics"
- :deploymentTime="deploymentTime"
- height="25"
- width="100" />
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
+ <p
+ v-if="shouldShowLoading"
+ class="usage-info js-usage-info usage-info-loading">
+ <i
+ class="fa fa-spinner fa-spin usage-info-load-spinner"
+ aria-hidden="true">
+ </i>Loading deployment statistics
+ </p>
+ <p
+ v-if="shouldShowMemoryGraph"
+ class="usage-info js-usage-info">
+ <a
+ :href="metricsMonitoringUrl"
+ >Memory</a> usage <b>{{ memoryChangeType }}</b> from {{ memoryFrom }}MB to {{ memoryTo }}MB
+ </p>
+ <p
+ v-if="shouldShowLoadFailure"
+ class="usage-info js-usage-info usage-info-failed">
+ Failed to load deployment statistics
+ </p>
+ <p
+ v-if="shouldShowMetricsUnavailable"
+ class="usage-info js-usage-info usage-info-unavailable">
+ Deployment statistics are not available currently
+ </p>
+ <memory-graph
+ v-if="shouldShowMemoryGraph"
+ :metrics="memoryMetrics"
+ :deployment-time="deploymentTime"
+ height="25"
+ width="100"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index d174a900f63..c7f992384c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -1,7 +1,7 @@
import { getTimeago } from '~/lib/utils/datetime_utility';
import { visitUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash';
-import MemoryUsage from './mr_widget_memory_usage';
+import MemoryUsage from './memory_usage.vue';
import StatusIcon from './mr_widget_status_icon.vue';
import MRWidgetService from '../services/mr_widget_service';
@@ -12,8 +12,8 @@ export default {
service: { type: Object, required: true },
},
components: {
- 'mr-widget-memory-usage': MemoryUsage,
- 'status-icon': StatusIcon,
+ MemoryUsage,
+ StatusIcon,
},
methods: {
formatDate(date) {
@@ -100,7 +100,7 @@ export default {
class="btn btn-default btn-xs">
Stop environment
</button>
- <mr-widget-memory-usage
+ <memory-usage
v-if="deployment.metrics_url"
:metrics-url="deployment.metrics_url"
:metrics-monitoring-url="deployment.metrics_monitoring_url"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 18a3787857d..3d886e7d628 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -67,6 +67,7 @@
<clipboard-button
:text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')"
+ css-class="btn-default btn-transparent btn-clipboard"
/>
{{ s__("mrWidget|into") }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue
new file mode 100644
index 00000000000..f0298f732ea
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue
@@ -0,0 +1,20 @@
+<script>
+ export default {
+ name: 'MRWidgetMaintainerEdit',
+ props: {
+ maintainerEditAllowed: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ };
+</script>
+
+<template>
+ <section class="mr-info-list mr-links">
+ <p v-if="maintainerEditAllowed">
+ {{ s__("mrWidget|Allows edits from maintainers") }}
+ </p>
+ </section>
+</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 109a302a172..54a98abf860 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,8 +1,8 @@
<script>
/* eslint-disable vue/require-default-prop */
- import pipelineStage from '../../pipelines/components/stage.vue';
- import ciIcon from '../../vue_shared/components/ci_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
+ import pipelineStage from '~/pipelines/components/stage.vue';
+ import ciIcon from '~/vue_shared/components/ci_icon.vue';
+ import icon from '~/vue_shared/components/icon.vue';
export default {
name: 'MRWidgetPipeline',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
new file mode 100644
index 00000000000..460437ceeff
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
@@ -0,0 +1,34 @@
+<script>
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import { __ } from '../../locale';
+
+ export default {
+ directives: {
+ tooltip,
+ },
+ created() {
+ this.removesBranchText = __('<strong>Removes</strong> source branch');
+ this.tooltipTitle = __('A user with write access to the source branch selected this option');
+ },
+ };
+</script>
+
+<template>
+ <p
+ v-once
+ class="mr-info-list mr-links source-branch-removal-status append-bottom-0"
+ >
+ <span
+ class="status-text"
+ v-html="removesBranchText"
+ >
+ </span>
+ <i
+ v-tooltip
+ class="fa fa-question-circle"
+ :title="tooltipTitle"
+ :aria-label="tooltipTitle"
+ >
+ </i>
+ </p>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index 162f048aac7..3c781ccddc8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -93,7 +93,7 @@ export default {
|| this.mr.preventMerge);
},
isRemoveSourceBranchButtonDisabled() {
- return this.isMergeButtonDisabled || !this.mr.canRemoveSourceBranch;
+ return this.isMergeButtonDisabled;
},
shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge } = this.mr;
@@ -282,7 +282,7 @@ export default {
</span>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls()">
- <label>
+ <label v-if="mr.canRemoveSourceBranch">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index edb3baa39e4..b867dd90a41 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -15,6 +15,7 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
+export { default as WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.vue';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
export { default as MergedState } from './components/states/mr_widget_merged.vue';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
@@ -39,7 +40,9 @@ export { default as MRWidgetStore } from './stores/mr_widget_store';
export { default as MRWidgetService } from './services/mr_widget_service';
export { default as eventHub } from './event_hub';
export { default as getStateKey } from './stores/get_state_key';
-export { default as mrWidgetOptions } from './mr_widget_options';
export { default as stateMaps } from './stores/state_maps';
export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge';
export { default as notify } from '../lib/utils/notify';
+export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue';
+
+export { default as mrWidgetOptions } from './mr_widget_options';
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 6b9918b65b0..69a9132a2da 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -6,7 +6,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
const vm = new Vue(mrWidgetOptions);
@@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => {
window.gl.mrWidget = {
checkStatus: vm.checkStatus,
};
-});
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 797f0f6ec0f..01365b70897 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -6,6 +6,7 @@ import {
WidgetMergeHelp,
WidgetPipeline,
WidgetDeployment,
+ WidgetMaintainerEdit,
WidgetRelatedLinks,
MergedState,
ClosedState,
@@ -32,6 +33,7 @@ import {
stateMaps,
SquashBeforeMerge,
notify,
+ SourceBranchRemovalStatus,
} from './dependencies';
import { setFavicon } from '../lib/utils/common_utils';
@@ -68,6 +70,9 @@ export default {
shouldRenderDeployments() {
return this.mr.deployments.length;
},
+ shouldRenderSourceBranchRemovalStatus() {
+ return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch;
+ },
},
methods: {
createService(store) {
@@ -211,6 +216,7 @@ export default {
'mr-widget-merge-help': WidgetMergeHelp,
'mr-widget-pipeline': WidgetPipeline,
'mr-widget-deployment': WidgetDeployment,
+ 'mr-widget-maintainer-edit': WidgetMaintainerEdit,
'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState,
'mr-widget-closed': ClosedState,
@@ -232,6 +238,7 @@ export default {
'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState,
+ SourceBranchRemovalStatus,
},
template: `
<div class="mr-state-widget prepend-top-default">
@@ -251,11 +258,15 @@ export default {
:is="componentName"
:mr="mr"
:service="service" />
+ <mr-widget-maintainer-edit
+ :maintainerEditAllowed="mr.maintainerEditAllowed" />
<mr-widget-related-links
v-if="shouldRenderRelatedLinks"
:state="mr.state"
- :related-links="mr.relatedLinks"
- />
+ :related-links="mr.relatedLinks" />
+ <source-branch-removal-status
+ v-if="shouldRenderSourceBranchRemovalStatus"
+ />
</div>
<div
class="mr-widget-footer"
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 9a750ce42bd..5d07bcf1bb9 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
@@ -76,6 +76,7 @@ export default class MergeRequestStore {
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
+ this.maintainerEditAllowed = data.allow_maintainer_to_push;
// Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index 31d9b9d9c48..cab126a7eca 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -1,8 +1,8 @@
<script>
- import tooltip from '../directives/tooltip';
/**
* Falls back to the code used in `copy_to_clipboard.js`
*/
+ import tooltip from '../directives/tooltip';
export default {
name: 'ClipboardButton',
@@ -28,6 +28,11 @@
required: false,
default: false,
},
+ cssClass: {
+ type: String,
+ required: false,
+ default: 'btn-default',
+ },
},
};
</script>
@@ -35,7 +40,8 @@
<template>
<button
type="button"
- class="btn btn-transparent btn-clipboard"
+ class="btn"
+ :class="cssClass"
:title="title"
:data-clipboard-text="text"
v-tooltip
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index 3595a9389e9..c943c8d98a4 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -39,7 +39,7 @@
@click="onClick">
...
</button>
- <span v-show="!isCollapsed">
+ <span v-if="!isCollapsed">
<slot name="expanded"></slot>
</span>
</span>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
index b48828ae81f..3d39b3ab173 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -11,14 +11,12 @@
default: false,
required: false,
},
-
isConfidential: {
type: Boolean,
default: false,
required: false,
},
},
-
computed: {
warningIcon() {
if (this.isConfidential) return 'eye-slash';
@@ -26,7 +24,6 @@
return '';
},
-
isLockedAndConfidential() {
return this.isConfidential && this.isLocked;
},
diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.js b/app/assets/javascripts/vue_shared/components/memory_graph.vue
index f37ef1a5ca3..b07f6b07afe 100644
--- a/app/assets/javascripts/vue_shared/components/memory_graph.js
+++ b/app/assets/javascripts/vue_shared/components/memory_graph.vue
@@ -1,3 +1,4 @@
+<script>
import { getTimeago } from '../../lib/utils/datetime_utility';
export default {
@@ -22,6 +23,9 @@ export default {
return `Deployed ${deployedSince}`;
},
},
+ mounted() {
+ this.renderGraph(this.deploymentTime, this.metrics);
+ },
methods: {
/**
* Returns metric value index in metrics array
@@ -103,15 +107,27 @@ export default {
this.dotY = dotY;
},
},
- mounted() {
- this.renderGraph(this.deploymentTime, this.metrics);
- },
- template: `
- <div class="memory-graph-container">
- <svg class="has-tooltip" :title="getFormattedMedian" :width="width" :height="height" xmlns="http://www.w3.org/2000/svg">
- <path :d="pathD" :viewBox="pathViewBox" />
- <circle r="1.5" :cx="dotX" :cy="dotY" tranform="translate(0 -1)" />
- </svg>
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="memory-graph-container">
+ <svg
+ class="has-tooltip"
+ :title="getFormattedMedian"
+ :width="width"
+ :height="height"
+ xmlns="http://www.w3.org/2000/svg">
+ <path
+ :d="pathD"
+ :viewBox="pathViewBox"
+ />
+ <circle
+ r="1.5"
+ :cx="dotX"
+ :cy="dotY"
+ tranform="translate(0 -1)"
+ />
+ </svg>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
new file mode 100644
index 00000000000..80e3db52cb0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -0,0 +1,24 @@
+<template>
+ <li class="timeline-entry note">
+ <div class="timeline-entry-inner">
+ <div class="timeline-icon">
+ </div>
+ <div class="timeline-content">
+ <div class="note-header"></div>
+ <div class="note-body">
+ <skeleton-loading-container />
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
+
+<script>
+ import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+
+ export default {
+ components: {
+ skeletonLoadingContainer,
+ },
+ };
+</script>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 1413dd69f24..3fcacd156c5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -14,6 +14,11 @@
collapsedCalendarIcon,
},
props: {
+ blockClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
collapsed: {
type: Boolean,
required: false,
@@ -91,7 +96,10 @@
</script>
<template>
- <div class="block">
+ <div
+ class="block"
+ :class="blockClass"
+ >
<div class="issuable-sidebar-header">
<toggle-sidebar
:collapsed="collapsed"
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
new file mode 100644
index 00000000000..c1dd4d42d9d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -0,0 +1,149 @@
+<script>
+import LabelsSelect from '~/labels_select';
+import LoadingIcon from '../../loading_icon.vue';
+
+import DropdownTitle from './dropdown_title.vue';
+import DropdownValue from './dropdown_value.vue';
+import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
+import DropdownButton from './dropdown_button.vue';
+import DropdownHiddenInput from './dropdown_hidden_input.vue';
+import DropdownHeader from './dropdown_header.vue';
+import DropdownSearchInput from './dropdown_search_input.vue';
+import DropdownFooter from './dropdown_footer.vue';
+import DropdownCreateLabel from './dropdown_create_label.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ DropdownTitle,
+ DropdownValue,
+ DropdownValueCollapsed,
+ DropdownButton,
+ DropdownHiddenInput,
+ DropdownHeader,
+ DropdownSearchInput,
+ DropdownFooter,
+ DropdownCreateLabel,
+ },
+ props: {
+ showCreate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ abilityName: {
+ type: String,
+ required: true,
+ },
+ context: {
+ type: Object,
+ required: true,
+ },
+ namespace: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ updatePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ labelsPath: {
+ type: String,
+ required: true,
+ },
+ labelsWebUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ labelFilterBasePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canEdit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ hiddenInputName() {
+ return this.showCreate ? `${this.abilityName}[label_names][]` : 'label_id[]';
+ },
+ },
+ mounted() {
+ this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
+ handleClick: this.handleClick,
+ });
+ },
+ methods: {
+ handleClick(label) {
+ this.$emit('onLabelClick', label);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block labels js-labels-block">
+ <dropdown-value-collapsed
+ v-if="showCreate"
+ :labels="context.labels"
+ />
+ <dropdown-title
+ :can-edit="canEdit"
+ />
+ <dropdown-value
+ :labels="context.labels"
+ :label-filter-base-path="labelFilterBasePath"
+ >
+ <slot></slot>
+ </dropdown-value>
+ <div
+ v-if="canEdit"
+ class="selectbox js-selectbox"
+ style="display: none;"
+ >
+ <dropdown-hidden-input
+ v-for="label in context.labels"
+ :key="label.id"
+ :name="hiddenInputName"
+ :label="label"
+ />
+ <div class="dropdown">
+ <dropdown-button
+ :ability-name="abilityName"
+ :field-name="hiddenInputName"
+ :update-path="updatePath"
+ :labels-path="labelsPath"
+ :namespace="namespace"
+ :labels="context.labels"
+ :show-extra-options="!showCreate"
+ />
+ <div
+ class="dropdown-menu dropdown-select dropdown-menu-paging
+dropdown-menu-labels dropdown-menu-selectable"
+ >
+ <div class="dropdown-page-one">
+ <dropdown-header v-if="showCreate" />
+ <dropdown-search-input/>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading">
+ <loading-icon />
+ </div>
+ <dropdown-footer
+ v-if="showCreate"
+ :labels-web-url="labelsWebUrl"
+ />
+ </div>
+ <dropdown-create-label
+ v-if="showCreate"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
new file mode 100644
index 00000000000..47497c1de98
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
@@ -0,0 +1,78 @@
+<script>
+import { __, s__, sprintf } from '~/locale';
+
+export default {
+ props: {
+ abilityName: {
+ type: String,
+ required: true,
+ },
+ fieldName: {
+ type: String,
+ required: true,
+ },
+ updatePath: {
+ type: String,
+ required: true,
+ },
+ labelsPath: {
+ type: String,
+ required: true,
+ },
+ namespace: {
+ type: String,
+ required: true,
+ },
+ labels: {
+ type: Array,
+ required: true,
+ },
+ showExtraOptions: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ dropdownToggleText() {
+ if (this.labels.length === 0) {
+ return __('Label');
+ }
+
+ if (this.labels.length > 1) {
+ return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
+ firstLabelName: this.labels[0].title,
+ remainingLabelCount: this.labels.length - 1,
+ });
+ }
+
+ return this.labels[0].title;
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ ref="dropdownButton"
+ class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
+ data-toggle="dropdown"
+ :class="{ 'js-extra-options': showExtraOptions }"
+ :data-ability-name="abilityName"
+ :data-field-name="fieldName"
+ :data-issue-update="updatePath"
+ :data-labels="labelsPath"
+ :data-namespace-path="namespace"
+ :data-show-any="showExtraOptions"
+ >
+ <span class="dropdown-toggle-text">
+ {{ dropdownToggleText }}
+ </span>
+ <i
+ aria-hidden="true"
+ class="fa fa-chevron-down"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
new file mode 100644
index 00000000000..4200d1e8473
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
@@ -0,0 +1,84 @@
+<script>
+export default {
+ created() {
+ this.suggestedColors = gon.suggested_label_colors;
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown-page-two dropdown-new-label">
+ <div class="dropdown-title">
+ <button
+ type="button"
+ class="dropdown-title-button dropdown-menu-back"
+ :aria-label="__('Go back')"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-arrow-left"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+ {{ __('Create new label') }}
+ <button
+ type="button"
+ class="dropdown-title-button dropdown-menu-close"
+ :aria-label="__('Close')"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-times dropdown-menu-close-icon"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+ </div>
+ <div class="dropdown-content">
+ <div class="dropdown-labels-error js-label-error"></div>
+ <input
+ id="new_label_name"
+ type="text"
+ class="default-dropdown-input"
+ :placeholder="__('Name new label')"
+ />
+ <div class="suggest-colors suggest-colors-dropdown">
+ <a
+ v-for="(color, index) in suggestedColors"
+ href="#"
+ :key="index"
+ :data-color="color"
+ :style="{
+ backgroundColor: color,
+ }"
+ >
+ &nbsp;
+ </a>
+ </div>
+ <div class="dropdown-label-color-input">
+ <div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div>
+ <input
+ id="new_label_color"
+ type="text"
+ class="default-dropdown-input"
+ :placeholder="__('Assign custom color like #FF0000')"
+ />
+ </div>
+ <div class="clearfix">
+ <button
+ type="button"
+ class="btn btn-primary pull-left js-new-label-btn disabled"
+ >
+ {{ __('Create') }}
+ </button>
+ <button
+ type="button"
+ class="btn btn-default pull-right js-cancel-label-btn"
+ >
+ {{ __('Cancel') }}
+ </button>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
new file mode 100644
index 00000000000..e951a863811
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
@@ -0,0 +1,34 @@
+<script>
+export default {
+ props: {
+ labelsWebUrl: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown-footer">
+ <ul class="dropdown-footer-list">
+ <li>
+ <a
+ href="#"
+ class="dropdown-toggle-page"
+ >
+ {{ __('Create new label') }}
+ </a>
+ </li>
+ <li>
+ <a
+ data-is-link="true"
+ class="dropdown-external-link"
+ :href="labelsWebUrl"
+ >
+ {{ __('Manage labels') }}
+ </a>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
new file mode 100644
index 00000000000..7664acdf19c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
@@ -0,0 +1,21 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div class="dropdown-title">
+ <span>{{ __('Assign labels') }}</span>
+ <button
+ type="button"
+ class="dropdown-title-button dropdown-menu-close"
+ :aria-label="__('Close')"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-times dropdown-menu-close-icon"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue
new file mode 100644
index 00000000000..1832c3c1757
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue
@@ -0,0 +1,22 @@
+<script>
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <input
+ type="hidden"
+ :name="name"
+ :value="label.id"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
new file mode 100644
index 00000000000..ae633460c95
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
@@ -0,0 +1,27 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div class="dropdown-input">
+ <input
+ autocomplete="off"
+ class="dropdown-input-field"
+ type="search"
+ :placeholder="__('Search')"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ data-hidden="true"
+ >
+ </i>
+ <i
+ aria-hidden="true"
+ class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
+ data-hidden="true"
+ role="button"
+ >
+ </i>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
new file mode 100644
index 00000000000..7da82e90e29
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
@@ -0,0 +1,30 @@
+<script>
+export default {
+ props: {
+ canEdit: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="title hide-collapsed append-bottom-10">
+ {{ __('Labels') }}
+ <template v-if="canEdit">
+ <i
+ aria-hidden="true"
+ class="fa fa-spinner fa-spin block-loading"
+ data-hidden="true"
+ >
+ </i>
+ <button
+ type="button"
+ class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle"
+ >
+ {{ __('Edit') }}
+ </button>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
new file mode 100644
index 00000000000..69d588eb25d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
@@ -0,0 +1,63 @@
+<script>
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ labels: {
+ type: Array,
+ required: true,
+ },
+ labelFilterBasePath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isEmpty() {
+ return this.labels.length === 0;
+ },
+ },
+ methods: {
+ labelFilterUrl(label) {
+ return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
+ },
+ labelStyle(label) {
+ return {
+ color: label.textColor,
+ backgroundColor: label.color,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="hide-collapsed value issuable-show-labels js-value">
+ <span
+ v-if="isEmpty"
+ class="text-secondary"
+ >
+ <slot>{{ __('None') }}</slot>
+ </span>
+ <a
+ v-else
+ v-for="label in labels"
+ :key="label.id"
+ :href="labelFilterUrl(label)"
+ >
+ <span
+ v-tooltip
+ class="label color-label"
+ data-placement="bottom"
+ data-container="body"
+ :style="labelStyle(label)"
+ :title="label.description"
+ >
+ {{ label.title }}
+ </span>
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
new file mode 100644
index 00000000000..5cf728fe050
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -0,0 +1,48 @@
+<script>
+import { s__, sprintf } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ labels: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ labelsList() {
+ const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', ');
+
+ if (this.labels.length > 5) {
+ return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
+ labelsString,
+ remainingLabelCount: this.labels.length - 5,
+ });
+ }
+
+ return labelsString;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-tooltip
+ class="sidebar-collapsed-icon"
+ data-placement="left"
+ data-container="body"
+ :title="labelsList"
+ >
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-tags"
+ >
+ </i>
+ <span>{{ labels.length }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/vue_shared/models/label.js
index 98c1ec014c4..70b9efe0c68 100644
--- a/app/assets/javascripts/boards/models/label.js
+++ b/app/assets/javascripts/vue_shared/models/label.js
@@ -1,7 +1,5 @@
-/* eslint-disable no-unused-vars, space-before-function-paren */
-
class ListLabel {
- constructor (obj) {
+ constructor(obj) {
this.id = obj.id;
this.title = obj.title;
this.type = obj.type;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index ae517c41cb2..37d33320445 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -14,6 +14,10 @@
color: $gl-text-color-secondary;
}
+.text-tertiary {
+ color: $gl-text-color-tertiary;
+}
+
.text-primary,
.text-primary:hover {
color: $brand-primary;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 1d7b0b602cc..127583626cf 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -272,7 +272,7 @@
.divider {
height: 1px;
- margin: 6px 0;
+ margin: #{$grid-size / 2} 0;
padding: 0;
background-color: $dropdown-divider-color;
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 88ce119ee3a..cb2f71b0033 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -12,6 +12,12 @@
margin: 0;
}
+ .flash-warning {
+ @extend .alert;
+ @extend .alert-warning;
+ margin: 0;
+ }
+
.flash-alert {
@extend .alert;
@extend .alert-danger;
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index a6b1bf9b099..48b981dd31f 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -2,14 +2,17 @@
background-color: $modal-body-bg;
padding: #{3 * $grid-size} #{2 * $grid-size};
- .page-title {
- margin-top: 0;
-
+ .page-title,
+ .modal-title {
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
}
}
+
+ .page-title {
+ margin-top: 0;
+ }
}
.modal-body {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 17801ed5910..8b680c2dc52 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -196,17 +196,9 @@
@media (min-width: $screen-sm-min) {
font-size: 0;
- div {
- display: inline;
- }
-
.fa-spinner {
font-size: 12px;
}
-
- span {
- font-size: 6px;
- }
}
.ci-status-link {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 884665d35c7..58700661142 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -369,7 +369,8 @@
}
> text {
- font-size: 12px;
+ fill: $theme-gray-600;
+ font-size: 10px;
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 0cf67734237..4c9732c26d9 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -103,6 +103,7 @@
.issuable-show-labels {
a {
margin-bottom: 5px;
+ margin-right: 5px;
display: inline-block;
.color-label {
@@ -116,6 +117,12 @@
}
&.has-labels {
+ // this font size is a fix to
+ // prevent unintended spacing between labels
+ // which shows up when rendering markup has white-space
+ // characters present.
+ // see: https://css-tricks.com/fighting-the-space-between-inline-block-elements/#article-header-id-3
+ font-size: 0;
margin-bottom: -5px;
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 26e6e8688b6..3c565837383 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -723,7 +723,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 5px 10px 6px;
+ padding: 6px 10px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 47672783d5a..a6ca8ed5016 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -205,7 +205,8 @@
}
.badge {
- font-size: inherit;
+ font-size: 12px;
+ line-height: 12px;
}
.panel-heading .badge-count {
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index a94726887d9..cc38608eda5 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -48,7 +48,7 @@ class Admin::GroupsController < Admin::ApplicationController
def members_update
member_params = params.permit(:user_ids, :access_level, :expires_at)
- result = Members::CreateService.new(@group, current_user, member_params.merge(limit: -1)).execute
+ result = Members::CreateService.new(current_user, member_params.merge(limit: -1)).execute(@group)
if result[:status] == :success
redirect_to [:admin, @group], notice: 'Users were successfully added.'
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index 7a2c7234a1e..a7b562b1d8e 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -9,7 +9,6 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
@impersonation_token = finder.build(impersonation_token_params)
if @impersonation_token.save
- flash[:impersonation_token] = @impersonation_token.token
redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
else
set_index_vars
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index e6a41202f04..7f83bd10e93 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -191,7 +191,7 @@ class ApplicationController < ActionController::Base
return unless signed_in? && session[:service_tickets]
valid = session[:service_tickets].all? do |provider, ticket|
- Gitlab::OAuth::Session.valid?(provider, ticket)
+ Gitlab::Auth::OAuth::Session.valid?(provider, ticket)
end
unless valid
@@ -215,7 +215,7 @@ class ApplicationController < ActionController::Base
if current_user && current_user.requires_ldap_check?
return unless current_user.try_obtain_ldap_lease
- unless Gitlab::LDAP::Access.allowed?(current_user)
+ unless Gitlab::Auth::LDAP::Access.allowed?(current_user)
sign_out current_user
flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path
@@ -230,7 +230,7 @@ class ApplicationController < ActionController::Base
end
def gitlab_ldap_access(&block)
- Gitlab::LDAP::Access.open { |access| yield(access) }
+ Gitlab::Auth::LDAP::Access.open { |access| yield(access) }
end
# JSON for infinite scroll via Pager object
@@ -284,7 +284,7 @@ class ApplicationController < ActionController::Base
end
def github_import_configured?
- Gitlab::OAuth::Provider.enabled?(:github)
+ Gitlab::Auth::OAuth::Provider.enabled?(:github)
end
def gitlab_import_enabled?
@@ -292,7 +292,7 @@ class ApplicationController < ActionController::Base
end
def gitlab_import_configured?
- Gitlab::OAuth::Provider.enabled?(:gitlab)
+ Gitlab::Auth::OAuth::Provider.enabled?(:gitlab)
end
def bitbucket_import_enabled?
@@ -300,7 +300,7 @@ class ApplicationController < ActionController::Base
end
def bitbucket_import_configured?
- Gitlab::OAuth::Provider.enabled?(:bitbucket)
+ Gitlab::Auth::OAuth::Provider.enabled?(:bitbucket)
end
def google_code_import_enabled?
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 352f12a89fd..19dbee84c11 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -1,6 +1,9 @@
module Boards
class IssuesController < Boards::ApplicationController
include BoardsResponses
+ include ControllerWithCrossProjectAccessCheck
+
+ requires_cross_project_access if: -> { board&.group_board? }
before_action :whitelist_query_limiting, only: [:index, :update]
before_action :authorize_read_issue, only: [:index]
@@ -64,11 +67,19 @@ module Boards
end
def issues_finder
- IssuesFinder.new(current_user, project_id: board_parent.id)
+ if board.group_board?
+ IssuesFinder.new(current_user, group_id: board_parent.id)
+ else
+ IssuesFinder.new(current_user, project_id: board_parent.id)
+ end
end
def project
- board_parent
+ @project ||= if board.group_board?
+ Project.find(issue_params[:project_id])
+ else
+ board_parent
+ end
end
def move_params
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index db8c362f125..2753f83c3cf 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -56,6 +56,7 @@ module AuthenticatesWithTwoFactor
session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1'
+ user.save!
sign_in(user)
else
user.increment_failed_attempts!
diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
index a145049dc7d..da830ec2cb1 100644
--- a/app/controllers/concerns/boards_responses.rb
+++ b/app/controllers/concerns/boards_responses.rb
@@ -1,10 +1,46 @@
module BoardsResponses
+ include Gitlab::Utils::StrongMemoize
+
+ def board_params
+ params.require(:board).permit(:name, :weight, :milestone_id, :assignee_id, label_ids: [])
+ end
+
+ def parent
+ strong_memoize(:parent) do
+ group? ? group : project
+ end
+ end
+
+ def boards_path
+ if group?
+ group_boards_path(parent)
+ else
+ project_boards_path(parent)
+ end
+ end
+
+ def board_path(board)
+ if group?
+ group_board_path(parent, board)
+ else
+ project_board_path(parent, board)
+ end
+ end
+
+ def group?
+ instance_variable_defined?(:@group)
+ end
+
def authorize_read_list
- authorize_action_for!(board.parent, :read_list)
+ ability = board.group_board? ? :read_group : :read_list
+
+ authorize_action_for!(board.parent, ability)
end
def authorize_read_issue
- authorize_action_for!(board.parent, :read_issue)
+ ability = board.group_board? ? :read_group : :read_issue
+
+ authorize_action_for!(board.parent, ability)
end
def authorize_update_issue
@@ -31,6 +67,10 @@ module BoardsResponses
respond_with(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+ def serialize_as_json(resource)
+ resource.as_json(only: [:id])
+ end
+
def respond_with(resource)
respond_to do |format|
format.html
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 6f4fdcdaa4f..b26a76d2b62 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -4,7 +4,7 @@ module CreatesCommit
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
- if can?(current_user, :push_code, @project)
+ if user_access(@project).can_push_to_branch?(branch_name_or_ref)
@project_to_commit_into = @project
@branch_name ||= @ref
else
@@ -50,7 +50,7 @@ module CreatesCommit
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def authorize_edit_tree!
- return if can_collaborate_with_project?
+ return if can_collaborate_with_project?(project, ref: branch_name_or_ref)
access_denied!
end
@@ -123,4 +123,8 @@ module CreatesCommit
params[:create_merge_request].present? &&
(different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+
+ def branch_name_or_ref
+ @branch_name || @ref # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 337957c366d..a21e658fda1 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -77,6 +77,20 @@ module IssuableActions
render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
end
+ def discussions
+ notes = issuable.notes
+ .inc_relations_for_view
+ .includes(:noteable)
+ .fresh
+
+ notes = prepare_notes_for_rendering(notes)
+ notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
+
+ discussions = Discussion.build_collection(notes, issuable)
+
+ render json: DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user).represent(discussions, context: self)
+ end
+
private
def recaptcha_check_if_spammable(should_redirect = true, &block)
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index f7ba305a59f..4114ca6bf7c 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -17,7 +17,7 @@ module IssuableCollections
set_pagination
return if redirect_out_of_range(@total_pages)
- if params[:label_name].present?
+ if params[:label_name].present? && @project
labels_params = { project_id: @project.id, title: params[:label_name] }
@labels = LabelsFinder.new(current_user, labels_params).execute
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index c6b1e443de6..7a6a00b8e13 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -3,20 +3,31 @@ module MembershipActions
def create
create_params = params.permit(:user_ids, :access_level, :expires_at)
- result = Members::CreateService.new(membershipable, current_user, create_params).execute
-
- redirect_url = members_page_url
+ result = Members::CreateService.new(current_user, create_params).execute(membershipable)
if result[:status] == :success
- redirect_to redirect_url, notice: 'Users were successfully added.'
+ redirect_to members_page_url, notice: 'Users were successfully added.'
else
- redirect_to redirect_url, alert: result[:message]
+ redirect_to members_page_url, alert: result[:message]
+ end
+ end
+
+ def update
+ update_params = params.require(root_params_key).permit(:access_level, :expires_at)
+ member = membershipable.members_and_requesters.find(params[:id])
+ member = Members::UpdateService
+ .new(current_user, update_params)
+ .execute(member)
+ .present(current_user: current_user)
+
+ respond_to do |format|
+ format.js { render 'shared/members/update', locals: { member: member } }
end
end
def destroy
- Members::DestroyService.new(membershipable, current_user, params)
- .execute(:all)
+ member = membershipable.members_and_requesters.find(params[:id])
+ Members::DestroyService.new(current_user).execute(member)
respond_to do |format|
format.html do
@@ -36,14 +47,17 @@ module MembershipActions
end
def approve_access_request
- Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
+ access_requester = membershipable.requesters.find(params[:id])
+ Members::ApproveAccessRequestService
+ .new(current_user, params)
+ .execute(access_requester)
redirect_to members_page_url
end
def leave
- member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id)
- .execute(:all)
+ member = membershipable.members_and_requesters.find_by!(user_id: current_user.id)
+ Members::DestroyService.new(current_user).execute(member)
notice =
if member.request?
@@ -62,17 +76,43 @@ module MembershipActions
end
end
+ def resend_invite
+ member = membershipable.members.find(params[:id])
+
+ if member.invite?
+ member.resend_invite
+
+ redirect_to members_page_url, notice: 'The invitation was successfully resent.'
+ else
+ redirect_to members_page_url, alert: 'The invitation has already been accepted.'
+ end
+ end
+
protected
def membershipable
raise NotImplementedError
end
+ def root_params_key
+ case membershipable
+ when Namespace
+ :group_member
+ when Project
+ :project_member
+ else
+ raise "Unknown membershipable type: #{membershipable}!"
+ end
+ end
+
def members_page_url
- if membershipable.is_a?(Project)
+ case membershipable
+ when Namespace
+ polymorphic_url([membershipable, :members])
+ when Project
project_project_members_path(membershipable)
else
- polymorphic_url([membershipable, :members])
+ raise "Unknown membershipable type: #{membershipable}!"
end
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index e82a5650935..03ed5b5310b 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -22,7 +22,7 @@ module NotesActions
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes_json[:notes] =
- if noteable.discussions_rendered_on_frontend?
+ if use_note_serializer?
note_serializer.represent(notes)
else
notes.map { |note| note_json(note) }
@@ -95,7 +95,7 @@ module NotesActions
if note.persisted?
attrs[:valid] = true
- if noteable.discussions_rendered_on_frontend?
+ if use_note_serializer?
attrs.merge!(note_serializer.represent(note))
else
attrs.merge!(
@@ -233,4 +233,14 @@ module NotesActions
the_project
end
end
+
+ def use_note_serializer?
+ return false if params['html']
+
+ if noteable.is_a?(MergeRequest)
+ cookies[:vue_mr_discussions] == 'true'
+ else
+ noteable.discussions_rendered_on_frontend?
+ end
+ end
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 4a2bfc1f887..9f3bb60b4cc 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -18,10 +18,6 @@ class Groups::ApplicationController < ApplicationController
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end
- def group_merge_requests
- @group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
- end
-
def authorize_admin_group!
unless can?(current_user, :admin_group, group)
return render_404
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
new file mode 100644
index 00000000000..7c2016f0326
--- /dev/null
+++ b/app/controllers/groups/boards_controller.rb
@@ -0,0 +1,27 @@
+class Groups::BoardsController < Groups::ApplicationController
+ include BoardsResponses
+
+ before_action :assign_endpoint_vars
+
+ def index
+ @boards = Boards::ListService.new(group, current_user).execute
+
+ respond_with_boards
+ end
+
+ def show
+ @board = group.boards.find(params[:id])
+
+ respond_with_board
+ end
+
+ def assign_endpoint_vars
+ @boards_endpoint = group_boards_url(group)
+ @namespace_path = group.to_param
+ @labels_endpoint = group_labels_url(group)
+ end
+
+ def serialize_as_json(resource)
+ resource.as_json(only: [:id])
+ end
+end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 2c371e76313..f210434b2d7 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -27,35 +27,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member = @group.group_members.new
end
- def update
- @group_member = @group.members_and_requesters.find(params[:id])
- .present(current_user: current_user)
-
- return render_403 unless can?(current_user, :update_group_member, @group_member)
-
- @group_member.update_attributes(member_params)
- end
-
- def resend_invite
- redirect_path = group_group_members_path(@group)
-
- @group_member = @group.group_members.find(params[:id])
-
- if @group_member.invite?
- @group_member.resend_invite
-
- redirect_to redirect_path, notice: 'The invitation was successfully resent.'
- else
- redirect_to redirect_path, alert: 'The invitation has already been accepted.'
- end
- end
-
- protected
-
- def member_params
- params.require(:group_member).permit(:access_level, :user_id, :expires_at)
- end
-
# MembershipActions concern
alias_method :membershipable, :group
end
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index f3a9e591c3e..58be330f466 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -14,7 +14,14 @@ class Groups::LabelsController < Groups::ApplicationController
end
format.json do
- available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute
+ available_labels = LabelsFinder.new(
+ current_user,
+ group_id: @group.id,
+ only_group_labels: params[:only_group_labels],
+ include_ancestor_groups: params[:include_ancestor_groups],
+ include_descendant_groups: params[:include_descendant_groups]
+ ).execute
+
render json: LabelSerializer.new.represent_appearance(available_labels)
end
end
@@ -28,10 +35,18 @@ class Groups::LabelsController < Groups::ApplicationController
def create
@label = Labels::CreateService.new(label_params).execute(group: group)
- if @label.valid?
- redirect_to group_labels_path(@group)
- else
- render :new
+ respond_to do |format|
+ format.html do
+ if @label.valid?
+ redirect_to group_labels_path(@group)
+ else
+ render :new
+ end
+ end
+
+ format.json do
+ render json: LabelSerializer.new.represent_appearance(@label)
+ end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 14b9d6c22bd..283c3e5f1e0 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -14,7 +14,6 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
- before_action :group_merge_requests, only: [:merge_requests]
before_action :event_filter, only: [:activity]
before_action :user_actions, only: [:show, :subgroups]
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
deleted file mode 100644
index 1ff25a45398..00000000000
--- a/app/controllers/ide_controller.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class IdeController < ApplicationController
- layout 'nav_only'
-
- def index
- end
-end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 13ea736688d..61d81ad8a71 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -71,7 +71,7 @@ class Import::BitbucketController < Import::BaseController
end
def provider
- Gitlab::OAuth::Provider.config_for('bitbucket')
+ Gitlab::Auth::OAuth::Provider.config_for('bitbucket')
end
def options
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 69fb8121ded..eb7d5fca367 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -42,7 +42,9 @@ class Import::GithubController < Import::BaseController
target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
if can?(current_user, :create_projects, target_namespace)
- project = Gitlab::LegacyGithubImport::ProjectCreator.new(repo, project_name, target_namespace, current_user, access_params, type: provider).execute
+ project = Gitlab::LegacyGithubImport::ProjectCreator
+ .new(repo, project_name, target_namespace, current_user, access_params, type: provider)
+ .execute(extra_project_attrs)
if project.persisted?
render json: ProjectSerializer.new.represent(project)
@@ -73,15 +75,15 @@ class Import::GithubController < Import::BaseController
end
def new_import_url
- public_send("new_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend
+ public_send("new_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
end
def status_import_url
- public_send("status_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend
+ public_send("status_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
end
def callback_import_url
- public_send("callback_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend
+ public_send("callback_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
end
def provider_unauthorized
@@ -116,4 +118,12 @@ class Import::GithubController < Import::BaseController
def client_options
{}
end
+
+ def extra_project_attrs
+ {}
+ end
+
+ def extra_import_params
+ {}
+ end
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 52430ea771f..025d8270b7c 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -62,7 +62,7 @@ class InvitesController < ApplicationController
case source
when Project
project = member.source
- label = "project #{project.name_with_namespace}"
+ label = "project #{project.full_name}"
path = project_path(project)
when Group
group = member.source
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 83c9a3f035e..8440945ab43 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -10,8 +10,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
- if Gitlab::LDAP::Config.enabled?
- Gitlab::LDAP::Config.available_servers.each do |server|
+ if Gitlab::Auth::LDAP::Config.enabled?
+ Gitlab::Auth::LDAP::Config.available_servers.each do |server|
define_method server['provider_name'] do
ldap
end
@@ -31,7 +31,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# We only find ourselves here
# if the authentication to LDAP was successful.
def ldap
- ldap_user = Gitlab::LDAP::User.new(oauth)
+ ldap_user = Gitlab::Auth::LDAP::User.new(oauth)
ldap_user.save if ldap_user.changed? # will also save new users
@user = ldap_user.gl_user
@@ -62,13 +62,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to after_sign_in_path_for(current_user)
end
else
- saml_user = Gitlab::Saml::User.new(oauth)
+ saml_user = Gitlab::Auth::Saml::User.new(oauth)
saml_user.save if saml_user.changed?
@user = saml_user.gl_user
continue_login_process
end
- rescue Gitlab::OAuth::SignupDisabledError
+ rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
end
@@ -106,20 +106,20 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
- oauth_user = Gitlab::OAuth::User.new(oauth)
+ oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
oauth_user.save
@user = oauth_user.gl_user
continue_login_process
end
- rescue Gitlab::OAuth::SigninDisabledForProviderError
+ rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
handle_disabled_provider
- rescue Gitlab::OAuth::SignupDisabledError
+ rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
end
def handle_service_ticket(provider, ticket)
- Gitlab::OAuth::Session.create provider, ticket
+ Gitlab::Auth::OAuth::Session.create provider, ticket
session[:service_tickets] ||= {}
session[:service_tickets][provider] = ticket
end
@@ -142,7 +142,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def handle_signup_error
- label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
+ label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
if Gitlab::CurrentSettings.allow_signup?
@@ -171,7 +171,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def handle_disabled_provider
- label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
+ label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
flash[:alert] = "Signing in using #{label} has been disabled"
redirect_to new_user_session_path
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index fa72f67c77e..b8ccc6e3c99 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -1,5 +1,6 @@
class Profiles::PasswordsController < Profiles::ApplicationController
skip_before_action :check_password_expiration, only: [:new, :create]
+ skip_before_action :check_two_factor_requirement, only: [:new, :create]
before_action :set_user
before_action :authorize_change_password!
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 6025a40348b..6d9b42a2c04 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -6,7 +6,7 @@ class Projects::ApplicationController < ApplicationController
before_action :repository
layout 'project'
- helper_method :repository, :can_collaborate_with_project?
+ helper_method :repository, :can_collaborate_with_project?, :user_access
private
@@ -31,11 +31,12 @@ class Projects::ApplicationController < ApplicationController
@repository ||= project.repository
end
- def can_collaborate_with_project?(project = nil)
+ def can_collaborate_with_project?(project = nil, ref: nil)
project ||= @project
can?(current_user, :push_code, project) ||
- (current_user && current_user.already_forked?(project))
+ (current_user && current_user.already_forked?(project)) ||
+ user_access(project).can_push_to_branch?(ref)
end
def authorize_action!(action)
@@ -90,4 +91,9 @@ class Projects::ApplicationController < ApplicationController
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
+
+ def user_access(project)
+ @user_access ||= {}
+ @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
+ end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 74c25505e36..0c1c286a0a4 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -9,8 +9,12 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
- before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
+
+ # We need to assign the blob vars before `authorize_edit_tree!` so we can
+ # validate access to a specific ref.
before_action :assign_blob_vars
+ before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
+
before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create]
before_action :require_branch_head, only: [:edit, :update]
@@ -38,7 +42,7 @@ class Projects::BlobController < Projects::ApplicationController
end
format.json do
- page_title @blob.path, @ref, @project.name_with_namespace
+ page_title @blob.path, @ref, @project.full_name
show_json
end
@@ -46,7 +50,7 @@ class Projects::BlobController < Projects::ApplicationController
end
def edit
- if can_collaborate_with_project?
+ if can_collaborate_with_project?(project, ref: @ref)
blob.load_all_data!
else
redirect_to action: 'show'
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index cabafe26357..965cece600e 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -7,13 +7,19 @@ class Projects::BranchesController < Projects::ApplicationController
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
- def index
- @sort = params[:sort].presence || sort_value_recently_updated
- @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
- @branches = Kaminari.paginate_array(@branches).page(params[:page])
+ # Support legacy URLs
+ before_action :redirect_for_legacy_index_sort_or_search, only: [:index]
+ def index
respond_to do |format|
format.html do
+ @sort = params[:sort].presence || sort_value_recently_updated
+ @mode = params[:state].presence || 'overview'
+ @overview_max_branches = 5
+
+ # Fetch branches for the specified mode
+ fetch_branches_by_mode
+
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@merged_branch_names =
repository.merged_branch_names(@branches.map(&:name))
@@ -28,7 +34,9 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
format.json do
- render json: @branches.map(&:name)
+ branches = BranchesFinder.new(@repository, params).execute
+ branches = Kaminari.paginate_array(branches).page(params[:page])
+ render json: branches.map(&:name)
end
end
end
@@ -123,4 +131,27 @@ class Projects::BranchesController < Projects::ApplicationController
context: 'autodeploy'
)
end
+
+ def redirect_for_legacy_index_sort_or_search
+ # Normalize a legacy URL with redirect
+ if request.format != :json && !params[:state].presence && [:sort, :search, :page].any? { |key| params[key].presence }
+ redirect_to project_branches_filtered_path(@project, state: 'all'), notice: 'Update your bookmarked URLs as filtered/sorted branches URL has been changed.'
+ end
+ end
+
+ def fetch_branches_by_mode
+ if @mode == 'overview'
+ # overview mode
+ @active_branches, @stale_branches = BranchesFinder.new(@repository, sort: sort_value_recently_updated).execute.partition(&:active?)
+ # Here we get one more branch to indicate if there are more data we're not showing
+ @active_branches = @active_branches.first(@overview_max_branches + 1)
+ @stale_branches = @stale_branches.first(@overview_max_branches + 1)
+ @branches = @active_branches + @stale_branches
+ else
+ # active/stale/all view mode
+ @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
+ @branches = @branches.select { |b| b.state.to_s == @mode } if %w[active stale].include?(@mode)
+ @branches = Kaminari.paginate_array(@branches).page(params[:page])
+ end
+ end
end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 142e8b6e4bc..aeaba3a0acf 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -4,6 +4,7 @@ class Projects::ClustersController < Projects::ApplicationController
before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
+ before_action :update_applications_status, only: [:status]
STATUS_POLLING_INTERVAL = 10_000
@@ -114,4 +115,8 @@ class Projects::ClustersController < Projects::ApplicationController
def authorize_admin_cluster!
access_denied! unless can?(current_user, :admin_cluster, cluster)
end
+
+ def update_applications_status
+ @cluster.applications.each(&:schedule_status_update)
+ end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 1d910e461b1..7b7cb52d7ed 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -14,37 +14,31 @@ class Projects::CommitsController < Projects::ApplicationController
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
- # https://gitlab.com/gitlab-org/gitaly/issues/931
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- respond_to do |format|
- format.html
- format.atom { render layout: 'xml.atom' }
+ respond_to do |format|
+ format.html
+ format.atom { render layout: 'xml.atom' }
- format.json do
- pager_json(
- 'projects/commits/_commits',
- @commits.size,
- project: @project,
- ref: @ref)
- end
+ format.json do
+ pager_json(
+ 'projects/commits/_commits',
+ @commits.size,
+ project: @project,
+ ref: @ref)
end
end
end
def signatures
- # https://gitlab.com/gitlab-org/gitaly/issues/931
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- respond_to do |format|
- format.json do
- render json: {
- signatures: @commits.select(&:has_signature?).map do |commit|
- {
- commit_sha: commit.sha,
- html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
- }
- end
- }
- end
+ respond_to do |format|
+ format.json do
+ render json: {
+ signatures: @commits.select(&:has_signature?).map do |commit|
+ {
+ commit_sha: commit.sha,
+ html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
+ }
+ end
+ }
end
end
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 3cb4eb23981..2b0c2ca97c0 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -17,10 +17,8 @@ class Projects::CompareController < Projects::ApplicationController
def show
apply_diff_view_cookie!
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37430
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render
- end
+
+ render
end
def diff_for_path
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index 1a418d0f15a..b68cdc39cb8 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -24,7 +24,7 @@ class Projects::DeploymentsController < Projects::ApplicationController
end
def additional_metrics
- return render_404 unless deployment.has_additional_metrics?
+ return render_404 unless deployment.has_metrics?
respond_to do |format|
format.json do
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 2e6ab7903b8..ee507009e50 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -1,4 +1,7 @@
class Projects::DiscussionsController < Projects::ApplicationController
+ include NotesHelper
+ include RendersNotes
+
before_action :check_merge_requests_available!
before_action :merge_request
before_action :discussion
@@ -7,22 +10,45 @@ class Projects::DiscussionsController < Projects::ApplicationController
def resolve
Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion)
- render json: {
- resolved_by: discussion.resolved_by.try(:name),
- discussion_headline_html: view_to_html_string('discussions/_headline', discussion: discussion)
- }
+ render_discussion
end
def unresolve
discussion.unresolve!
+ render_discussion
+ end
+
+ private
+
+ def render_discussion
+ if serialize_notes?
+ # TODO - It is not needed to serialize notes when resolving
+ # or unresolving discussions. We should remove this behavior
+ # passing a parameter to DiscussionEntity to return an empty array
+ # for notes.
+ # Check issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/42853
+ prepare_notes_for_rendering(discussion.notes, merge_request)
+ render_json_with_discussions_serializer
+ else
+ render_json_with_html
+ end
+ end
+
+ def render_json_with_discussions_serializer
+ render json:
+ DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user)
+ .represent(discussion, context: self)
+ end
+
+ # Legacy method used to render discussions notes when not using Vue on views.
+ def render_json_with_html
render json: {
+ resolved_by: discussion.resolved_by.try(:name),
discussion_headline_html: view_to_html_string('discussions/_headline', discussion: discussion)
}
end
- private
-
def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id])
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 73806454525..b14939c4216 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -60,20 +60,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue)
end
- def discussions
- notes = @issue.notes
- .inc_relations_for_view
- .includes(:noteable)
- .fresh
-
- notes = prepare_notes_for_rendering(notes)
- notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
-
- discussions = Discussion.build_collection(notes, @issue)
-
- render json: DiscussionSerializer.new(project: @project, noteable: @issue, current_user: current_user).represent(discussions)
- end
-
def create
create_params = issue_params.merge(spammable_params).merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index e0f4710175f..99790b8e7e8 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -112,12 +112,14 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
+ flash[:notice] = "#{@label.title} promoted to group label."
respond_to do |format|
format.html do
- redirect_to(project_labels_path(@project),
- notice: 'Label was promoted to a Group Label')
+ redirect_to(project_labels_path(@project), status: 303)
+ end
+ format.json do
+ render json: { url: project_labels_path(@project) }
end
- format.js
end
rescue ActiveRecord::RecordInvalid => e
Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label"
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 793ae03fb88..67d4ea2ca8f 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -15,6 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes
[
+ :allow_maintainer_to_push,
:assignee_id,
:description,
:force_remove_source_branch,
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index a1af125547c..54e7d81de6a 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -187,7 +187,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
begin
@merge_request.environments_for(current_user).map do |environment|
project = environment.project
- deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
+ deployment = environment.first_deployment_for(@merge_request.diff_head_sha)
stop_url =
if environment.stop_action? && can?(current_user, :create_deployment, environment)
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 75b17d05e22..ff93147d00f 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -70,9 +70,17 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def promote
- promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
- flash[:notice] = "Milestone has been promoted to group milestone."
- redirect_to group_milestone_path(project.group, promoted_milestone.iid)
+ Milestones::PromoteService.new(project, current_user).execute(milestone)
+
+ flash[:notice] = "#{milestone.title} promoted to group milestone"
+ respond_to do |format|
+ format.html do
+ redirect_to project_milestones_path(project)
+ end
+ format.json do
+ render json: { url: project_milestones_path(project) }
+ end
+ end
rescue Milestones::PromoteService::PromoteMilestoneError => error
redirect_to milestone, alert: error.message
end
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index 3b10a93e97f..35fec229db7 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -9,25 +9,22 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :assign_commit
def show
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37602
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- @url = project_network_path(@project, @ref, @options.merge(format: :json))
- @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
-
- respond_to do |format|
- format.html do
- if @options[:extended_sha1] && !@commit
- flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
- end
- end
+ @url = project_network_path(@project, @ref, @options.merge(format: :json))
+ @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
- format.json do
- @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
+ respond_to do |format|
+ format.html do
+ if @options[:extended_sha1] && !@commit
+ flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
end
end
- render
+ format.json do
+ @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
+ end
end
+
+ render
end
def assign_commit
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 4f8978c93c3..dd41b9648e8 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -1,5 +1,6 @@
class Projects::NotesController < Projects::ApplicationController
include NotesActions
+ include NotesHelper
include ToggleAwardEmoji
before_action :whitelist_query_limiting, only: [:create]
@@ -38,10 +39,14 @@ class Projects::NotesController < Projects::ApplicationController
discussion = note.discussion
- render json: {
- resolved_by: note.resolved_by.try(:name),
- discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
- }
+ if serialize_notes?
+ render_json_with_notes_serializer
+ else
+ render json: {
+ resolved_by: note.resolved_by.try(:name),
+ discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
+ }
+ end
end
def unresolve
@@ -51,16 +56,27 @@ class Projects::NotesController < Projects::ApplicationController
discussion = note.discussion
- render json: {
- discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
- }
+ if serialize_notes?
+ render_json_with_notes_serializer
+ else
+ render json: {
+ discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
+ }
+ end
end
private
+ def render_json_with_notes_serializer
+ Notes::RenderService.new(current_user).execute([note], project)
+
+ render json: note_serializer.represent(note)
+ end
+
def note
@note ||= @project.notes.find(params[:id])
end
+
alias_method :awardable, :note
def finder_params
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
index b71f1e5fef4..4856be61e88 100644
--- a/app/controllers/projects/pages_domains_controller.rb
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
before_action :require_pages_enabled!
before_action :authorize_update_pages!, except: [:show]
- before_action :domain, only: [:show, :destroy, :verify]
+ before_action :domain, except: [:new, :create]
def show
end
@@ -24,8 +24,11 @@ class Projects::PagesDomainsController < Projects::ApplicationController
redirect_to project_pages_domain_path(@project, @domain)
end
+ def edit
+ end
+
def create
- @domain = @project.pages_domains.create(pages_domain_params)
+ @domain = @project.pages_domains.create(create_params)
if @domain.valid?
redirect_to project_pages_domain_path(@project, @domain)
@@ -34,6 +37,16 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end
end
+ def update
+ if @domain.update(update_params)
+ redirect_to project_pages_path(@project),
+ status: 302,
+ notice: 'Domain was updated'
+ else
+ render 'edit'
+ end
+ end
+
def destroy
@domain.destroy
@@ -49,12 +62,12 @@ class Projects::PagesDomainsController < Projects::ApplicationController
private
- def pages_domain_params
- params.require(:pages_domain).permit(
- :certificate,
- :key,
- :domain
- )
+ def create_params
+ params.require(:pages_domain).permit(:key, :certificate, :domain)
+ end
+
+ def update_params
+ params.require(:pages_domain).permit(:key, :certificate)
end
def domain
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index d7372beb9d3..e9b4679f94c 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -26,29 +26,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_member = @project.project_members.new
end
- def update
- @project_member = @project.members_and_requesters.find(params[:id])
- .present(current_user: current_user)
-
- return render_403 unless can?(current_user, :update_project_member, @project_member)
-
- @project_member.update_attributes(member_params)
- end
-
- def resend_invite
- redirect_path = project_project_members_path(@project)
-
- @project_member = @project.project_members.find(params[:id])
-
- if @project_member.invite?
- @project_member.resend_invite
-
- redirect_to redirect_path, notice: 'The invitation was successfully resent.'
- else
- redirect_to redirect_path, alert: 'The invitation has already been accepted.'
- end
- end
-
def import
@projects = current_user.authorized_projects.order_id_desc
end
@@ -67,12 +44,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
notice: notice)
end
- protected
-
- def member_params
- params.require(:project_member).permit(:user_id, :access_level, :expires_at)
- end
-
# MembershipActions concern
alias_method :membershipable, :project
end
diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb
index b739d0f0f90..1dd886409a5 100644
--- a/app/controllers/projects/prometheus/metrics_controller.rb
+++ b/app/controllers/projects/prometheus/metrics_controller.rb
@@ -2,11 +2,12 @@ module Projects
module Prometheus
class MetricsController < Projects::ApplicationController
before_action :authorize_admin_project!
+ before_action :require_prometheus_metrics!
def active_common
respond_to do |format|
format.json do
- matched_metrics = prometheus_service.matched_metrics || {}
+ matched_metrics = prometheus_adapter.query(:matched_metrics) || {}
if matched_metrics.any?
render json: matched_metrics
@@ -19,8 +20,12 @@ module Projects
private
- def prometheus_service
- @prometheus_service ||= project.find_or_initialize_service('prometheus')
+ def prometheus_adapter
+ @prometheus_adapter ||= ::Prometheus::AdapterService.new(project).prometheus_adapter
+ end
+
+ def require_prometheus_metrics!
+ render_404 unless prometheus_adapter.can_query?
end
end
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index daa5c88aae0..f14cb5f6a9f 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -3,7 +3,8 @@ class Projects::ServicesController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
- before_action :service, only: [:edit, :update, :test]
+ before_action :ensure_service_enabled
+ before_action :service
respond_to :html
@@ -23,26 +24,30 @@ class Projects::ServicesController < Projects::ApplicationController
end
def test
- message = {}
+ if @service.can_test?
+ render json: service_test_response, status: :ok
+ else
+ render json: {}, status: :not_found
+ end
+ end
- if @service.can_test? && @service.update_attributes(service_params[:service])
+ private
+
+ def service_test_response
+ if @service.update_attributes(service_params[:service])
data = @service.test_data(project, current_user)
outcome = @service.test(data)
- unless outcome[:success]
- message = { error: true, message: 'Test failed.', service_response: outcome[:result].to_s }
+ if outcome[:success]
+ {}
+ else
+ { error: true, message: 'Test failed.', service_response: outcome[:result].to_s }
end
-
- status = :ok
else
- status = :not_found
+ { error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
end
-
- render json: message, status: status
end
- private
-
def success_message
if @service.active?
"#{@service.title} activated."
@@ -54,4 +59,8 @@ class Projects::ServicesController < Projects::ApplicationController
def service
@service ||= @project.find_or_initialize_service(params[:id])
end
+
+ def ensure_service_enabled
+ render_404 unless service
+ end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 86717bb7242..259809f3429 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -13,12 +13,14 @@ module Projects
def reset_cache
if ResetProjectCacheService.new(@project, current_user).execute
- flash[:notice] = _("Project cache successfully reset.")
+ respond_to do |format|
+ format.json { head :ok }
+ end
else
- flash[:error] = _("Unable to reset project cache.")
+ respond_to do |format|
+ format.json { head :bad_request }
+ end
end
-
- redirect_to project_pipelines_path(@project)
end
private
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index f752a46f828..ee9b5458282 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -36,7 +36,7 @@ class Projects::TreeController < Projects::ApplicationController
end
format.json do
- page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
+ page_title @path.presence || _("Files"), @ref, @project.full_name
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
Gitlab::GitalyClient.allow_n_plus_1_calls do
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 913689a1e74..ee197c75764 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -41,11 +41,11 @@ class ProjectsController < Projects::ApplicationController
cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) }
redirect_to(
- project_path(@project),
+ project_path(@project, custom_import_params),
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
)
else
- render 'new', locals: { active_tab: ('import' if project_params[:import_url].present?) }
+ render 'new', locals: { active_tab: active_new_project_tab }
end
end
@@ -103,7 +103,7 @@ class ProjectsController < Projects::ApplicationController
def show
if @project.import_in_progress?
- redirect_to project_import_path(@project)
+ redirect_to project_import_path(@project, custom_import_params)
return
end
@@ -130,7 +130,7 @@ class ProjectsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :remove_project, @project)
::Projects::DestroyService.new(@project, current_user, {}).async_execute
- flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.name_with_namespace }
+ flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
redirect_to dashboard_projects_path, status: 302
rescue Projects::DestroyService::DestroyError => ex
@@ -359,6 +359,14 @@ class ProjectsController < Projects::ApplicationController
]
end
+ def custom_import_params
+ {}
+ end
+
+ def active_new_project_tab
+ project_params[:import_url].present? ? 'import' : 'blank'
+ end
+
def repo_exists?
project.repository_exists? && !project.empty_repo?
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index c73306a6b66..f3a4aa849c7 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -16,7 +16,7 @@ class SessionsController < Devise::SessionsController
def new
set_minimum_password_length
- @ldap_servers = Gitlab::LDAP::Config.available_servers
+ @ldap_servers = Gitlab::Auth::LDAP::Config.available_servers
super
end
diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb
index 852eac3647d..8bb1366867c 100644
--- a/app/finders/branches_finder.rb
+++ b/app/finders/branches_finder.rb
@@ -1,5 +1,5 @@
class BranchesFinder
- def initialize(repository, params)
+ def initialize(repository, params = {})
@repository = repository
@params = params
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 9dd6634b38f..b2d4f9938ff 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -19,6 +19,10 @@
# non_archived: boolean
# iids: integer[]
# my_reaction_emoji: string
+# created_after: datetime
+# created_before: datetime
+# updated_after: datetime
+# updated_before: datetime
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
@@ -79,6 +83,7 @@ class IssuableFinder
def filter_items(items)
items = by_scope(items)
items = by_created_at(items)
+ items = by_updated_at(items)
items = by_state(items)
items = by_group(items)
items = by_search(items)
@@ -283,6 +288,13 @@ class IssuableFinder
end
end
+ def by_updated_at(items)
+ items = items.updated_after(params[:updated_after]) if params[:updated_after].present?
+ items = items.updated_before(params[:updated_before]) if params[:updated_before].present?
+
+ items
+ end
+
def by_state(items)
case params[:state].to_s
when 'closed'
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index d65c620e75a..2a27ff0e386 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -17,6 +17,10 @@
# my_reaction_emoji: string
# public_only: boolean
# due_date: date or '0', '', 'overdue', 'week', or 'month'
+# created_after: datetime
+# created_before: datetime
+# updated_after: datetime
+# updated_before: datetime
#
class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 5c9fce211ec..780c0fdb03e 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -61,12 +61,20 @@ class LabelsFinder < UnionFinder
def group_ids
strong_memoize(:group_ids) do
- group = Group.find(params[:group_id])
- groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group]
- groups_user_can_read_labels(groups).map(&:id)
+ groups_user_can_read_labels(groups_to_include).map(&:id)
end
end
+ def groups_to_include
+ group = Group.find(params[:group_id])
+ groups = [group]
+
+ groups += group.ancestors if params[:include_ancestor_groups].present?
+ groups += group.descendants if params[:include_descendant_groups].present?
+
+ groups
+ end
+
def group?
params[:group_id].present?
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index d0687d28c21..64dc1e6af0f 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -17,14 +17,46 @@
# sort: string
# non_archived: boolean
# my_reaction_emoji: string
+# source_branch: string
+# target_branch: string
+# created_after: datetime
+# created_before: datetime
+# updated_after: datetime
+# updated_before: datetime
#
class MergeRequestsFinder < IssuableFinder
def klass
MergeRequest
end
+ def filter_items(_items)
+ items = by_source_branch(super)
+
+ by_target_branch(items)
+ end
+
private
+ def source_branch
+ @source_branch ||= params[:source_branch].presence
+ end
+
+ def by_source_branch(items)
+ return items unless source_branch
+
+ items.where(source_branch: source_branch)
+ end
+
+ def target_branch
+ @target_branch ||= params[:target_branch].presence
+ end
+
+ def by_target_branch(items)
+ return items unless target_branch
+
+ items.where(target_branch: target_branch)
+ end
+
def item_project_ids(items)
items&.reorder(nil)&.select(:target_project_id)
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 33ee1e975b9..35f4ff2f62f 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -48,11 +48,23 @@ class NotesFinder
def init_collection
if target
notes_on_target
+ elsif target_type
+ notes_of_target_type
else
notes_of_any_type
end
end
+ def notes_of_target_type
+ notes = notes_for_type(target_type)
+
+ search(notes)
+ end
+
+ def target_type
+ @params[:target_type]
+ end
+
def notes_of_any_type
types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) }
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index a73c573736e..d498a2d6d11 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -58,11 +58,37 @@ class SnippetsFinder < UnionFinder
.public_or_visible_to_user(current_user)
end
+ # Returns a collection of projects that is either public or visible to the
+ # logged in user.
+ #
+ # A caller must pass in a block to modify individual parts of
+ # the query, e.g. to apply .with_feature_available_for_user on top of it.
+ # This is useful for performance as we can stick those additional filters
+ # at the bottom of e.g. the UNION.
+ def projects_for_user
+ return yield(Project.public_to_user) unless current_user
+
+ # If the current_user is allowed to see all projects,
+ # we can shortcut and just return.
+ return yield(Project.all) if current_user.full_private_access?
+
+ authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects))
+
+ levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
+ visible_projects = yield(Project.where(visibility_level: levels))
+
+ # We use a UNION here instead of OR clauses since this results in better
+ # performance.
+ union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
+
+ Project.from("(#{union.to_sql}) AS #{Project.table_name}")
+ end
+
def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project
return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project)
- projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part|
+ projects = projects_for_user do |part|
part.with_feature_available_for_user(:snippets, current_user)
end.select(:id)
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index edb17843002..150f4c7688b 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -110,10 +110,6 @@ class TodosFinder
ids
end
- def projects(items)
- ProjectsFinder.new(current_user: current_user, project_ids_relation: project_ids(items)).execute
- end
-
def type?
type.present? && %w(Issue MergeRequest).include?(type)
end
@@ -152,13 +148,12 @@ class TodosFinder
def by_project(items)
if project?
- items = items.where(project: project)
+ items.where(project: project)
else
- item_projects = projects(items)
- items = items.merge(item_projects).joins(:project)
- end
+ projects = Project.public_or_visible_to_user(current_user)
- items
+ items.joins(:project).merge(projects)
+ end
end
def by_state(items)
diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb
index 6f7f7c30d92..65d6e019746 100644
--- a/app/finders/user_recent_events_finder.rb
+++ b/app/finders/user_recent_events_finder.rb
@@ -12,6 +12,8 @@ class UserRecentEventsFinder
attr_reader :current_user, :target_user, :params
+ LIMIT = 20
+
def initialize(current_user, target_user, params = {})
@current_user = current_user
@target_user = target_user
@@ -19,15 +21,44 @@ class UserRecentEventsFinder
end
def execute
- target_user
- .recent_events
- .merge(projects_for_current_user)
- .references(:project)
+ recent_events(params[:offset] || 0)
+ .joins(:project)
.with_associations
- .limit_recent(20, params[:offset])
+ .limit_recent(LIMIT, params[:offset])
+ end
+
+ private
+
+ def recent_events(offset)
+ sql = <<~SQL
+ (#{projects}) AS projects_for_join
+ JOIN (#{target_events.to_sql}) AS #{Event.table_name}
+ ON #{Event.table_name}.project_id = projects_for_join.id
+ SQL
+
+ # Workaround for https://github.com/rails/rails/issues/24193
+ Event.from([Arel.sql(sql)])
end
- def projects_for_current_user
- ProjectsFinder.new(current_user: current_user).execute
+ def target_events
+ Event.where(author: target_user)
+ end
+
+ def projects
+ # Compile a list of projects `current_user` interacted with
+ # and `target_user` is allowed to see.
+
+ authorized = target_user
+ .project_interactions
+ .joins(:project_authorizations)
+ .where(project_authorizations: { user: current_user })
+ .select(:id)
+
+ visible = target_user
+ .project_interactions
+ .where(visibility_level: [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC])
+ .select(:id)
+
+ Gitlab::SQL::Union.new([authorized, visible]).to_sql
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 475341cf9b1..af9c8bf1bd3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -320,10 +320,6 @@ module ApplicationHelper
cookies["sidebar_collapsed"] == "true"
end
- def show_new_ide?
- cookies["new_repo"] == "true" && body_data_page != 'projects:show'
- end
-
def locale_path
asset_path("locale/#{Gitlab::I18n.locale}/app.js")
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index ab68ecad2ba..4c4d7cca8a5 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -77,7 +77,7 @@ module ApplicationSettingsHelper
label_tag(checkbox_name, class: css_class) do
check_box_tag(checkbox_name, source, !disabled,
- autocomplete: 'off') + Gitlab::OAuth::Provider.label_for(source)
+ autocomplete: 'off') + Gitlab::Auth::OAuth::Provider.label_for(source)
end
end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index f909f664034..c109954f3a3 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -3,7 +3,7 @@ module AuthHelper
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
- Gitlab::LDAP::Config.enabled?
+ Gitlab::Auth::LDAP::Config.enabled?
end
def omniauth_enabled?
@@ -15,11 +15,11 @@ module AuthHelper
end
def auth_providers
- Gitlab::OAuth::Provider.providers
+ Gitlab::Auth::OAuth::Provider.providers
end
def label_for_provider(name)
- Gitlab::OAuth::Provider.label_for(name)
+ Gitlab::Auth::OAuth::Provider.label_for(name)
end
def form_based_provider?(name)
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 0e806d16bc5..5ff09b23a78 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -33,20 +33,6 @@ module BlobHelper
ref)
end
- def ide_edit_button(project = @project, ref = @ref, path = @path, options = {})
- return unless show_new_ide?
- return unless blob = readable_blob(options, path, project, ref)
-
- common_classes = "btn js-edit-ide #{options[:extra_class]}"
-
- edit_button_tag(blob,
- common_classes,
- _('Web IDE'),
- ide_edit_path(project, ref, path, options),
- project,
- ref)
- end
-
def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 12b3d9bac1a..275e892b2e6 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -17,23 +17,35 @@ module BoardsHelper
end
def build_issue_link_base
- project_issues_path(@project)
+ if board.group_board?
+ "#{group_path(@board.group)}/:project_path/issues"
+ else
+ project_issues_path(@project)
+ end
end
def board_base_url
- project_boards_path(@project)
+ if board.group_board?
+ group_boards_url(@group)
+ else
+ project_boards_path(@project)
+ end
end
def multiple_boards_available?
- current_board_parent.multiple_issue_boards_available?(current_user)
+ current_board_parent.multiple_issue_boards_available?
end
def current_board_path(board)
- @current_board_path ||= project_board_path(current_board_parent, board)
+ @current_board_path ||= if board.group_board?
+ group_board_path(current_board_parent, board)
+ else
+ project_board_path(current_board_parent, board)
+ end
end
def current_board_parent
- @current_board_parent ||= @project
+ @current_board_parent ||= @group || @project
end
def can_admin_issue?
@@ -47,7 +59,8 @@ module BoardsHelper
labels: labels_filter_path(true),
labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path,
- project_path: @project&.try(:path)
+ project_path: @project&.path,
+ group_path: @group&.path
}
end
@@ -59,7 +72,8 @@ module BoardsHelper
field_name: 'issue[assignee_ids][]',
first_user: current_user&.username,
current_user: 'true',
- project_id: @project&.try(:id),
+ project_id: @project&.id,
+ group_id: @group&.id,
null_user: 'true',
multi_select: 'true',
'dropdown-header': dropdown_options[:data][:'dropdown-header'],
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 00b9a0e00eb..07b1fc3d7cf 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -1,15 +1,4 @@
module BranchesHelper
- def filter_branches_path(options = {})
- exist_opts = {
- search: params[:search],
- sort: params[:sort]
- }
-
- options = exist_opts.merge(options)
-
- project_branches_path(@project, @id, options)
- end
-
def project_branches
options_for_select(@project.repository.branch_names, @project.default_branch)
end
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index e26ce6da030..905e2002592 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -27,7 +27,7 @@ module FormHelper
first_user: current_user&.username,
null_user: true,
current_user: true,
- project_id: @project.id,
+ project_id: @project&.id,
field_name: 'issue[assignee_ids][]',
default_label: 'Unassigned',
'max-select': 1,
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 5fbaa17c40e..16eceb3f48f 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -19,6 +19,20 @@ module GroupsHelper
can?(current_user, :change_share_with_group_lock, group)
end
+ def group_issues_count(state:)
+ IssuesFinder
+ .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
+ .execute
+ .count
+ end
+
+ def group_merge_requests_count(state:)
+ MergeRequestsFinder
+ .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
+ .execute
+ .count
+ end
+
def group_icon(group, options = {})
img_path = group_icon_url(group, options)
image_tag img_path, options
@@ -77,10 +91,6 @@ module GroupsHelper
end
end
- def group_issues(group)
- IssuesFinder.new(current_user, group_id: group.id).execute
- end
-
def remove_group_message(group)
_("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
@@ -119,7 +129,7 @@ module GroupsHelper
links = [:overview, :group_members]
if can?(current_user, :read_cross_project)
- links += [:activity, :issues, :labels, :milestones, :merge_requests]
+ links += [:activity, :issues, :boards, :labels, :milestones, :merge_requests]
end
if can?(current_user, :admin_group, @group)
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index a18ebfb6030..9149d79ecb8 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -1,19 +1,81 @@
module ImportHelper
+ def has_ci_cd_only_params?
+ false
+ end
+
def import_project_target(owner, name)
namespace = current_user.can_create_group? ? owner : current_user.namespace_path
"#{namespace}/#{name}"
end
- def provider_project_link(provider, path_with_namespace)
- url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend
+ def provider_project_link(provider, full_path)
+ url = __send__("#{provider}_project_url", full_path) # rubocop:disable GitlabSecurity/PublicSend
+
+ link_to full_path, url, target: '_blank', rel: 'noopener noreferrer'
+ end
+
+ def import_will_timeout_message(_ci_cd_only)
+ timeout = time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)
+ _('The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination.') % { timeout: timeout }
+ end
+
+ def import_svn_message(_ci_cd_only)
+ svn_link = link_to _('this document'), help_page_path('user/project/import/svn')
+ _('To import an SVN repository, check out %{svn_link}.').html_safe % { svn_link: svn_link }
+ end
+
+ def import_in_progress_title
+ if @project.forked?
+ _('Forking in progress')
+ else
+ _('Import in progress')
+ end
+ end
+
+ def import_wait_and_refresh_message
+ _('Please wait while we import the repository for you. Refresh at will.')
+ end
+
+ def import_github_title
+ _('Import repositories from GitHub')
+ end
+
+ def import_github_authorize_message
+ _('To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:')
+ end
+
+ def import_github_personal_access_token_message
+ personal_access_token_link = link_to _('Personal Access Token'), 'https://github.com/settings/tokens'
+
+ if github_import_configured?
+ _('Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { personal_access_token_link: personal_access_token_link }
+ else
+ _('To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { personal_access_token_link: personal_access_token_link }
+ end
+ end
+
+ def import_configure_github_admin_message
+ github_integration_link = link_to 'GitHub integration', help_page_path('integration/github')
+
+ if current_user.admin?
+ _('Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link }
+ else
+ _('Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link }
+ end
+ end
+
+ def import_githubish_choose_repository_message
+ _('Choose which repositories you want to import.')
+ end
- link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
+ def import_all_githubish_repositories_button_label
+ _('Import all repositories')
end
private
- def github_project_url(path_with_namespace)
- "#{github_root_url}/#{path_with_namespace}"
+ def github_project_url(full_path)
+ "#{github_root_url}/#{full_path}"
end
def github_root_url
@@ -23,7 +85,7 @@ module ImportHelper
@github_url = provider.fetch('url', 'https://github.com') if provider
end
- def gitea_project_url(path_with_namespace)
- "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}"
+ def gitea_project_url(full_path)
+ "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{full_path}"
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 44ecc2212f2..f6ddb6d4cfe 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -99,7 +99,7 @@ module IssuablesHelper
project = Project.find_by(id: project_id)
if project
- project.name_with_namespace
+ project.full_name
else
default_label
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index c1c19062c91..b2c641a5dbd 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -1,4 +1,5 @@
module LabelsHelper
+ extend self
include ActionView::Helpers::TagHelper
def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index ce57422f45d..fb4fe1c40b7 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -125,6 +125,19 @@ module MergeRequestsHelper
link_to(url[merge_request.project, merge_request], data: data_attrs, &block)
end
+ def allow_maintainer_push_unavailable_reason(merge_request)
+ return if merge_request.can_allow_maintainer_to_push?(current_user)
+
+ minimum_visibility = [merge_request.target_project.visibility_level,
+ merge_request.source_project.visibility_level].min
+
+ if minimum_visibility < Gitlab::VisibilityLevel::INTERNAL
+ _('Not available for private projects')
+ elsif ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch)
+ _('Not available for protected branches')
+ end
+ end
+
def merge_params_ee(merge_request)
{}
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index c219aa3d6a9..a70e73a6da9 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -11,7 +11,7 @@ module NotesHelper
end
def note_supports_quick_actions?(note)
- Notes::QuickActionsService.supported?(note, current_user)
+ Notes::QuickActionsService.supported?(note)
end
def noteable_json(noteable)
@@ -151,7 +151,38 @@ module NotesHelper
}
end
+ def notes_data(issuable)
+ discussions_path =
+ if issuable.is_a?(Issue)
+ discussions_project_issue_path(@project, issuable, format: :json)
+ else
+ discussions_project_merge_request_path(@project, issuable, format: :json)
+ end
+
+ {
+ discussionsPath: discussions_path,
+ registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
+ newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
+ markdownDocsPath: help_page_path('user/markdown'),
+ quickActionsDocsPath: help_page_path('user/project/quick_actions'),
+ closePath: close_issuable_path(issuable),
+ reopenPath: reopen_issuable_path(issuable),
+ notesPath: notes_url,
+ totalNotes: issuable.discussions.length,
+ lastFetchedAt: Time.now
+
+ }.to_json
+ end
+
def discussion_resolved_intro(discussion)
discussion.resolved_by_push? ? 'Automatically resolved' : 'Resolved'
end
+
+ def has_vue_discussions_cookie?
+ cookies[:vue_mr_discussions] == 'true'
+ end
+
+ def serialize_notes?
+ has_vue_discussions_cookie? && !params['html']
+ end
end
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 5a4fda0724c..e7aa92e6e5c 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -3,7 +3,7 @@ module ProfilesHelper
user_synced_attributes_metadata = current_user.user_synced_attributes_metadata
if user_synced_attributes_metadata&.synced?(attribute)
if user_synced_attributes_metadata.provider
- Gitlab::OAuth::Provider.label_for(user_synced_attributes_metadata.provider)
+ Gitlab::Auth::OAuth::Provider.label_for(user_synced_attributes_metadata.provider)
else
'LDAP'
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index cc1c69a1999..da9fe734f1c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -97,13 +97,13 @@ module ProjectsHelper
end
def remove_project_message(project)
- _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
- { project_name_with_namespace: project.name_with_namespace }
+ _("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
+ { project_full_name: project.full_name }
end
def transfer_project_message(project)
- _("You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?") %
- { project_name_with_namespace: project.name_with_namespace }
+ _("You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?") %
+ { project_full_name: project.full_name }
end
def remove_fork_project_message(project)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index e6a6496871a..761c1252fc8 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -110,7 +110,7 @@ module SearchHelper
category: "Projects",
id: p.id,
value: "#{search_result_sanitize(p.name)}",
- label: "#{search_result_sanitize(p.name_with_namespace)}",
+ label: "#{search_result_sanitize(p.full_name)}",
url: project_path(p)
}
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index ddb48371c79..f7620e0b6b8 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -114,7 +114,7 @@ module TodosHelper
projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route
projects = projects.map do |project|
- { id: project.id, text: project.name_with_namespace }
+ { id: project.id, text: project.full_name }
end
projects.unshift({ id: '', text: 'Any Project' }).to_json
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index f6a6d9bebde..b64be89c181 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -49,15 +49,13 @@ module TreeHelper
return false unless on_top_of_branch?(project, ref)
- can_collaborate_with_project?(project)
+ can_collaborate_with_project?(project, ref: ref)
end
def tree_edit_branch(project = @project, ref = @ref)
return unless can_edit_tree?(project, ref)
- project = project.present(current_user: current_user)
-
- if project.can_current_user_push_to_branch?(ref)
+ if user_access(project).can_push_to_branch?(ref)
ref
else
project = tree_edit_project(project)
@@ -88,7 +86,16 @@ module TreeHelper
end
def commit_in_fork_help
- "A new branch will be created in your fork and a new merge request will be started."
+ _("A new branch will be created in your fork and a new merge request will be started.")
+ end
+
+ def commit_in_single_accessible_branch
+ branch_name = html_escape(selected_branch)
+
+ message = _("Your changes can be committed to %{branch_name} because a merge "\
+ "request is open.") % { branch_name: "<strong>#{branch_name}</strong>" }
+
+ message.html_safe
end
def path_breadcrumbs(max_links = 6)
@@ -125,4 +132,8 @@ module TreeHelper
return tree.name
end
end
+
+ def selected_branch
+ @branch_name || tree_edit_branch
+ end
end
diff --git a/app/helpers/u2f_helper.rb b/app/helpers/u2f_helper.rb
deleted file mode 100644
index 81bfe5d4eeb..00000000000
--- a/app/helpers/u2f_helper.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module U2fHelper
- def inject_u2f_api?
- ((browser.chrome? && browser.version.to_i >= 41) || (browser.opera? && browser.version.to_i >= 40)) && !browser.device.mobile?
- end
-end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 45d4fb451d8..e4212775956 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -117,7 +117,7 @@ class Notify < BaseMailer
if Gitlab::IncomingEmail.enabled? && @sent_notification
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
- address.display_name = @project.name_with_namespace
+ address.display_name = @project.full_name
headers['Reply-To'] = address
diff --git a/app/models/badge.rb b/app/models/badge.rb
new file mode 100644
index 00000000000..f7e10c2ebfc
--- /dev/null
+++ b/app/models/badge.rb
@@ -0,0 +1,51 @@
+class Badge < ActiveRecord::Base
+ # This structure sets the placeholders that the urls
+ # can have. This hash also sets which action to ask when
+ # the placeholder is found.
+ PLACEHOLDERS = {
+ 'project_path' => :full_path,
+ 'project_id' => :id,
+ 'default_branch' => :default_branch,
+ 'commit_sha' => ->(project) { project.commit&.sha }
+ }.freeze
+
+ # This regex is built dynamically using the keys from the PLACEHOLDER struct.
+ # So, we can easily add new placeholder just by modifying the PLACEHOLDER hash.
+ # This regex will build the new PLACEHOLDER_REGEX with the new information
+ PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/.freeze
+
+ default_scope { order_created_at_asc }
+
+ scope :order_created_at_asc, -> { reorder(created_at: :asc) }
+
+ validates :link_url, :image_url, url_placeholder: { protocols: %w(http https), placeholder_regex: PLACEHOLDERS_REGEX }
+ validates :type, presence: true
+
+ def rendered_link_url(project = nil)
+ build_rendered_url(link_url, project)
+ end
+
+ def rendered_image_url(project = nil)
+ build_rendered_url(image_url, project)
+ end
+
+ private
+
+ def build_rendered_url(url, project = nil)
+ return url unless valid? && project
+
+ Gitlab::StringPlaceholderReplacer.replace_string_placeholders(url, PLACEHOLDERS_REGEX) do |arg|
+ replace_placeholder_action(PLACEHOLDERS[arg], project)
+ end
+ end
+
+ # The action param represents the :symbol or Proc to call in order
+ # to retrieve the return value from the project.
+ # This method checks if it is a Proc and use the call method, and if it is
+ # a symbol just send the action
+ def replace_placeholder_action(action, project)
+ return unless project
+
+ action.is_a?(Proc) ? action.call(project) : project.public_send(action) # rubocop:disable GitlabSecurity/PublicSend
+ end
+end
diff --git a/app/models/badges/group_badge.rb b/app/models/badges/group_badge.rb
new file mode 100644
index 00000000000..f4b2bdecdcc
--- /dev/null
+++ b/app/models/badges/group_badge.rb
@@ -0,0 +1,5 @@
+class GroupBadge < Badge
+ belongs_to :group
+
+ validates :group, presence: true
+end
diff --git a/app/models/badges/project_badge.rb b/app/models/badges/project_badge.rb
new file mode 100644
index 00000000000..3945b376052
--- /dev/null
+++ b/app/models/badges/project_badge.rb
@@ -0,0 +1,15 @@
+class ProjectBadge < Badge
+ belongs_to :project
+
+ validates :project, presence: true
+
+ def rendered_link_url(project = nil)
+ project ||= self.project
+ super
+ end
+
+ def rendered_image_url(project = nil)
+ project ||= self.project
+ super
+ end
+end
diff --git a/app/models/board.rb b/app/models/board.rb
index 5bb7d3d3722..3cede6fc99a 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -1,20 +1,22 @@
class Board < ActiveRecord::Base
+ belongs_to :group
belongs_to :project
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
validates :project, presence: true, if: :project_needed?
+ validates :group, presence: true, unless: :project
def project_needed?
- true
+ !group
end
def parent
- project
+ @parent ||= group || project
end
def group_board?
- false
+ group_id.present?
end
def backlog_list
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index f321db75eeb..fbd0f123341 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -1,4 +1,6 @@
class ChatName < ActiveRecord::Base
+ LAST_USED_AT_INTERVAL = 1.hour
+
belongs_to :service
belongs_to :user
@@ -9,4 +11,23 @@ class ChatName < ActiveRecord::Base
validates :user_id, uniqueness: { scope: [:service_id] }
validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
+
+ # Updates the "last_used_timestamp" but only if it wasn't already updated
+ # recently.
+ #
+ # The throttling this method uses is put in place to ensure that high chat
+ # traffic doesn't result in many UPDATE queries being performed.
+ def update_last_used_at
+ return unless update_last_used_at?
+
+ obtained = Gitlab::ExclusiveLease
+ .new("chat_name/last_used_at/#{id}", timeout: LAST_USED_AT_INTERVAL.to_i)
+ .try_obtain
+
+ touch(:last_used_at) if obtained
+ end
+
+ def update_last_used_at?
+ last_used_at.nil? || last_used_at > LAST_USED_AT_INTERVAL.ago
+ end
end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index afeae69ba39..1dd0e050ba9 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -6,7 +6,10 @@ module Ci
belongs_to :group
- validates :key, uniqueness: { scope: :group_id }
+ validates :key, uniqueness: {
+ scope: :group_id,
+ message: "(%{value}) has already been taken"
+ }
scope :unprotected, -> { where(protected: false) }
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 13c784bea0d..609620a62bb 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -49,7 +49,7 @@ module Ci
ref_protected: 1
}
- cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at
+ cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
# Searches for runners matching the given query.
#
@@ -157,7 +157,7 @@ module Ci
end
def update_cached_info(values)
- values = values&.slice(:version, :revision, :platform, :architecture) || {}
+ values = values&.slice(:version, :revision, :platform, :architecture, :ip_address) || {}
values[:contacted_at] = Time.now
cache_attributes(values)
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 67d3ec81b6f..7c71291de84 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -6,7 +6,10 @@ module Ci
belongs_to :project
- validates :key, uniqueness: { scope: [:project_id, :environment_scope] }
+ validates :key, uniqueness: {
+ scope: [:project_id, :environment_scope],
+ message: "(%{value}) has already been taken"
+ }
scope :unprotected, -> { where(protected: false) }
end
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index 193bb48e54d..58de3448577 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -15,7 +15,7 @@ module Clusters
end
def install_command
- Gitlab::Kubernetes::Helm::InstallCommand.new(name, install_helm: true)
+ Gitlab::Kubernetes::Helm::InitCommand.new(name)
end
end
end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index aa5cf97756f..27fc3b85465 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -5,6 +5,8 @@ module Clusters
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationData
+ include AfterCommitQueue
default_value_for :ingress_type, :nginx
default_value_for :version, :nginx
@@ -13,16 +15,34 @@ module Clusters
nginx: 1
}
+ FETCH_IP_ADDRESS_DELAY = 30.seconds
+
+ state_machine :status do
+ before_transition any => [:installed] do |application|
+ application.run_after_commit do
+ ClusterWaitForIngressIpAddressWorker.perform_in(
+ FETCH_IP_ADDRESS_DELAY, application.name, application.id)
+ end
+ end
+ end
+
def chart
'stable/nginx-ingress'
end
- def chart_values_file
- "#{Rails.root}/vendor/#{name}/values.yaml"
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name,
+ chart: chart,
+ values: values
+ )
end
- def install_command
- Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
+ def schedule_status_update
+ return unless installed?
+ return if external_ip
+
+ ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
end
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index aa22e9d5d58..7b25d8c4089 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -1,12 +1,15 @@
module Clusters
module Applications
class Prometheus < ActiveRecord::Base
+ include PrometheusAdapter
+
VERSION = "2.0.0".freeze
self.table_name = 'clusters_applications_prometheus'
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationData
default_value_for :version, VERSION
@@ -30,15 +33,15 @@ module Clusters
80
end
- def chart_values_file
- "#{Rails.root}/vendor/#{name}/values.yaml"
- end
-
def install_command
- Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name,
+ chart: chart,
+ values: values
+ )
end
- def proxy_client
+ def prometheus_client
return unless kube_client
proxy_url = kube_client.proxy_url('service', service_name, service_port, Gitlab::Kubernetes::Helm::NAMESPACE)
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
new file mode 100644
index 00000000000..16efe90fa27
--- /dev/null
+++ b/app/models/clusters/applications/runner.rb
@@ -0,0 +1,69 @@
+module Clusters
+ module Applications
+ class Runner < ActiveRecord::Base
+ VERSION = '0.1.13'.freeze
+
+ self.table_name = 'clusters_applications_runners'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationData
+
+ belongs_to :runner, class_name: 'Ci::Runner', foreign_key: :runner_id
+ delegate :project, to: :cluster
+
+ default_value_for :version, VERSION
+
+ def chart
+ "#{name}/gitlab-runner"
+ end
+
+ def repository
+ 'https://charts.gitlab.io'
+ end
+
+ def values
+ content_values.to_yaml
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name,
+ chart: chart,
+ values: values,
+ repository: repository
+ )
+ end
+
+ private
+
+ def ensure_runner
+ runner || create_and_assign_runner
+ end
+
+ def create_and_assign_runner
+ transaction do
+ project.runners.create!(name: 'kubernetes-cluster', tag_list: %w(kubernetes cluster)).tap do |runner|
+ update!(runner_id: runner.id)
+ end
+ end
+ end
+
+ def gitlab_url
+ Gitlab::Routing.url_helpers.root_url(only_path: false)
+ end
+
+ def specification
+ {
+ "gitlabUrl" => gitlab_url,
+ "runnerToken" => ensure_runner.token,
+ "runners" => { "privileged" => privileged }
+ }
+ end
+
+ def content_values
+ YAML.load_file(chart_values_file).deep_merge!(specification)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 8678f70f78c..49eb069016a 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -7,7 +7,8 @@ module Clusters
APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress,
- Applications::Prometheus.application_name => Applications::Prometheus
+ Applications::Prometheus.application_name => Applications::Prometheus,
+ Applications::Runner.application_name => Applications::Runner
}.freeze
belongs_to :user
@@ -23,6 +24,7 @@ module Clusters
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
+ has_one :application_runner, class_name: 'Clusters::Applications::Runner'
accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
@@ -49,9 +51,6 @@ module Clusters
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
- scope :for_environment, -> (env) { where(environment_scope: ['*', '', env.slug]) }
- scope :for_all_environments, -> { where(environment_scope: ['*', '']) }
-
def status_name
if provider
provider.status_name
@@ -68,7 +67,8 @@ module Clusters
[
application_helm || build_application_helm,
application_ingress || build_application_ingress,
- application_prometheus || build_application_prometheus
+ application_prometheus || build_application_prometheus,
+ application_runner || build_application_runner
]
end
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
index a98fa85a5ff..623b836c0ed 100644
--- a/app/models/clusters/concerns/application_core.rb
+++ b/app/models/clusters/concerns/application_core.rb
@@ -23,6 +23,11 @@ module Clusters
def name
self.class.application_name
end
+
+ def schedule_status_update
+ # Override if you need extra data synchronized
+ # from K8s after installation
+ end
end
end
end
diff --git a/app/models/clusters/concerns/application_data.rb b/app/models/clusters/concerns/application_data.rb
new file mode 100644
index 00000000000..96ac757e99e
--- /dev/null
+++ b/app/models/clusters/concerns/application_data.rb
@@ -0,0 +1,23 @@
+module Clusters
+ module Concerns
+ module ApplicationData
+ extend ActiveSupport::Concern
+
+ included do
+ def repository
+ nil
+ end
+
+ def values
+ File.read(chart_values_file)
+ end
+
+ private
+
+ def chart_values_file
+ "#{Rails.root}/vendor/#{name}/values.yaml"
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index add5fcf0e79..cceae5efb72 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -9,6 +9,7 @@ class Commit
include Mentionable
include Referable
include StaticModel
+ include ::Gitlab::Utils::StrongMemoize
attr_mentionable :safe_message, pipeline: :single_line
@@ -19,6 +20,7 @@ class Commit
attr_accessor :project, :author
attr_accessor :redacted_description_html
attr_accessor :redacted_title_html
+ attr_reader :gpg_commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
@@ -110,6 +112,7 @@ class Commit
@raw = raw_commit
@project = project
@statuses = {}
+ @gpg_commit = Gitlab::Gpg::Commit.new(self) if project
end
def id
@@ -223,11 +226,13 @@ class Commit
end
def parents
- @parents ||= parent_ids.map { |id| project.commit(id) }
+ @parents ||= parent_ids.map { |oid| Commit.lazy(project, oid) }
end
def parent
- @parent ||= project.commit(self.parent_id) if self.parent_id
+ strong_memoize(:parent) do
+ project.commit_by(oid: self.parent_id) if self.parent_id
+ end
end
def notes
@@ -452,8 +457,4 @@ class Commit
def merged_merge_request_no_cache(user)
MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
end
-
- def gpg_commit
- @gpg_commit ||= Gitlab::Gpg::Commit.new(self)
- end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3469d5d795c..9fb5b7efec6 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base
end
def group_name
- name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip
+ name.to_s.gsub(%r{\d+[\.\s:/\\]+\d+\s*}, '').strip
end
def failed_but_allowed?
diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb
index 62bc6b809f4..d502e7e54c6 100644
--- a/app/models/concerns/access_requestable.rb
+++ b/app/models/concerns/access_requestable.rb
@@ -8,6 +8,6 @@ module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
- Members::RequestAccessService.new(self, user).execute
+ Members::RequestAccessService.new(user).execute(self)
end
end
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index 89d0474a596..faa94204e33 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -1,5 +1,6 @@
module DeploymentPlatform
- def deployment_platform
+ # EE would override this and utilize the extra argument
+ def deployment_platform(environment: nil)
@deployment_platform ||=
find_cluster_platform_kubernetes ||
find_kubernetes_service_integration ||
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 7049f340c9d..5a566f3ac02 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -19,6 +19,7 @@ module Issuable
include AfterCommitQueue
include Sortable
include CreatedAtFilterable
+ include UpdatedAtFilterable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
@@ -222,6 +223,10 @@ module Issuable
def to_ability_name
model_name.singular
end
+
+ def parent_class
+ ::Project
+ end
end
def today?
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
new file mode 100644
index 00000000000..18cbbd871a1
--- /dev/null
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -0,0 +1,48 @@
+module PrometheusAdapter
+ extend ActiveSupport::Concern
+
+ included do
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(adapter) { [adapter.class.model_name.singular, adapter.id] }
+ self.reactive_cache_lease_timeout = 30.seconds
+ self.reactive_cache_refresh_interval = 30.seconds
+ self.reactive_cache_lifetime = 1.minute
+
+ def prometheus_client
+ raise NotImplementedError
+ end
+
+ def prometheus_client_wrapper
+ Gitlab::PrometheusClient.new(prometheus_client)
+ end
+
+ def can_query?
+ prometheus_client.present?
+ end
+
+ def query(query_name, *args)
+ return unless can_query?
+
+ query_class = Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query")
+
+ args.map!(&:id)
+
+ with_reactive_cache(query_class.name, *args, &query_class.method(:transform_reactive_result))
+ end
+
+ # Cache metrics for specific environment
+ def calculate_reactive_cache(query_class_name, *args)
+ return unless prometheus_client
+
+ data = Kernel.const_get(query_class_name).new(prometheus_client_wrapper).query(*args)
+ {
+ success: true,
+ data: data,
+ last_update: Time.now.utc
+ }
+ rescue Gitlab::PrometheusClient::Error => err
+ { success: false, result: err.message }
+ end
+ end
+end
diff --git a/app/models/concerns/updated_at_filterable.rb b/app/models/concerns/updated_at_filterable.rb
new file mode 100644
index 00000000000..edb423b7828
--- /dev/null
+++ b/app/models/concerns/updated_at_filterable.rb
@@ -0,0 +1,12 @@
+module UpdatedAtFilterable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :updated_before, ->(date) { where(scoped_table[:updated_at].lteq(date)) }
+ scope :updated_after, ->(date) { where(scoped_table[:updated_at].gteq(date)) }
+
+ def self.scoped_table
+ arel_table.alias(table_name)
+ end
+ end
+end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index d2e626c22e8..b34d1382d43 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -6,6 +6,12 @@ class CycleAnalytics
@options = options
end
+ def all_medians_per_stage
+ STAGES.each_with_object({}) do |stage_name, medians_per_stage|
+ medians_per_stage[stage_name] = self[stage_name].median
+ end
+ end
+
def summary
@summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
from: @options[:from],
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/app/models/cycle_analytics/summary.rb
+++ /dev/null
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index b6cf168d60e..66e61c06765 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -98,28 +98,29 @@ class Deployment < ActiveRecord::Base
end
def has_metrics?
- project.monitoring_service.present?
+ prometheus_adapter&.can_query?
end
def metrics
return {} unless has_metrics?
- project.monitoring_service.deployment_metrics(self)
- end
-
- def has_additional_metrics?
- project.prometheus_service.present?
+ metrics = prometheus_adapter.query(:deployment, self)
+ metrics&.merge(deployment_time: created_at.to_i) || {}
end
def additional_metrics
- return {} unless project.prometheus_service.present?
+ return {} unless has_metrics?
- metrics = project.prometheus_service.additional_deployment_metrics(self)
+ metrics = prometheus_adapter.query(:additional_metrics_deployment, self)
metrics&.merge(deployment_time: created_at.to_i) || {}
end
private
+ def prometheus_adapter
+ environment.prometheus_adapter
+ end
+
def ref_path
File.join(environment.ref_path, 'deployments', iid.to_s)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index f78c21aebe5..2b0a88ac5b4 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -99,8 +99,8 @@ class Environment < ActiveRecord::Base
folder_name == "production"
end
- def first_deployment_for(commit)
- ref = project.repository.ref_name_for_sha(ref_path, commit.sha)
+ def first_deployment_for(commit_sha)
+ ref = project.repository.ref_name_for_sha(ref_path, commit_sha)
return nil unless ref
@@ -146,21 +146,19 @@ class Environment < ActiveRecord::Base
end
def has_metrics?
- project.monitoring_service.present? && available? && last_deployment.present?
+ prometheus_adapter&.can_query? && available? && last_deployment.present?
end
def metrics
- project.monitoring_service.environment_metrics(self) if has_metrics?
+ prometheus_adapter.query(:environment, self) if has_metrics?
end
- def has_additional_metrics?
- project.prometheus_service.present? && available? && last_deployment.present?
+ def additional_metrics
+ prometheus_adapter.query(:additional_metrics_environment, self) if has_metrics?
end
- def additional_metrics
- if has_additional_metrics?
- project.prometheus_service.additional_environment_metrics(self)
- end
+ def prometheus_adapter
+ @prometheus_adapter ||= Prometheus::AdapterService.new(project, deployment_platform).prometheus_adapter
end
def slug
@@ -226,6 +224,10 @@ class Environment < ActiveRecord::Base
self.environment_type || self.name
end
+ def deployment_platform
+ project.deployment_platform(environment: self)
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/app/models/event.rb b/app/models/event.rb
index 75538ba196c..17a198d52c7 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -65,6 +65,7 @@ class Event < ActiveRecord::Base
# Callbacks
after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push?
+ after_create :track_user_interacted_projects
# Scopes
scope :recent, -> { reorder(id: :desc) }
@@ -158,7 +159,7 @@ class Event < ActiveRecord::Base
def project_name
if project
- project.name_with_namespace
+ project.full_name
else
"(deleted project)"
end
@@ -389,4 +390,11 @@ class Event < ActiveRecord::Base
Project.unscoped.where(id: project_id)
.update_all(last_repository_updated_at: created_at)
end
+
+ def track_user_interacted_projects
+ # Note the call to .available? is due to earlier migrations
+ # that would otherwise conflict with the call to .track
+ # (because the table does not exist yet).
+ UserInteractedProject.track(self) if UserInteractedProject.available?
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 75bf013ecd2..8d183006c65 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -31,6 +31,9 @@ class Group < Namespace
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :boards
+ has_many :badges, class_name: 'GroupBadge'
+
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 2b433e9b988..1011b9f1109 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -17,12 +17,12 @@ class Identity < ActiveRecord::Base
end
def ldap?
- Gitlab::OAuth::Provider.ldap_provider?(provider)
+ Gitlab::Auth::OAuth::Provider.ldap_provider?(provider)
end
def self.normalize_uid(provider, uid)
- if Gitlab::OAuth::Provider.ldap_provider?(provider)
- Gitlab::LDAP::Person.normalize_dn(uid)
+ if Gitlab::Auth::OAuth::Provider.ldap_provider?(provider)
+ Gitlab::Auth::LDAP::Person.normalize_dn(uid)
else
uid.to_s
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 7538f2d8718..de7f1d56c64 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -35,6 +35,7 @@ class Label < ActiveRecord::Base
scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) }
scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
+ scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) }
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
def self.prioritized(project)
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index fc586fa216e..b444812a4cf 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -15,4 +15,8 @@ class LfsObject < ActiveRecord::Base
.where(lfs_objects_projects: { id: nil })
.destroy_all
end
+
+ def self.calculate_oid(path)
+ Digest::SHA256.file(path).hexdigest
+ end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 2d17795e62d..36090676051 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -85,6 +85,7 @@ class Member < ActiveRecord::Base
after_create :create_notification_setting, unless: [:pending?, :importing?]
after_create :post_create_hook, unless: [:pending?, :importing?]
after_update :post_update_hook, unless: [:pending?, :importing?]
+ after_destroy :destroy_notification_setting
after_destroy :post_destroy_hook, unless: :pending?
after_commit :refresh_member_authorized_projects
@@ -128,7 +129,7 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
end
- def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil)
+ def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
# `user` can be either a User object, User ID or an email to be invited
member = retrieve_member(source, user, existing_members)
access_level = retrieve_access_level(access_level)
@@ -143,11 +144,13 @@ class Member < ActiveRecord::Base
if member.request?
::Members::ApproveAccessRequestService.new(
- source,
current_user,
- id: member.id,
access_level: access_level
- ).execute
+ ).execute(
+ member,
+ skip_authorization: ldap,
+ skip_log_audit_event: ldap
+ )
else
member.save
end
@@ -313,6 +316,10 @@ class Member < ActiveRecord::Base
user.notification_settings.find_or_create_for(source)
end
+ def destroy_notification_setting
+ notification_setting&.destroy
+ end
+
def notification_setting
@notification_setting ||= user&.notification_settings_for(source)
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index b6f1dd272cd..1c7ed4a96df 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -13,8 +13,6 @@ class ProjectMember < Member
scope :in_project, ->(project) { where(source_id: project.id) }
- before_destroy :delete_member_todos
-
class << self
# Add users to projects with passed access option
#
@@ -93,10 +91,6 @@ class ProjectMember < Member
private
- def delete_member_todos
- user.todos.where(project_id: source_id).destroy_all if user
- end
-
def send_invite
notification_service.invite_project_member(self, @raw_invite_token)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 5bec68ce4f6..c2bae379a94 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -375,15 +375,27 @@ class MergeRequest < ActiveRecord::Base
end
def diff_start_sha
- diff_start_commit.try(:sha)
+ if persisted?
+ merge_request_diff.start_commit_sha
+ else
+ target_branch_head.try(:sha)
+ end
end
def diff_base_sha
- diff_base_commit.try(:sha)
+ if persisted?
+ merge_request_diff.base_commit_sha
+ else
+ branch_merge_base_commit.try(:sha)
+ end
end
def diff_head_sha
- diff_head_commit.try(:sha)
+ if persisted?
+ merge_request_diff.head_commit_sha
+ else
+ source_branch_head.try(:sha)
+ end
end
# When importing a pull request from GitHub, the old and new branches may no
@@ -646,7 +658,7 @@ class MergeRequest < ActiveRecord::Base
!ProtectedBranch.protected?(source_project, source_branch) &&
!source_project.root_ref?(source_branch) &&
Ability.allowed?(current_user, :push_code, source_project) &&
- diff_head_commit == source_branch_head
+ diff_head_sha == source_branch_head.try(:sha)
end
def should_remove_source_branch?
@@ -853,7 +865,7 @@ class MergeRequest < ActiveRecord::Base
def can_be_merged_by?(user)
access = ::Gitlab::UserAccess.new(user, project: project)
- access.can_push_to_branch?(target_branch) || access.can_merge_to_branch?(target_branch)
+ access.can_update_branch?(target_branch)
end
def can_be_merged_via_command_line_by?(user)
@@ -1075,4 +1087,22 @@ class MergeRequest < ActiveRecord::Base
project.merge_requests.merged.where(author_id: author_id).empty?
end
+
+ def allow_maintainer_to_push
+ maintainer_push_possible? && super
+ end
+
+ alias_method :allow_maintainer_to_push?, :allow_maintainer_to_push
+
+ def maintainer_push_possible?
+ source_project.present? && for_fork? &&
+ target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
+ source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
+ !ProtectedBranch.protected?(source_project, source_branch)
+ end
+
+ def can_allow_maintainer_to_push?(user)
+ maintainer_push_possible? &&
+ Ability.allowed?(user, :push_code, source_project)
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index db274ea8172..e350b675639 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -222,6 +222,11 @@ class Namespace < ActiveRecord::Base
has_parent?
end
+ # Overridden on EE module
+ def multiple_issue_boards_available?
+ false
+ end
+
def full_path_was
if parent_id_was.nil?
path_was
diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb
index 9357e55b419..22d48c9e661 100644
--- a/app/models/network/commit.rb
+++ b/app/models/network/commit.rb
@@ -24,12 +24,7 @@ module Network
end
def parents(map)
- @commit.parents.map do |p|
- if map.include?(p.id)
- map[p.id]
- end
- end
- .compact
+ map.values_at(*@commit.parent_ids).compact
end
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index cac60845a49..787a80f0196 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -81,7 +81,7 @@ class Note < ActiveRecord::Base
validates :author, presence: true
validates :discussion_id, presence: true, format: { with: /\A\h{40}\z/ }
- validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note|
+ validate unless: [:for_commit?, :importing?, :skip_project_check?] do |note|
unless note.noteable.try(:project) == note.project
errors.add(:project, 'does not match noteable project')
end
@@ -133,6 +133,7 @@ class Note < ActiveRecord::Base
def find_discussion(discussion_id)
notes = where(discussion_id: discussion_id).fresh.to_a
+
return if notes.empty?
Discussion.build(notes)
@@ -227,7 +228,7 @@ class Note < ActiveRecord::Base
end
def skip_project_check?
- for_personal_snippet?
+ !for_project_noteable?
end
def commit
@@ -307,6 +308,11 @@ class Note < ActiveRecord::Base
self.noteable.supports_discussions? && !part_of_discussion?
end
+ def can_create_todo?
+ # Skip system notes, and notes on project snippet
+ !system? && !for_snippet?
+ end
+
def discussion_class(noteable = nil)
# When commit notes are rendered on an MR's Discussion page, they are
# displayed in one discussion instead of individually.
diff --git a/app/models/project.rb b/app/models/project.rb
index ba278a49688..5f9d9785d64 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -150,6 +150,7 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
+ has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues
has_many :labels, class_name: 'ProjectLabel'
has_many :services
@@ -221,6 +222,8 @@ class Project < ActiveRecord::Base
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
+ has_many :project_badges, class_name: 'ProjectBadge'
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
@@ -274,7 +277,8 @@ class Project < ActiveRecord::Base
scope :without_storage_feature, ->(feature) { where('storage_version < :version OR storage_version IS NULL', version: HASHED_STORAGE_FEATURES[feature]) }
scope :with_unmigrated_storage, -> { where('storage_version < :version OR storage_version IS NULL', version: LATEST_STORAGE_VERSION) }
- scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
+ # last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push
+ scope :sorted_by_activity, -> { reorder("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC") }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
@@ -317,42 +321,13 @@ class Project < ActiveRecord::Base
# Returns a collection of projects that is either public or visible to the
# logged in user.
- #
- # A caller may pass in a block to modify individual parts of
- # the query, e.g. to apply .with_feature_available_for_user on top of it.
- # This is useful for performance as we can stick those additional filters
- # at the bottom of e.g. the UNION.
- #
- # Optionally, turning `use_where_in` off leads to returning a
- # relation using #from instead of #where. This can perform much better
- # but leads to trouble when used in conjunction with AR's #merge method.
- def self.public_or_visible_to_user(user = nil, use_where_in: true, &block)
- # If we don't get a block passed, use identity to avoid if/else repetitions
- block = ->(part) { part } unless block_given?
-
- return block.call(public_to_user) unless user
-
- # If the user is allowed to see all projects,
- # we can shortcut and just return.
- return block.call(all) if user.full_private_access?
-
- authorized = user
- .project_authorizations
- .select(1)
- .where('project_authorizations.project_id = projects.id')
- authorized_projects = block.call(where('EXISTS (?)', authorized))
-
- levels = Gitlab::VisibilityLevel.levels_for_user(user)
- visible_projects = block.call(where(visibility_level: levels))
-
- # We use a UNION here instead of OR clauses since this results in better
- # performance.
- union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
-
- if use_where_in
- where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ def self.public_or_visible_to_user(user = nil)
+ if user
+ where('EXISTS (?) OR projects.visibility_level IN (?)',
+ user.authorizations_for_projects,
+ Gitlab::VisibilityLevel.levels_for_user(user))
else
- from("(#{union.to_sql}) AS #{table_name}")
+ public_to_user
end
end
@@ -371,14 +346,11 @@ class Project < ActiveRecord::Base
elsif user
column = ProjectFeature.quoted_access_level_column(feature)
- authorized = user.project_authorizations.select(1)
- .where('project_authorizations.project_id = projects.id')
-
with_project_feature
.where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
visible,
ProjectFeature::PRIVATE,
- authorized)
+ user.authorizations_for_projects)
else
with_feature_access_level(feature, visible)
end
@@ -808,7 +780,7 @@ class Project < ActiveRecord::Base
end
def last_activity_date
- last_repository_updated_at || last_activity_at || updated_at
+ [last_activity_at, last_repository_updated_at, updated_at].compact.max
end
def project_id
@@ -1557,16 +1529,34 @@ class Project < ActiveRecord::Base
end
end
+ def import_export_shared
+ @import_export_shared ||= Gitlab::ImportExport::Shared.new(self)
+ end
+
def export_path
return nil unless namespace.present? || hashed_storage?(:repository)
- File.join(Gitlab::ImportExport.storage_path, disk_path)
+ import_export_shared.archive_path
end
def export_project_path
Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) }
end
+ def export_status
+ if export_in_progress?
+ :started
+ elsif export_project_path
+ :finished
+ else
+ :none
+ end
+ end
+
+ def export_in_progress?
+ import_export_shared.active_export_count > 0
+ end
+
def remove_exports
return nil unless export_path.present?
@@ -1695,8 +1685,9 @@ class Project < ActiveRecord::Base
end
end
- def multiple_issue_boards_available?(user)
- feature_available?(:multiple_issue_boards, user)
+ # Overridden on EE module
+ def multiple_issue_boards_available?
+ false
end
def issue_board_milestone_available?(user = nil)
@@ -1798,6 +1789,44 @@ class Project < ActiveRecord::Base
.set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
end
+ def badges
+ return project_badges unless group
+
+ group_badges_rel = GroupBadge.where(group: group.self_and_ancestors)
+
+ union = Gitlab::SQL::Union.new([project_badges.select(:id),
+ group_badges_rel.select(:id)])
+
+ Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ end
+
+ def merge_requests_allowing_push_to_user(user)
+ return MergeRequest.none unless user
+
+ developer_access_exists = user.project_authorizations
+ .where('access_level >= ? ', Gitlab::Access::DEVELOPER)
+ .where('project_authorizations.project_id = merge_requests.target_project_id')
+ .limit(1)
+ .select(1)
+ source_of_merge_requests.opened
+ .where(allow_maintainer_to_push: true)
+ .where('EXISTS (?)', developer_access_exists)
+ end
+
+ def branch_allows_maintainer_push?(user, branch_name)
+ return false unless user
+
+ cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push"
+
+ memoized_results = strong_memoize(:branch_allows_maintainer_push) do
+ Hash.new do |result, cache_key|
+ result[cache_key] = fetch_branch_allows_maintainer_push?(user, branch_name)
+ end
+ end
+
+ memoized_results[cache_key]
+ end
+
private
def storage
@@ -1920,4 +1949,22 @@ class Project < ActiveRecord::Base
raise ex
end
+
+ def fetch_branch_allows_maintainer_push?(user, branch_name)
+ check_access = -> do
+ merge_request = source_of_merge_requests.opened
+ .where(allow_maintainer_to_push: true)
+ .find_by(source_branch: branch_name)
+
+ merge_request&.can_be_merged_by?(user)
+ end
+
+ if RequestStore.active?
+ RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_maintainer_push") do
+ check_access.call
+ end
+ else
+ check_access.call
+ end
+ end
end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 109258d1eb7..4f289e6e215 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -68,7 +68,7 @@ http://app.asana.com/-/account_api'
end
user = data[:user_name]
- project_name = project.name_with_namespace
+ project_name = project.full_name
data[:commits].each do |commit|
push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):"
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index c3f5b310619..8d7a4fceb08 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -86,7 +86,7 @@ class CampfireService < Service
after = push[:after]
message = ""
- message << "[#{project.name_with_namespace}] "
+ message << "[#{project.full_name}] "
message << "#{push[:user_name]} "
if Gitlab::Git.blank_ref?(before)
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 818cfb01b14..dab0ea1a681 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -99,7 +99,7 @@ class ChatNotificationService < Service
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
- ChatMessage::PushMessage.new(data)
+ ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
when "issue"
ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
@@ -129,7 +129,7 @@ class ChatNotificationService < Service
end
def project_name
- project.name_with_namespace.gsub(/\s/, '')
+ project.full_name.gsub(/\s/, '')
end
def project_url
@@ -145,10 +145,16 @@ class ChatNotificationService < Service
end
def notify_for_ref?(data)
- return true if data[:object_attributes][:tag]
+ return true if data.dig(:object_attributes, :tag)
return true unless notify_only_default_branch?
- data[:object_attributes][:ref] == project.default_branch
+ ref = if data[:ref]
+ Gitlab::Git.ref_name(data[:ref])
+ else
+ data.dig(:object_attributes, :ref)
+ end
+
+ ref == project.default_branch
end
def notify_for_pipeline?(data)
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index bfe7ac29c18..f31c3f02af2 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -120,7 +120,7 @@ class HipchatService < Service
else
message << "pushed to #{ref_type} <a href=\""\
"#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> "
- message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/, '')}</a> "
+ message << "of <a href=\"#{project.web_url}\">#{project.full_name.gsub!(/\s/, '')}</a> "
message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
push[:commits].take(MAX_COMMITS).each do |commit|
@@ -274,7 +274,7 @@ class HipchatService < Service
end
def project_name
- project.name_with_namespace.gsub(/\s/, '')
+ project.full_name.gsub(/\s/, '')
end
def project_url
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 436a870b0c4..e5035c81df0 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,5 +1,7 @@
class JiraService < IssueTrackerService
include Gitlab::Routing
+ include ApplicationHelper
+ include ActionView::Helpers::AssetUrlHelper
validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
@@ -268,7 +270,9 @@ class JiraService < IssueTrackerService
url: url,
title: title,
status: status,
- icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' }
+ icon: {
+ title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url)
+ }
}
}
end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 4d2037286a2..227d430083d 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -37,7 +37,7 @@ class MattermostSlashCommandsService < SlashCommandsService
private
def command(params)
- pretty_project_name = project.name_with_namespace
+ pretty_project_name = project.full_name
params.merge(
auto_complete: true,
diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb
index ee9cd78327a..9af68b4e821 100644
--- a/app/models/project_services/monitoring_service.rb
+++ b/app/models/project_services/monitoring_service.rb
@@ -9,11 +9,11 @@ class MonitoringService < Service
%w()
end
- def environment_metrics(environment)
+ def can_query?
raise NotImplementedError
end
- def deployment_metrics(deployment)
+ def query(_, *_)
raise NotImplementedError
end
end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 58731451429..dcaeb65dc32 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -1,9 +1,5 @@
class PrometheusService < MonitoringService
- include ReactiveService
-
- self.reactive_cache_lease_timeout = 30.seconds
- self.reactive_cache_refresh_interval = 30.seconds
- self.reactive_cache_lifetime = 1.minute
+ include PrometheusAdapter
# Access to prometheus is directly through the API
prop_accessor :api_url
@@ -13,7 +9,7 @@ class PrometheusService < MonitoringService
validates :api_url, url: true
end
- before_save :synchronize_service_state!
+ before_save :synchronize_service_state
after_save :clear_reactive_cache!
@@ -66,63 +62,15 @@ class PrometheusService < MonitoringService
# Check we can connect to the Prometheus API
def test(*args)
- client.ping
+ Gitlab::PrometheusClient.new(prometheus_client).ping
{ success: true, result: 'Checked API endpoint' }
rescue Gitlab::PrometheusClient::Error => err
{ success: false, result: err }
end
- def environment_metrics(environment)
- with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &rename_field(:data, :metrics))
- end
-
- def deployment_metrics(deployment)
- metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.environment.id, deployment.id, &rename_field(:data, :metrics))
- metrics&.merge(deployment_time: deployment.created_at.to_i) || {}
- end
-
- def additional_environment_metrics(environment)
- with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery.name, environment.id, &:itself)
- end
-
- def additional_deployment_metrics(deployment)
- with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.environment.id, deployment.id, &:itself)
- end
-
- def matched_metrics
- with_reactive_cache(Gitlab::Prometheus::Queries::MatchedMetricsQuery.name, &:itself)
- end
-
- # Cache metrics for specific environment
- def calculate_reactive_cache(query_class_name, *args)
- return unless active? && project && !project.pending_delete?
-
- environment_id = args.first
- client = client(environment_id)
-
- data = Kernel.const_get(query_class_name).new(client).query(*args)
- {
- success: true,
- data: data,
- last_update: Time.now.utc
- }
- rescue Gitlab::PrometheusClient::Error => err
- { success: false, result: err.message }
- end
-
- def client(environment_id = nil)
- if manual_configuration?
- Gitlab::PrometheusClient.new(RestClient::Resource.new(api_url))
- else
- cluster = cluster_with_prometheus(environment_id)
- raise Gitlab::PrometheusClient::Error, "couldn't find cluster with Prometheus installed" unless cluster
-
- rest_client = client_from_cluster(cluster)
- raise Gitlab::PrometheusClient::Error, "couldn't create proxy Prometheus client" unless rest_client
-
- Gitlab::PrometheusClient.new(rest_client)
- end
+ def prometheus_client
+ RestClient::Resource.new(api_url) if api_url && manual_configuration? && active?
end
def prometheus_installed?
@@ -134,32 +82,7 @@ class PrometheusService < MonitoringService
private
- def cluster_with_prometheus(environment_id = nil)
- clusters = if environment_id
- ::Environment.find_by(id: environment_id).try do |env|
- # sort results by descending order based on environment_scope being longer
- # thus more closely matching environment slug
- project.clusters.enabled.for_environment(env).sort_by { |c| c.environment_scope&.length }.reverse!
- end
- else
- project.clusters.enabled.for_all_environments
- end
-
- clusters&.detect { |cluster| cluster.application_prometheus&.installed? }
- end
-
- def client_from_cluster(cluster)
- cluster.application_prometheus.proxy_client
- end
-
- def rename_field(old_field, new_field)
- -> (metrics) do
- metrics[new_field] = metrics.delete(old_field)
- metrics
- end
- end
-
- def synchronize_service_state!
+ def synchronize_service_state
self.active = prometheus_installed? || manual_configuration?
true
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index aa7bd4c3c84..e3a1ca2d45f 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -88,10 +88,10 @@ class PushoverService < Service
user: user_key,
device: device,
priority: priority,
- title: "#{project.name_with_namespace}",
+ title: "#{project.full_name}",
message: message,
url: data[:project][:web_url],
- url_title: "See project #{project.name_with_namespace}"
+ url_title: "See project #{project.full_name}"
}
# Sound parameter MUST NOT be sent to API if not selected
diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb
index eb4da68bb7e..37ea45109ae 100644
--- a/app/models/project_services/slash_commands_service.rb
+++ b/app/models/project_services/slash_commands_service.rb
@@ -30,10 +30,10 @@ class SlashCommandsService < Service
def trigger(params)
return unless valid_token?(params[:token])
- user = find_chat_user(params)
+ chat_user = find_chat_user(params)
- if user
- Gitlab::SlashCommands::Command.new(project, user, params).execute
+ if chat_user&.user
+ Gitlab::SlashCommands::Command.new(project, chat_user, params).execute
else
url = authorize_chat_name_url(params)
Gitlab::SlashCommands::Presenters::Access.new(url).authorize
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index a9e5cfb8240..33280eda0b9 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -85,6 +85,15 @@ class ProjectTeam
@masters ||= fetch_members(Gitlab::Access::MASTER)
end
+ def owners
+ @owners ||=
+ if group
+ group.owners
+ else
+ [project.owner]
+ end
+ end
+
def import(source_project, current_user = nil)
target_project = project
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 299a3f32a85..42f1ac43e29 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -16,6 +16,7 @@ class Repository
].freeze
include Gitlab::ShellAdapter
+ include Gitlab::RepositoryCacheAdapter
attr_accessor :full_path, :disk_path, :project, :is_wiki
@@ -35,7 +36,7 @@ class Repository
CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
- tag_count avatar exists? empty? root_ref has_visible_content?
+ tag_count avatar exists? root_ref has_visible_content?
issue_template_names merge_request_template_names).freeze
# Methods that use cache_method but only memoize the value
@@ -57,22 +58,6 @@ class Repository
merge_request_template: :merge_request_template_names
}.freeze
- # Wraps around the given method and caches its output in Redis and an instance
- # variable.
- #
- # This only works for methods that do not take any arguments.
- def self.cache_method(name, fallback: nil, memoize_only: false)
- original = :"_uncached_#{name}"
-
- alias_method(original, name)
-
- define_method(name) do
- cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do
- __send__(original) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
- end
-
def initialize(full_path, project, disk_path: nil, is_wiki: false)
@full_path = full_path
@disk_path = disk_path || full_path
@@ -139,7 +124,7 @@ class Repository
end
end
- def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
+ def commits(ref = nil, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil, all: nil)
options = {
repo: raw_repository,
ref: ref,
@@ -149,7 +134,8 @@ class Repository
after: after,
before: before,
follow: Array(path).length == 1,
- skip_merges: skip_merges
+ skip_merges: skip_merges,
+ all: all
}
commits = Gitlab::Git::Commit.where(options)
@@ -252,7 +238,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds.
def keep_around(sha)
- return unless sha && commit_by(oid: sha)
+ return unless sha.present? && commit_by(oid: sha)
return if kept_around?(sha)
@@ -301,17 +287,6 @@ class Repository
expire_method_caches(CACHED_METHODS)
end
- # Expires the caches of a specific set of methods
- def expire_method_caches(methods)
- methods.each do |key|
- cache.expire(key)
-
- ivar = cache_instance_variable_name(key)
-
- remove_instance_variable(ivar) if instance_variable_defined?(ivar)
- end
- end
-
def expire_avatar_cache
expire_method_caches(%i(avatar))
end
@@ -359,7 +334,7 @@ class Repository
def expire_emptiness_caches
return unless empty?
- expire_method_caches(%i(empty? has_visible_content?))
+ expire_method_caches(%i(has_visible_content?))
end
def lookup_cache
@@ -505,12 +480,14 @@ class Repository
end
cache_method :exists?
+ # We don't need to cache the output of this method because both exists? and
+ # has_visible_content? are already memoized and cached. There's no guarantee
+ # that the values are expired and loaded atomically.
def empty?
return true unless exists?
!has_visible_content?
end
- cache_method :empty?
# The size of this repository in megabytes.
def size
@@ -589,15 +566,7 @@ class Repository
def license_key
return unless exists?
- # The licensee gem creates a Rugged object from the path:
- # https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
- begin
- Licensee.license(path).try(:key)
- # Normally we would rescue Rugged::Error, but that is banned by lint-rugged
- # and we need to migrate this endpoint to Gitaly:
- # https://gitlab.com/gitlab-org/gitaly/issues/1026
- rescue
- end
+ raw_repository.license_short_name
end
cache_method :license_key
@@ -658,14 +627,15 @@ class Repository
end
def last_commit_for_path(sha, path)
- commit_by(oid: last_commit_id_for_path(sha, path))
+ commit = raw_repository.last_commit_for_path(sha, path)
+ ::Commit.new(commit, @project) if commit
end
def last_commit_id_for_path(sha, path)
key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}"
cache.fetch(key) do
- raw_repository.last_commit_id_for_path(sha, path)
+ last_commit_for_path(sha, path)&.id
end
end
@@ -873,20 +843,20 @@ class Repository
raw_repository.ancestor?(ancestor_id, descendant_id)
end
- def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil)
+ def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil, prune: true)
unless remote_name
remote_name = "tmp-#{SecureRandom.hex}"
tmp_remote_name = true
end
add_remote(remote_name, url, mirror_refmap: refmap)
- fetch_remote(remote_name, forced: forced)
+ fetch_remote(remote_name, forced: forced, prune: prune)
ensure
remove_remote(remote_name) if tmp_remote_name
end
- def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false)
- gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
+ def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false, prune: true)
+ gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune)
end
def fetch_source_branch!(source_repository, source_branch, local_ref)
@@ -928,49 +898,6 @@ class Repository
end
end
- # Caches the supplied block both in a cache and in an instance variable.
- #
- # The cache key and instance variable are named the same way as the value of
- # the `key` argument.
- #
- # This method will return `nil` if the corresponding instance variable is also
- # set to `nil`. This ensures we don't keep yielding the block when it returns
- # `nil`.
- #
- # key - The name of the key to cache the data in.
- # fallback - A value to fall back to in the event of a Git error.
- def cache_method_output(key, fallback: nil, memoize_only: false, &block)
- ivar = cache_instance_variable_name(key)
-
- if instance_variable_defined?(ivar)
- instance_variable_get(ivar)
- else
- # If the repository doesn't exist and a fallback was specified we return
- # that value inmediately. This saves us Rugged/gRPC invocations.
- return fallback unless fallback.nil? || exists?
-
- begin
- value =
- if memoize_only
- yield
- else
- cache.fetch(key, &block)
- end
-
- instance_variable_set(ivar, value)
- rescue Gitlab::Git::Repository::NoRepository
- # Even if the above `#exists?` check passes these errors might still
- # occur (for example because of a non-existing HEAD). We want to
- # gracefully handle this and not cache anything
- fallback
- end
- end
- end
-
- def cache_instance_variable_name(key)
- :"@#{key.to_s.tr('?!', '')}"
- end
-
def file_on_head(type)
if head = tree(:head)
head.blobs.find do |blob|
@@ -1025,8 +952,7 @@ class Repository
end
def cache
- # TODO: should we use UUIDs here? We could move repositories without clearing this cache
- @cache ||= RepositoryCache.new(full_path, @project.id)
+ @cache ||= Gitlab::RepositoryCache.new(self)
end
def tags_sorted_by_committed_date
diff --git a/app/models/service.rb b/app/models/service.rb
index 369cae2e85f..99bf757ae44 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -129,6 +129,17 @@ class Service < ActiveRecord::Base
fields
end
+ def configurable_events
+ events = self.class.supported_events
+
+ # No need to disable individual triggers when there is only one
+ if events.count == 1
+ []
+ else
+ events
+ end
+ end
+
def supported_events
self.class.supported_events
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index a58c208279e..644120453cf 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -168,5 +168,9 @@ class Snippet < ActiveRecord::Base
def search_code(query)
fuzzy_search(query, [:content])
end
+
+ def parent_class
+ ::Project
+ end
end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index bb5965e20eb..8afacd188e0 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -32,8 +32,6 @@ class Todo < ActiveRecord::Base
validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
- default_scope { reorder(id: :desc) }
-
scope :pending, -> { with_state(:pending) }
scope :done, -> { with_state(:done) }
@@ -53,10 +51,14 @@ class Todo < ActiveRecord::Base
# milestones, but still show something if the user has a URL with that
# selected.
def sort(method)
- case method.to_s
- when 'priority', 'label_priority' then order_by_labels_priority
- else order_by(method)
- end
+ sorted =
+ case method.to_s
+ when 'priority', 'label_priority' then order_by_labels_priority
+ else order_by(method)
+ end
+
+ # Break ties with the ID column for pagination
+ sorted.order(id: :desc)
end
# Order by priority depending on which issue/merge request the Todo belongs to
diff --git a/app/models/user.rb b/app/models/user.rb
index 8610ca27b7f..b8c55205ab8 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -114,13 +114,15 @@ class User < ActiveRecord::Base
has_many :project_authorizations
has_many :authorized_projects, through: :project_authorizations, source: :project
+ has_many :user_interacted_projects
+ has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
+
has_many :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issues, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :events, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :subscriptions, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :abuse_report, dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent
has_many :reported_abuse_reports, dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" # rubocop:disable Cop/ActiveRecordDependent
@@ -431,7 +433,7 @@ class User < ActiveRecord::Base
end
def self.non_internal
- where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
+ where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND "))
end
#
@@ -601,6 +603,15 @@ class User < ActiveRecord::Base
authorized_projects(min_access_level).exists?({ id: project.id })
end
+ # Typically used in conjunction with projects table to get projects
+ # a user has been given access to.
+ #
+ # Example use:
+ # `Project.where('EXISTS(?)', user.authorizations_for_projects)`
+ def authorizations_for_projects
+ project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
+ end
+
# Returns the projects this user has reporter (or greater) access to, limited
# to at most the given projects.
#
@@ -728,7 +739,7 @@ class User < ActiveRecord::Base
def ldap_user?
if identities.loaded?
- identities.find { |identity| Gitlab::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
+ identities.find { |identity| Gitlab::Auth::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
else
identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
end
@@ -1026,14 +1037,33 @@ class User < ActiveRecord::Base
end
end
+ def todos_done_count(force: false)
+ Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do
+ TodosFinder.new(self, state: :done).execute.count
+ end
+ end
+
+ def todos_pending_count(force: false)
+ Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do
+ TodosFinder.new(self, state: :pending).execute.count
+ end
+ end
+
def update_cache_counts
assigned_open_merge_requests_count(force: true)
assigned_open_issues_count(force: true)
end
+ def update_todos_count_cache
+ todos_done_count(force: true)
+ todos_pending_count(force: true)
+ end
+
def invalidate_cache_counts
invalidate_issue_cache_counts
invalidate_merge_request_cache_counts
+ invalidate_todos_done_count
+ invalidate_todos_pending_count
end
def invalidate_issue_cache_counts
@@ -1044,21 +1074,12 @@ class User < ActiveRecord::Base
Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
end
- def todos_done_count(force: false)
- Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do
- TodosFinder.new(self, state: :done).execute.count
- end
+ def invalidate_todos_done_count
+ Rails.cache.delete(['users', id, 'todos_done_count'])
end
- def todos_pending_count(force: false)
- Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do
- TodosFinder.new(self, state: :pending).execute.count
- end
- end
-
- def update_todos_count_cache
- todos_done_count(force: true)
- todos_pending_count(force: true)
+ def invalidate_todos_pending_count
+ Rails.cache.delete(['users', id, 'todos_pending_count'])
end
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
new file mode 100644
index 00000000000..dd55a6acb79
--- /dev/null
+++ b/app/models/user_interacted_project.rb
@@ -0,0 +1,59 @@
+class UserInteractedProject < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :project
+
+ validates :project_id, presence: true
+ validates :user_id, presence: true
+
+ CACHE_EXPIRY_TIME = 1.day
+
+ # Schema version required for this model
+ REQUIRED_SCHEMA_VERSION = 20180223120443
+
+ class << self
+ def track(event)
+ # For events without a project, we simply don't care.
+ # An example of this is the creation of a snippet (which
+ # is not related to any project).
+ return unless event.project_id
+
+ attributes = {
+ project_id: event.project_id,
+ user_id: event.author_id
+ }
+
+ cached_exists?(attributes) do
+ transaction(requires_new: true) do
+ begin
+ where(attributes).select(1).first || create!(attributes)
+ true # not caching the whole record here for now
+ rescue ActiveRecord::RecordNotUnique
+ # Note, above queries are not atomic and prone
+ # to race conditions (similar like #find_or_create!).
+ # In the case where we hit this, the record we want
+ # already exists - shortcut and return.
+ true
+ end
+ end
+ end
+ end
+
+ # Check if we can safely call .track (table exists)
+ def available?
+ @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization
+ end
+
+ # Flushes cached information about schema
+ def reset_column_information
+ @available_flag = nil
+ super
+ end
+
+ private
+
+ def cached_exists?(project_id:, user_id:, &block)
+ cache_key = "user_interacted_projects:#{project_id}:#{user_id}"
+ Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRY_TIME, &block)
+ end
+ end
+end
diff --git a/app/models/user_synced_attributes_metadata.rb b/app/models/user_synced_attributes_metadata.rb
index 548b99b69d9..688432a9d67 100644
--- a/app/models/user_synced_attributes_metadata.rb
+++ b/app/models/user_synced_attributes_metadata.rb
@@ -26,6 +26,6 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base
private
def sync_profile_from_provider?
- Gitlab::OAuth::Provider.sync_profile_from_provider?(provider)
+ Gitlab::Auth::OAuth::Provider.sync_profile_from_provider?(provider)
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index f0bcba588a2..c9cb730c4e9 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -48,7 +48,12 @@ class GroupPolicy < BasePolicy
rule { has_access }.enable :read_namespace
rule { developer }.enable :admin_milestones
- rule { reporter }.enable :admin_label
+
+ rule { reporter }.policy do
+ enable :admin_label
+ enable :admin_list
+ enable :admin_issue
+ end
rule { master }.policy do
enable :create_projects
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 3b0550b4dd6..57ab0c23dcd 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -61,6 +61,11 @@ class ProjectPolicy < BasePolicy
desc "Project has request access enabled"
condition(:request_access_enabled, scope: :subject) { project.request_access_enabled }
+ desc "Has merge requests allowing pushes to user"
+ condition(:has_merge_requests_allowing_pushes, scope: :subject) do
+ project.merge_requests_allowing_push_to_user(user).any?
+ end
+
features = %w[
merge_requests
issues
@@ -291,6 +296,15 @@ class ProjectPolicy < BasePolicy
prevent :read_issue
end
+ # These rules are included to allow maintainers of projects to push to certain
+ # to run pipelines for the branches they have access to.
+ rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
+ enable :create_build
+ enable :update_build
+ enable :create_pipeline
+ enable :update_pipeline
+ end
+
private
def team_member?
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 08ae49562c7..9f3f2637183 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -78,7 +78,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def rebase_path
- if !rebase_in_progress? && should_be_rebased? && user_can_push_to_source_branch?
+ if !rebase_in_progress? && should_be_rebased? && can_push_to_source_branch?
rebase_project_merge_request_path(project, merge_request)
end
end
@@ -160,7 +160,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def can_push_to_source_branch?
- source_branch_exists? && user_can_push_to_source_branch?
+ return false unless source_branch_exists?
+
+ !!::Gitlab::UserAccess
+ .new(current_user, project: source_project)
+ .can_push_to_branch?(source_branch)
end
private
@@ -191,17 +195,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end.sort.to_sentence
end
- def user_can_push_to_source_branch?
- return false unless source_branch_exists?
-
- ::Gitlab::UserAccess
- .new(current_user, project: source_project)
- .can_push_to_branch?(source_branch)
- end
-
def user_can_collaborate_with_project?
can?(current_user, :push_code, project) ||
- (current_user && current_user.already_forked?(project))
+ (current_user && current_user.already_forked?(project)) ||
+ can_push_to_source_branch?
end
def user_can_fork_project?
diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb
index 564612202b5..3e355a13e06 100644
--- a/app/serializers/analytics_stage_entity.rb
+++ b/app/serializers/analytics_stage_entity.rb
@@ -7,6 +7,7 @@ class AnalyticsStageEntity < Grape::Entity
expose :description
expose :median, as: :value do |stage|
- stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil
+ # median returns a BatchLoader instance which we first have to unwrap by using to_i
+ !stage.median.to_i.zero? ? distance_of_time_in_words(stage.median) : nil
end
end
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index 3f9a275ad08..b22a0b666ef 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -2,4 +2,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :name
expose :status_name, as: :status
expose :status_reason
+ expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
new file mode 100644
index 00000000000..6e68d275047
--- /dev/null
+++ b/app/serializers/diff_file_entity.rb
@@ -0,0 +1,41 @@
+class DiffFileEntity < Grape::Entity
+ include DiffHelper
+ include SubmoduleHelper
+ include BlobHelper
+ include IconsHelper
+ include ActionView::Helpers::TagHelper
+
+ expose :submodule?, as: :submodule
+
+ expose :submodule_link do |diff_file|
+ submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository).first
+ end
+
+ expose :blob_path do |diff_file|
+ diff_file.blob.path
+ end
+
+ expose :blob_icon do |diff_file|
+ blob_icon(diff_file.b_mode, diff_file.file_path)
+ end
+
+ expose :file_path
+ expose :deleted_file?, as: :deleted_file
+ expose :renamed_file?, as: :renamed_file
+ expose :old_path
+ expose :new_path
+ expose :mode_changed?, as: :mode_changed
+ expose :a_mode
+ expose :b_mode
+ expose :text?, as: :text
+
+ expose :old_path_html do |diff_file|
+ old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ old_path
+ end
+
+ expose :new_path_html do |diff_file|
+ _, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ new_path
+ end
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index 0a92e3f8167..bbbcf6a97c1 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -7,4 +7,42 @@ class DiscussionEntity < Grape::Entity
expose :notes, using: NoteEntity
expose :individual_note?, as: :individual_note
+ expose :resolvable?, as: :resolvable
+ expose :resolved?, as: :resolved
+ expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
+ resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
+ end
+ expose :resolve_with_issue_path do |discussion|
+ new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
+ end
+
+ expose :diff_file, using: DiffFileEntity, if: -> (d, _) { defined? d.diff_file }
+
+ expose :diff_discussion?, as: :diff_discussion
+
+ expose :truncated_diff_lines, if: -> (d, _) { (defined? d.diff_file) && d.diff_file.text? } do |discussion|
+ options[:context].render_to_string(
+ partial: "projects/diffs/line",
+ collection: discussion.truncated_diff_lines,
+ as: :line,
+ locals: { diff_file: discussion.diff_file,
+ discussion_expanded: true,
+ plain: true },
+ layout: false,
+ formats: [:html]
+ )
+ end
+
+ expose :image_diff_html, if: -> (d, _) { defined? d.diff_file } do |discussion|
+ diff_file = discussion.diff_file
+ partial = diff_file.new_file? || diff_file.deleted_file? ? 'single_image_diff' : 'replaced_image_diff'
+ options[:context].render_to_string(
+ partial: "projects/diffs/#{partial}",
+ locals: { diff_file: diff_file,
+ position: discussion.position.to_json,
+ click_to_comment: false },
+ layout: false,
+ formats: [:html]
+ )
+ end
end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index fbfe480503b..4a812e39ee1 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -11,6 +11,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :source_project_id
expose :target_branch
expose :target_project_id
+ expose :allow_maintainer_to_push
expose :should_be_rebased?, as: :should_be_rebased
expose :ff_only_enabled do |merge_request|
@@ -29,6 +30,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :can_push_to_source_branch do |merge_request|
presenter(merge_request).can_push_to_source_branch?
end
+
expose :rebase_path do |merge_request|
presenter(merge_request).rebase_path
end
@@ -38,7 +40,7 @@ class MergeRequestWidgetEntity < IssuableEntity
# Diff sha's
expose :diff_head_sha do |merge_request|
- merge_request.diff_head_sha if merge_request.diff_head_commit
+ merge_request.diff_head_sha.presence
end
expose :merge_commit_message
@@ -115,6 +117,14 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :can_cherry_pick_on_current_merge_request do |merge_request|
presenter(merge_request).can_cherry_pick_on_current_merge_request?
end
+
+ expose :can_create_note do |issue|
+ can?(request.current_user, :create_note, issue.project)
+ end
+
+ expose :can_update do |issue|
+ can?(request.current_user, :update_issue, issue)
+ end
end
# Paths
@@ -128,8 +138,8 @@ class MergeRequestWidgetEntity < IssuableEntity
end
expose :new_blob_path do |merge_request|
- if can?(current_user, :push_code, merge_request.project)
- project_new_blob_path(merge_request.project, merge_request.source_branch)
+ if presenter(merge_request).can_push_to_source_branch?
+ project_new_blob_path(merge_request.source_project, merge_request.source_branch)
end
end
@@ -189,6 +199,10 @@ class MergeRequestWidgetEntity < IssuableEntity
end
end
+ expose :create_note_path do |merge_request|
+ project_notes_path(merge_request.project, target_type: 'merge_request', target_id: merge_request.id)
+ end
+
expose :commit_change_content_path do |merge_request|
commit_change_content_project_merge_request_path(merge_request.project, merge_request)
end
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index 7d50e0ff10d..4ccf0bca476 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -23,6 +23,10 @@ class NoteEntity < API::Entities::Note
end
end
+ expose :resolved?, as: :resolved
+ expose :resolvable?, as: :resolvable
+ expose :resolved_by, using: NoteUserEntity
+
expose :system_note_icon_name, if: -> (note, _) { note.system? } do |note|
SystemNoteHelper.system_note_icon_name(note)
end
@@ -53,6 +57,14 @@ class NoteEntity < API::Entities::Note
end
end
+ expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
+ end
+
+ expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
+ end
+
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
delete_attachment_project_note_path(note.project, note)
diff --git a/app/services/badges/base_service.rb b/app/services/badges/base_service.rb
new file mode 100644
index 00000000000..4f87426bd38
--- /dev/null
+++ b/app/services/badges/base_service.rb
@@ -0,0 +1,11 @@
+module Badges
+ class BaseService
+ protected
+
+ attr_accessor :params
+
+ def initialize(params = {})
+ @params = params.dup
+ end
+ end
+end
diff --git a/app/services/badges/build_service.rb b/app/services/badges/build_service.rb
new file mode 100644
index 00000000000..6267e571838
--- /dev/null
+++ b/app/services/badges/build_service.rb
@@ -0,0 +1,12 @@
+module Badges
+ class BuildService < Badges::BaseService
+ # returns the created badge
+ def execute(source)
+ if source.is_a?(Group)
+ GroupBadge.new(params.merge(group: source))
+ else
+ ProjectBadge.new(params.merge(project: source))
+ end
+ end
+ end
+end
diff --git a/app/services/badges/create_service.rb b/app/services/badges/create_service.rb
new file mode 100644
index 00000000000..aafb87f7dcd
--- /dev/null
+++ b/app/services/badges/create_service.rb
@@ -0,0 +1,10 @@
+module Badges
+ class CreateService < Badges::BaseService
+ # returns the created badge
+ def execute(source)
+ badge = Badges::BuildService.new(params).execute(source)
+
+ badge.tap { |b| b.save }
+ end
+ end
+end
diff --git a/app/services/badges/update_service.rb b/app/services/badges/update_service.rb
new file mode 100644
index 00000000000..7ca84b5df31
--- /dev/null
+++ b/app/services/badges/update_service.rb
@@ -0,0 +1,12 @@
+module Badges
+ class UpdateService < Badges::BaseService
+ # returns the updated badge
+ def execute(badge)
+ if params.present?
+ badge.update_attributes(params)
+ end
+
+ badge
+ end
+ end
+end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 6078fe38064..ecd74b74f8a 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -40,7 +40,11 @@ module Boards
end
def set_parent
- params[:project_id] = parent.id
+ if parent.is_a?(Group)
+ params[:group_id] = parent.id
+ else
+ params[:project_id] = parent.id
+ end
end
def set_state
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 797d6df7c1a..15fed7d17c1 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -60,8 +60,10 @@ module Boards
label_ids =
if moving_to_list.movable?
moving_from_list.label_id
+ elsif board.group_board?
+ ::Label.on_group_boards(parent.id).pluck(:label_id)
else
- Label.on_project_boards(parent.id).pluck(:label_id)
+ ::Label.on_project_boards(parent.id).pluck(:label_id)
end
Array(label_ids).compact
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index 183556a1d6b..bebc90c7a8d 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -12,7 +12,11 @@ module Boards
private
def available_labels_for(board)
- LabelsFinder.new(current_user, project_id: parent.id).execute
+ if board.group_board?
+ parent.labels
+ else
+ LabelsFinder.new(current_user, project_id: parent.id).execute
+ end
end
def next_position(board)
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb
index 4f5c5567b42..d458b814183 100644
--- a/app/services/chat_names/find_user_service.rb
+++ b/app/services/chat_names/find_user_service.rb
@@ -9,8 +9,8 @@ module ChatNames
chat_name = find_chat_name
return unless chat_name
- chat_name.touch(:last_used_at)
- chat_name.user
+ chat_name.update_last_used_at
+ chat_name
end
private
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index c8b112132b3..3b3d9239086 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -81,7 +81,7 @@ module Ci
end
def related_merge_requests
- MergeRequest.opened.where(source_project: pipeline.project, source_branch: pipeline.ref)
+ pipeline.project.source_of_merge_requests.opened.where(source_branch: pipeline.ref)
end
end
end
diff --git a/app/services/ci/create_trace_artifact_service.rb b/app/services/ci/create_trace_artifact_service.rb
deleted file mode 100644
index ffde824972c..00000000000
--- a/app/services/ci/create_trace_artifact_service.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module Ci
- class CreateTraceArtifactService < BaseService
- def execute(job)
- return if job.job_artifacts_trace
-
- job.trace.read do |stream|
- break unless stream.file?
-
- clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path|
- create_job_trace!(job, clone_path)
- FileUtils.rm(stream.path)
- end
- end
- end
-
- private
-
- def create_job_trace!(job, path)
- File.open(path) do |stream|
- job.create_job_artifacts_trace!(
- project: job.project,
- file_type: :trace,
- file: stream)
- end
- end
-
- def clone_file!(src_path, temp_dir)
- FileUtils.mkdir_p(temp_dir)
- Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path|
- temp_path = File.join(dir_path, "job.log")
- FileUtils.copy(src_path, temp_path)
- yield(temp_path)
- end
- end
- end
-end
diff --git a/app/services/clusters/applications/check_ingress_ip_address_service.rb b/app/services/clusters/applications/check_ingress_ip_address_service.rb
new file mode 100644
index 00000000000..e572b1e5d99
--- /dev/null
+++ b/app/services/clusters/applications/check_ingress_ip_address_service.rb
@@ -0,0 +1,36 @@
+module Clusters
+ module Applications
+ class CheckIngressIpAddressService < BaseHelmService
+ include Gitlab::Utils::StrongMemoize
+
+ Error = Class.new(StandardError)
+
+ LEASE_TIMEOUT = 15.seconds.to_i
+
+ def execute
+ return if app.external_ip
+ return unless try_obtain_lease
+
+ app.update!(external_ip: ingress_ip) if ingress_ip
+ end
+
+ private
+
+ def try_obtain_lease
+ Gitlab::ExclusiveLease
+ .new("check_ingress_ip_address_service:#{app.id}", timeout: LEASE_TIMEOUT)
+ .try_obtain
+ end
+
+ def ingress_ip
+ service.status.loadBalancer.ingress&.first&.ip
+ end
+
+ def service
+ strong_memoize(:ingress_service) do
+ kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index e87fd49d193..02fb48108fb 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -109,6 +109,10 @@ class IssuableBaseService < BaseService
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
+ def handle_quick_actions_on_create(issuable)
+ merge_quick_actions_into_params!(issuable)
+ end
+
def merge_quick_actions_into_params!(issuable)
original_description = params.fetch(:description, issuable.description)
@@ -131,7 +135,7 @@ class IssuableBaseService < BaseService
end
def create(issuable)
- merge_quick_actions_into_params!(issuable)
+ handle_quick_actions_on_create(issuable)
filter_params(issuable)
params.delete(:state_event)
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
index 2a2bb0cae5b..6be08b590bc 100644
--- a/app/services/members/approve_access_request_service.rb
+++ b/app/services/members/approve_access_request_service.rb
@@ -1,51 +1,20 @@
module Members
- class ApproveAccessRequestService < BaseService
- include MembersHelper
-
- attr_accessor :source
-
- # source - The source object that respond to `#requesters` (i.g. project or group)
- # current_user - The user that performs the access request approval
- # params - A hash of parameters
- # :user_id - User ID used to retrieve the access requester
- # :id - Member ID used to retrieve the access requester
- # :access_level - Optional access level set when the request is accepted
- def initialize(source, current_user, params = {})
- @source = source
- @current_user = current_user
- @params = params.slice(:user_id, :id, :access_level)
- end
-
- # opts - A hash of options
- # :force - Bypass permission check: current_user can be nil in that case
- def execute(opts = {})
- condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
- access_requester = source.requesters.find_by!(condition)
-
- raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts)
+ class ApproveAccessRequestService < Members::BaseService
+ def execute(access_requester, skip_authorization: false, skip_log_audit_event: false)
+ raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_update_access_requester?(access_requester)
access_requester.access_level = params[:access_level] if params[:access_level]
access_requester.accept_request
+ after_execute(member: access_requester, skip_log_audit_event: skip_log_audit_event)
+
access_requester
end
private
- def can_update_access_requester?(access_requester, opts = {})
- access_requester && (
- opts[:force] ||
- can?(current_user, update_member_permission(access_requester), access_requester)
- )
- end
-
- def update_member_permission(member)
- case member
- when GroupMember
- :update_group_member
- when ProjectMember
- :update_project_member
- end
+ def can_update_access_requester?(access_requester)
+ can?(current_user, update_member_permission(access_requester), access_requester)
end
end
end
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
deleted file mode 100644
index 2e89f00dad8..00000000000
--- a/app/services/members/authorized_destroy_service.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-module Members
- class AuthorizedDestroyService < BaseService
- attr_accessor :member, :user
-
- def initialize(member, user = nil)
- @member, @user = member, user
- end
-
- def execute
- return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
-
- Member.transaction do
- unassign_issues_and_merge_requests(member) unless member.invite?
- member.notification_setting&.destroy
-
- member.destroy
- end
-
- if member.request? && member.user != user
- notification_service.decline_access_request(member)
- end
-
- member
- end
-
- private
-
- def unassign_issues_and_merge_requests(member)
- if member.is_a?(GroupMember)
- issues = Issue.unscoped.select(1)
- .joins(:project)
- .where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id)
-
- # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
- IssueAssignee.unscoped
- .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
- .delete_all
-
- MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id)
- .execute
- .update_all(assignee_id: nil)
- else
- project = member.source
-
- # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X
- issues = Issue.unscoped.select(1)
- .where('issues.id = issue_assignees.issue_id')
- .where(project_id: project.id)
-
- # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
- IssueAssignee.unscoped
- .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
- .delete_all
-
- project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
- end
-
- member.user.invalidate_cache_counts
- end
- end
-end
diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb
new file mode 100644
index 00000000000..74556fb20cf
--- /dev/null
+++ b/app/services/members/base_service.rb
@@ -0,0 +1,49 @@
+module Members
+ class BaseService < ::BaseService
+ # current_user - The user that performs the action
+ # params - A hash of parameters
+ def initialize(current_user = nil, params = {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def after_execute(args)
+ # overriden in EE::Members modules
+ end
+
+ private
+
+ def update_member_permission(member)
+ case member
+ when GroupMember
+ :update_group_member
+ when ProjectMember
+ :update_project_member
+ else
+ raise "Unknown member type: #{member}!"
+ end
+ end
+
+ def override_member_permission(member)
+ case member
+ when GroupMember
+ :override_group_member
+ when ProjectMember
+ :override_project_member
+ else
+ raise "Unknown member type: #{member}!"
+ end
+ end
+
+ def action_member_permission(action, member)
+ case action
+ when :update
+ update_member_permission(member)
+ when :override
+ override_member_permission(member)
+ else
+ raise "Unknown action '#{action}' on #{member}!"
+ end
+ end
+ end
+end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 26906ae7167..bc6a9405aac 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -1,15 +1,8 @@
module Members
- class CreateService < BaseService
+ class CreateService < Members::BaseService
DEFAULT_LIMIT = 100
- def initialize(source, current_user, params = {})
- @source = source
- @current_user = current_user
- @params = params
- @error = nil
- end
-
- def execute
+ def execute(source)
return error('No users specified.') if params[:user_ids].blank?
user_ids = params[:user_ids].split(',').uniq
@@ -17,13 +10,15 @@ module Members
return error("Too many users specified (limit is #{user_limit})") if
user_limit && user_ids.size > user_limit
- @source.add_users(
+ members = source.add_users(
user_ids,
params[:access_level],
expires_at: params[:expires_at],
current_user: current_user
)
+ members.each { |member| after_execute(member: member) }
+
success
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 05b93ac8fdb..5b51e1982f1 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -1,42 +1,27 @@
module Members
- class DestroyService < BaseService
- include MembersHelper
+ class DestroyService < Members::BaseService
+ def execute(member, skip_authorization: false)
+ raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
- attr_accessor :source
+ return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
- ALLOWED_SCOPES = %i[members requesters all].freeze
+ member.destroy
- def initialize(source, current_user, params = {})
- @source = source
- @current_user = current_user
- @params = params
- end
-
- def execute(scope = :members)
- raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
+ member.user&.invalidate_cache_counts
- member = find_member!(scope)
+ if member.request? && member.user != current_user
+ notification_service.decline_access_request(member)
+ end
- raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
+ after_execute(member: member)
- AuthorizedDestroyService.new(member, current_user).execute
+ member
end
private
- def find_member!(scope)
- condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
- case scope
- when :all
- source.members.find_by(condition) ||
- source.requesters.find_by!(condition)
- else
- source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
-
def can_destroy_member?(member)
- member && can?(current_user, destroy_member_permission(member), member)
+ can?(current_user, destroy_member_permission(member), member)
end
def destroy_member_permission(member)
@@ -45,6 +30,8 @@ module Members
:destroy_group_member
when ProjectMember
:destroy_project_member
+ else
+ raise "Unknown member type: #{member}!"
end
end
end
diff --git a/app/services/members/request_access_service.rb b/app/services/members/request_access_service.rb
index 2614153d900..24293b30005 100644
--- a/app/services/members/request_access_service.rb
+++ b/app/services/members/request_access_service.rb
@@ -1,13 +1,6 @@
module Members
- class RequestAccessService < BaseService
- attr_accessor :source
-
- def initialize(source, current_user)
- @source = source
- @current_user = current_user
- end
-
- def execute
+ class RequestAccessService < Members::BaseService
+ def execute(source)
raise Gitlab::Access::AccessDeniedError unless can_request_access?(source)
source.members.create(
@@ -19,7 +12,7 @@ module Members
private
def can_request_access?(source)
- source && can?(current_user, :request_access, source)
+ can?(current_user, :request_access, source)
end
end
end
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
new file mode 100644
index 00000000000..48b3d59f7bd
--- /dev/null
+++ b/app/services/members/update_service.rb
@@ -0,0 +1,16 @@
+module Members
+ class UpdateService < Members::BaseService
+ # returns the updated member
+ def execute(member, permission: :update)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member)
+
+ old_access_level = member.human_access
+
+ if member.update_attributes(params)
+ after_execute(action: permission, old_access_level: old_access_level, member: member)
+ end
+
+ member
+ end
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 20a2b50d3de..231ab76fde4 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -24,6 +24,25 @@ module MergeRequests
private
+ def handle_wip_event(merge_request)
+ if wip_event = params.delete(:wip_event)
+ # We update the title that is provided in the params or we use the mr title
+ title = params[:title] || merge_request.title
+ params[:title] = case wip_event
+ when 'wip' then MergeRequest.wip_title(title)
+ when 'unwip' then MergeRequest.wipless_title(title)
+ end
+ end
+ end
+
+ def filter_params(merge_request)
+ super
+
+ unless merge_request.can_allow_maintainer_to_push?(current_user)
+ params.delete(:allow_maintainer_to_push)
+ end
+ end
+
def merge_request_metrics_service(merge_request)
MergeRequestMetricsService.new(merge_request.metrics)
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 4b186d93772..a98bbdf74dd 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -6,6 +6,7 @@ module MergeRequests
@params_issue_iid = params.delete(:issue_iid)
self.merge_request = MergeRequest.new(params)
+ merge_request.author = current_user
merge_request.compare_commits = []
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
diff --git a/app/services/merge_requests/conflicts/list_service.rb b/app/services/merge_requests/conflicts/list_service.rb
index ca9a33678e4..72cbc49adb2 100644
--- a/app/services/merge_requests/conflicts/list_service.rb
+++ b/app/services/merge_requests/conflicts/list_service.rb
@@ -17,15 +17,7 @@ module MergeRequests
return @conflicts_can_be_resolved_in_ui = false unless merge_request.has_complete_diff_refs?
return @conflicts_can_be_resolved_in_ui = false if merge_request.branch_missing?
- begin
- # Try to parse each conflict. If the MR's mergeable status hasn't been
- # updated, ensure that we don't say there are conflicts to resolve
- # when there are no conflict files.
- conflicts.files.each(&:lines)
- @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
- rescue Gitlab::Git::CommandError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
- @conflicts_can_be_resolved_in_ui = false
- end
+ @conflicts_can_be_resolved_in_ui = conflicts.can_be_resolved_in_ui?
end
def conflicts
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index a18b1c90765..c57a2445341 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -34,6 +34,12 @@ module MergeRequests
super
end
+ # Override from IssuableBaseService
+ def handle_quick_actions_on_create(merge_request)
+ super
+ handle_wip_event(merge_request)
+ end
+
private
def update_merge_requests_head_pipeline(merge_request)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index c153872c874..8a40ad88182 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -98,17 +98,6 @@ module MergeRequests
private
- def handle_wip_event(merge_request)
- if wip_event = params.delete(:wip_event)
- # We update the title that is provided in the params or we use the mr title
- title = params[:title] || merge_request.title
- params[:title] = case wip_event
- when 'wip' then MergeRequest.wip_title(title)
- when 'unwip' then MergeRequest.wipless_title(title)
- end
- end
- end
-
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
diff --git a/app/services/notes/build_service.rb b/app/services/notes/build_service.rb
index abf25bb778b..77e7b8a5ea7 100644
--- a/app/services/notes/build_service.rb
+++ b/app/services/notes/build_service.rb
@@ -26,14 +26,19 @@ module Notes
if project
project.notes.find_discussion(discussion_id)
else
- # only PersonalSnippets can have discussions without project association
discussion = Note.find_discussion(discussion_id)
noteable = discussion.noteable
- return nil unless noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable)
+ return nil unless noteable_without_project?(noteable)
discussion
end
end
+
+ def noteable_without_project?(noteable)
+ return true if noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable)
+
+ false
+ end
end
end
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index 6a10e172483..ad3dcc5010b 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -11,7 +11,7 @@ module Notes
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
- return if @note.for_personal_snippet?
+ return unless @note.for_project_noteable?
@note.create_cross_references!
execute_note_hooks
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index a8d0cc15527..0a33d5f3f3d 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -9,14 +9,12 @@ module Notes
UPDATE_SERVICES[note.noteable_type]
end
- def self.supported?(note, current_user)
- noteable_update_service(note) &&
- current_user &&
- current_user.can?(:"update_#{note.to_ability_name}", note.noteable)
+ def self.supported?(note)
+ !!noteable_update_service(note)
end
def supported?(note)
- self.class.supported?(note, current_user)
+ self.class.supported?(note)
end
def extract_commands(note, options = {})
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 6835b14648b..e4be953e810 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -280,7 +280,7 @@ module NotificationRecipientService
add_participants(note.author)
add_mentions(note.author, target: note)
- unless note.for_personal_snippet?
+ if note.for_project_noteable?
# Merge project watchers
add_project_watchers
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 01838ec6b5d..7fa1387084c 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -85,7 +85,7 @@ module Projects
end
def after_create_actions
- log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
+ log_info("#{@project.owner.name} created a new project \"#{@project.full_name}\"")
unless @project.gitlab_project_import?
@project.write_repository_config
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index fe4e8ea10bf..af41ce82f65 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -2,7 +2,7 @@ module Projects
module ImportExport
class ExportService < BaseService
def execute(_options = {})
- @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.disk_path, 'work'))
+ @shared = project.import_export_shared
save_all
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index c760bd3b626..00fdd047208 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -1,5 +1,8 @@
module Projects
class UpdatePagesService < BaseService
+ InvaildStateError = Class.new(StandardError)
+ FailedToExtractError = Class.new(StandardError)
+
BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte
SITE_PATH = 'public/'.freeze
@@ -11,13 +14,15 @@ module Projects
end
def execute
+ register_attempt
+
# Create status notifying the deployment of pages
@status = create_status
@status.enqueue!
@status.run!
- raise 'missing pages artifacts' unless build.artifacts?
- raise 'pages are outdated' unless latest?
+ raise InvaildStateError, 'missing pages artifacts' unless build.artifacts?
+ raise InvaildStateError, 'pages are outdated' unless latest?
# Create temporary directory in which we will extract the artifacts
FileUtils.mkdir_p(tmp_path)
@@ -26,24 +31,22 @@ module Projects
# Check if we did extract public directory
archive_public_path = File.join(archive_path, 'public')
- raise 'pages miss the public folder' unless Dir.exist?(archive_public_path)
- raise 'pages are outdated' unless latest?
+ raise FailedToExtractError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
+ raise InvaildStateError, 'pages are outdated' unless latest?
deploy_page!(archive_public_path)
success
end
- rescue => e
+ rescue InvaildStateError, FailedToExtractError => e
register_failure
error(e.message)
- ensure
- register_attempt
- build.erase_artifacts! unless build.has_expiring_artifacts?
end
private
def success
@status.success
+ delete_artifact!
super
end
@@ -52,6 +55,7 @@ module Projects
@status.allow_failure = !latest?
@status.description = message
@status.drop(:script_failure)
+ delete_artifact!
super
end
@@ -72,7 +76,7 @@ module Projects
elsif artifacts.ends_with?('.zip')
extract_zip_archive!(temp_path)
else
- raise 'unsupported artifacts format'
+ raise FailedToExtractError, 'unsupported artifacts format'
end
end
@@ -81,17 +85,17 @@ module Projects
%W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
%W(tar -x -C #{temp_path} #{SITE_PATH}),
err: '/dev/null')
- raise 'pages failed to extract' unless results.compact.all?(&:success?)
+ raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?)
end
def extract_zip_archive!(temp_path)
- raise 'missing artifacts metadata' unless build.artifacts_metadata?
+ raise FailedToExtractError, 'missing artifacts metadata' unless build.artifacts_metadata?
# Calculate page size after extract
public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
if public_entry.total_size > max_size
- raise "artifacts for pages are too large: #{public_entry.total_size}"
+ raise FailedToExtractError, "artifacts for pages are too large: #{public_entry.total_size}"
end
# Requires UnZip at least 6.00 Info-ZIP.
@@ -100,7 +104,7 @@ module Projects
# We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
site_path = File.join(SITE_PATH, '*')
unless system(*%W(unzip -qq -n #{artifacts} #{site_path} -d #{temp_path}))
- raise 'pages failed to extract'
+ raise FailedToExtractError, 'pages failed to extract'
end
end
@@ -163,6 +167,11 @@ module Projects
build.artifacts_file.path
end
+ def delete_artifact!
+ build.reload # Reload stable object to prevent erase artifacts with old state
+ build.erase_artifacts! unless build.has_expiring_artifacts?
+ end
+
def latest_sha
project.commit(build.ref).try(:sha).to_s
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 0e235a6d2a0..5f2615a2c01 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -15,6 +15,8 @@ module Projects
return error("Could not set the default branch") unless project.change_head(params[:default_branch])
end
+ ensure_wiki_exists if enabling_wiki?
+
if project.update_attributes(params.except(:default_branch))
if project.previous_changes.include?('path')
project.rename_repo
@@ -52,5 +54,18 @@ module Projects
project.repository.exists? &&
new_branch && new_branch != project.default_branch
end
+
+ def enabling_wiki?
+ return false if @project.wiki_enabled?
+
+ params.dig(:project_feature_attributes, :wiki_access_level).to_i > ProjectFeature::DISABLED
+ end
+
+ def ensure_wiki_exists
+ ProjectWiki.new(project, project.owner).wiki
+ rescue ProjectWiki::CouldNotCreateWikiError
+ log_error("Could not create wiki for #{project.full_name}")
+ Gitlab::Metrics.counter(:wiki_can_not_be_created_total, 'Counts the times we failed to create a wiki')
+ end
end
end
diff --git a/app/services/prometheus/adapter_service.rb b/app/services/prometheus/adapter_service.rb
new file mode 100644
index 00000000000..4504d2ccfe6
--- /dev/null
+++ b/app/services/prometheus/adapter_service.rb
@@ -0,0 +1,36 @@
+module Prometheus
+ class AdapterService
+ def initialize(project, deployment_platform = nil)
+ @project = project
+
+ @deployment_platform = if deployment_platform
+ deployment_platform
+ else
+ project.deployment_platform
+ end
+ end
+
+ attr_reader :deployment_platform, :project
+
+ def prometheus_adapter
+ @prometheus_adapter ||= if service_prometheus_adapter.can_query?
+ service_prometheus_adapter
+ else
+ cluster_prometheus_adapter
+ end
+ end
+
+ def service_prometheus_adapter
+ project.find_or_initialize_service('prometheus')
+ end
+
+ def cluster_prometheus_adapter
+ return unless deployment_platform.respond_to?(:cluster)
+
+ cluster = deployment_platform.cluster
+ return unless cluster.application_prometheus&.installed?
+
+ cluster.application_prometheus
+ end
+ end
+end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 1e9bd84e749..cba49faac31 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -347,9 +347,9 @@ module QuickActions
"#{verb} this #{noun} as Work In Progress."
end
condition do
- issuable.persisted? &&
- issuable.respond_to?(:work_in_progress?) &&
- current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ issuable.respond_to?(:work_in_progress?) &&
+ # Allow it to mark as WIP on MR creation page _or_ through MR notes.
+ (issuable.new_record? || current_user.can?(:"update_#{issuable.to_ability_name}", issuable))
end
command :wip do
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index a6b7a6e1416..ba7946fd23c 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -11,6 +11,8 @@ class SystemHooksService
SystemHook.hooks_for(hooks_scope).find_each do |hook|
hook.async_execute(data, 'system_hooks')
end
+
+ Gitlab::Plugin.execute_all_async(data)
end
private
@@ -18,8 +20,8 @@ class SystemHooksService
def build_event_data(model, event)
data = {
event_name: build_event_name(model, event),
- created_at: model.created_at.xmlschema,
- updated_at: model.updated_at.xmlschema
+ created_at: model.created_at&.xmlschema,
+ updated_at: model.updated_at&.xmlschema
}
case model
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index c2ca404b179..ffd48e842c2 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -241,8 +241,7 @@ class TodoService
end
def handle_note(note, author, skip_users = [])
- # Skip system notes, and notes on project snippet
- return if note.system? || note.for_snippet?
+ return unless note.can_create_todo?
project = note.project
target = note.noteable
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index b71002433d6..06b604dad4d 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -49,6 +49,8 @@ module Users
::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute
end
+ yield(user) if block_given?
+
MigrateToGhostUserService.new(user).execute unless options[:hard_delete]
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
diff --git a/app/validators/url_placeholder_validator.rb b/app/validators/url_placeholder_validator.rb
new file mode 100644
index 00000000000..dd681218b6b
--- /dev/null
+++ b/app/validators/url_placeholder_validator.rb
@@ -0,0 +1,32 @@
+# UrlValidator
+#
+# Custom validator for URLs.
+#
+# By default, only URLs for the HTTP(S) protocols will be considered valid.
+# Provide a `:protocols` option to configure accepted protocols.
+#
+# Also, this validator can help you validate urls with placeholders inside.
+# Usually, if you have a url like 'http://www.example.com/%{project_path}' the
+# URI parser will reject that URL format. Provide a `:placeholder_regex` option
+# to configure accepted placeholders.
+#
+# Example:
+#
+# class User < ActiveRecord::Base
+# validates :personal_url, url: true
+#
+# validates :ftp_url, url: { protocols: %w(ftp) }
+#
+# validates :git_url, url: { protocols: %w(http https ssh git) }
+#
+# validates :placeholder_url, url: { placeholder_regex: /(project_path|project_id|default_branch)/ }
+# end
+#
+class UrlPlaceholderValidator < UrlValidator
+ def validate_each(record, attribute, value)
+ placeholder_regex = self.options[:placeholder_regex]
+ value = value.gsub(/%{#{placeholder_regex}}/, 'foo') if placeholder_regex && value
+
+ super(record, attribute, value)
+ end
+end
diff --git a/app/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb
index 4bfa3c45303..72660be6c43 100644
--- a/app/validators/variable_duplicates_validator.rb
+++ b/app/validators/variable_duplicates_validator.rb
@@ -5,6 +5,8 @@
# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
+ return if record.errors.include?(:"#{attribute}.key")
+
if options[:scope]
scoped = value.group_by do |variable|
Array(options[:scope]).map { |attr| variable.send(attr) } # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 20527d31870..81d7db04a3c 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -173,7 +173,7 @@
Password authentication enabled for Git over HTTP(S)
.help-block
When disabled, a Personal Access Token
- - if Gitlab::LDAP::Config.enabled?
+ - if Gitlab::Auth::LDAP::Config.enabled?
or LDAP password
must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any?
@@ -657,24 +657,26 @@
.checkbox
= f.label :version_check_enabled do
= f.check_box :version_check_enabled
- Version check enabled
+ Enable version check
.help-block
- Let GitLab inform you when an update is available.
+ GitLab will inform you if a new version is available.
+ = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
+ about what information is shared with GitLab Inc.
.form-group
.col-sm-offset-2.col-sm-10
- can_be_configured = @application_setting.usage_ping_can_be_configured?
.checkbox
= f.label :usage_ping_enabled do
= f.check_box :usage_ping_enabled, disabled: !can_be_configured
- Usage ping enabled
- = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
+ Enable usage ping
.help-block
- if can_be_configured
- Every week GitLab will report license usage back to GitLab, Inc.
- Disable this option if you do not want this to occur. To see the
- JSON payload that will be sent, visit the
- = succeed '.' do
- = link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping')
+ To help improve GitLab and its user experience, GitLab will
+ periodically collect usage information.
+ = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
+ about what information is shared with GitLab Inc. Visit
+ = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
+ to see the JSON payload sent.
- else
The usage ping is disabled, and cannot be configured through this
form. For more information, see the documentation on
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e3711421b61..05c41082882 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -164,7 +164,7 @@
%h4 Latest projects
- @projects.each do |project|
%p
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
+ = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
%span.light.pull-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 2545cecc721..324f3c0a22f 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -68,7 +68,7 @@
- @projects.each do |project|
%li
%strong
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
%span.badge
= storage_counter(project.statistics.storage_size)
%span.pull-right.light
@@ -86,7 +86,7 @@
- @group.shared_projects.sort_by(&:name).each do |project|
%li
%strong
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
%span.badge
= storage_counter(project.statistics.storage_size)
%span.pull-right.light
diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml
index d8f96ed5b0d..a6324a97fd5 100644
--- a/app/views/admin/hooks/_form.html.haml
+++ b/app/views/admin/hooks/_form.html.haml
@@ -1,21 +1,20 @@
= form_errors(hook)
.form-group
- = form.label :url, 'URL', class: 'control-label'
- .col-sm-10
- = form.text_field :url, class: 'form-control'
+ = form.label :url, 'URL', class: 'label-light'
+ = form.text_field :url, class: 'form-control'
.form-group
- = form.label :token, 'Secret Token', class: 'control-label'
- .col-sm-10
- = form.text_field :token, class: 'form-control'
- %p.help-block
- Use this token to validate received payloads
+ = form.label :token, 'Secret Token', class: 'label-light'
+ = form.text_field :token, class: 'form-control'
+ %p.help-block
+ Use this token to validate received payloads
.form-group
- = form.label :url, 'Trigger', class: 'control-label'
- .col-sm-10.prepend-top-10
- %div
- System hook will be triggered on set of events like creating project
- or adding ssh key. But you can also enable extra triggers like Push events.
+ = form.label :url, 'Trigger', class: 'label-light'
+ %ul.list-unstyled
+ %li
+ .help-block
+ System hook will be triggered on set of events like creating project
+ or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
= form.check_box :repository_update_events, class: 'pull-left'
@@ -24,21 +23,21 @@
%strong Repository update events
%p.light
This URL will be triggered when repository is updated
- %div
+ %li
= form.check_box :push_events, class: 'pull-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This URL will be triggered for each branch updated to the repository
- %div
+ %li
= form.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This URL will be triggered when a new tag is pushed to the repository
- %div
+ %li
= form.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= form.label :merge_requests_events, class: 'list-label' do
@@ -46,9 +45,8 @@
%p.light
This URL will be triggered when a merge request is created/updated/merged
.form-group
- = form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox'
- .col-sm-10
- .checkbox
- = form.label :enable_ssl_verification do
- = form.check_box :enable_ssl_verification
- %strong Enable SSL verification
+ = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
+ .checkbox
+ = form.label :enable_ssl_verification do
+ = form.check_box :enable_ssl_verification
+ %strong Enable SSL verification
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index bc02d9969d6..d9e2ce5e74c 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -1,33 +1,35 @@
- page_title 'System Hooks'
-%h3.page-title
- System hooks
+.row.prepend-top-default
+ .col-lg-4
+ %h4.prepend-top-0
+ = page_title
+ %p
+ #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
+ used for binding events when GitLab creates a User or Project.
-%p.light
- #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
- used for binding events when GitLab creates a User or Project.
+ .col-lg-8.append-bottom-default
+ = form_for @hook, as: :hook, url: admin_hooks_path do |f|
+ = render partial: 'form', locals: { form: f, hook: @hook }
+ = f.submit 'Add system hook', class: 'btn btn-create'
-%hr
+ %hr
-= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f|
- = render partial: 'form', locals: { form: f, hook: @hook }
- .form-actions
- = f.submit 'Add system hook', class: 'btn btn-create'
-%hr
+ - if @hooks.any?
+ .panel.panel-default
+ .panel-heading
+ System hooks (#{@hooks.count})
+ %ul.content-list
+ - @hooks.each do |hook|
+ %li
+ .controls
+ = render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: hook, button_class: 'btn-sm'
+ = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
+ = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
+ .monospace= hook.url
+ %div
+ - SystemHook.triggers.each_value do |event|
+ - if hook.public_send(event)
+ %span.label.label-gray= event.to_s.titleize
+ %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
-- if @hooks.any?
- .panel.panel-default
- .panel-heading
- System hooks (#{@hooks.count})
- %ul.content-list
- - @hooks.each do |hook|
- %li
- .controls
- = render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: hook, button_class: 'btn-sm'
- = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
- = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
- .monospace= hook.url
- %div
- - SystemHook.triggers.each_value do |event|
- - if hook.public_send(event)
- %span.label.label-gray= event.to_s.titleize
- %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
+= render 'shared/plugins/index'
diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml
index 112a201fafa..5381b854f5c 100644
--- a/app/views/admin/identities/_form.html.haml
+++ b/app/views/admin/identities/_form.html.haml
@@ -4,7 +4,7 @@
.form-group
= f.label :provider, class: 'control-label'
.col-sm-10
- - values = Gitlab::OAuth::Provider.providers.map { |name| ["#{Gitlab::OAuth::Provider.label_for(name)} (#{name})", name] }
+ - values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] }
= f.select :provider, values, { allow_blank: false }, class: 'form-control'
.form-group
= f.label :extern_uid, "Identifier", class: 'control-label'
diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml
index 8c658905bd6..ef5a3f1d969 100644
--- a/app/views/admin/identities/_identity.html.haml
+++ b/app/views/admin/identities/_identity.html.haml
@@ -1,6 +1,6 @@
%tr
%td
- #{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
+ #{Gitlab::Auth::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
%td
= identity.extern_uid
%td
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 42f92079d85..c02ddafe108 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,8 +1,8 @@
- add_to_breadcrumbs "Projects", admin_projects_path
-- breadcrumb_title @project.name_with_namespace
-- page_title @project.name_with_namespace, "Projects"
+- breadcrumb_title @project.full_name
+- page_title @project.full_name, "Projects"
%h3.page-title
- Project: #{@project.name_with_namespace}
+ Project: #{@project.full_name}
= link_to edit_project_path(@project), class: "btn btn-nr pull-right" do
%i.fa.fa-pencil-square-o
Edit
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 140688b52d3..e1cee584929 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -17,6 +17,8 @@
%td
= runner.version
%td
+ = runner.ip_address
+ %td
- if runner.shared?
n/a
- else
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index abec3607cab..9f13dbbbd82 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -60,6 +60,7 @@
%th Runner token
%th Description
%th Version
+ %th IP Address
%th Projects
%th Jobs
%th Tags
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 6d8fad0eb8d..185e9d7b35d 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -39,7 +39,7 @@
%tr.alert-info
%td
%strong
- = project.name_with_namespace
+ = project.full_name
%td
.pull-right
= link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
@@ -61,7 +61,7 @@
- @projects.each do |project|
%tr
%td
- = project.name_with_namespace
+ = project.full_name
%td
.pull-right
= form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f|
@@ -95,7 +95,7 @@
%td.status
- if project
- = project.name_with_namespace
+ = project.full_name
%td.build-link
- if project
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 4a440f3f6d4..96835ee9af5 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -29,12 +29,12 @@
.panel.panel-default
.panel-heading Joined projects (#{@joined_projects.count})
%ul.well-list
- - @joined_projects.sort_by(&:name_with_namespace).each do |project|
+ - @joined_projects.sort_by(&:full_name).each do |project|
- member = project.team.find_member(@user.id)
%li.project_member
.list-item-name
= link_to admin_project_path(project), class: dom_class(project) do
- = project.name_with_namespace
+ = project.full_name
- if member
.pull-right
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 56ec1b3db0d..6e54b9b5645 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -1,7 +1,3 @@
-- if inject_u2f_api?
- - content_for :page_specific_javascripts do
- = webpack_bundle_tag('u2f')
-
%div
= render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
.login-box
diff --git a/app/views/groups/boards/index.html.haml b/app/views/groups/boards/index.html.haml
new file mode 100644
index 00000000000..bb56769bd3f
--- /dev/null
+++ b/app/views/groups/boards/index.html.haml
@@ -0,0 +1 @@
+= render "shared/boards/show", board: @boards.first
diff --git a/app/views/groups/boards/show.html.haml b/app/views/groups/boards/show.html.haml
new file mode 100644
index 00000000000..92838fa4b11
--- /dev/null
+++ b/app/views/groups/boards/show.html.haml
@@ -0,0 +1 @@
+= render "shared/boards/show", board: @board, group: true
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
deleted file mode 100644
index 9d05bff6c4e..00000000000
--- a/app/views/groups/group_members/update.js.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-:plain
- var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
- $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
- gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}"));
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index f2ae7c52031..36df03302e8 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,12 +1,10 @@
- page_title "Issues"
-- group_issues_exists = group_issues(@group).exists?
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
-
-- if group_issues_exists
+- if group_issues_count(state: 'all').zero?
+ = render 'shared/empty_states/issues', project_select_button: true
+- else
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
@@ -19,5 +17,3 @@
= render 'shared/issuable/search_bar', type: :issues
= render 'shared/issues'
-- else
- = render 'shared/empty_states/issues', project_select_button: true
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index d10efdad53b..ac7e12fcd0b 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,8 +1,10 @@
- page_title 'Labels'
+- issuables = ['issues', 'merge requests']
+
.top-area.adjust
.nav-text
- Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
+ = _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence }
.nav-controls
- if can?(current_user, :admin_label, @group)
@@ -16,4 +18,4 @@
= paginate @labels, theme: 'gitlab'
- else
.nothing-here-block
- No labels created yet.
+ = _("No labels created yet.")
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 046b92bd9fb..4ccd16f3e11 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,9 +1,6 @@
- page_title "Merge Requests"
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
-
-- if @group_merge_requests.empty?
+- if group_merge_requests_count(state: 'all').zero?
= render 'shared/empty_states/merge_requests', project_select_button: true
- else
.top-area
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 8d2bc810a7d..ef181b425bc 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -14,7 +14,7 @@
.list-item-name
%span{ class: visibility_level_color(project.visibility_level) }
= visibility_level_icon(project.visibility_level)
- %strong= link_to project.name_with_namespace, project
+ %strong= link_to project.full_name, project
.pull-right
- if project.archived
%span.label.label-warning archived
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
deleted file mode 100644
index 3dbdfc97654..00000000000
--- a/app/views/ide/index.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- @body_class = 'ide'
-- page_title 'IDE'
-
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'ide', force_same_domain: true
-
-#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
- .text-center
- = icon('spinner spin 2x')
- %h2.clgray= _('Loading the GitLab IDE...')
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index e9a04e6c122..638c8b5a672 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -2,11 +2,11 @@
- provider_title = Gitlab::ImportSources.title(provider)
%p.light
- Select projects you want to import.
+ = import_githubish_choose_repository_message
%hr
%p
= button_tag class: "btn btn-import btn-success js-import-all" do
- Import all projects
+ = import_all_githubish_repositories_button_label
= icon("spinner spin", class: "loading-icon")
.table-responsive
@@ -16,9 +16,9 @@
%colgroup.import-jobs-status-col
%thead
%tr
- %th From #{provider_title}
- %th To GitLab
- %th Status
+ %th= _('From %{provider_title}') % { provider_title: provider_title }
+ %th= _('To GitLab')
+ %th= _('Status')
%tbody
- @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
@@ -30,10 +30,12 @@
- if project.import_status == 'finished'
%span
%i.fa.fa-check
- done
+ = _('Done')
- elsif project.import_status == 'started'
%i.fa.fa-spinner.fa-spin
- started
+ = _('Started')
+ - elsif project.import_status == 'failed'
+ = _('Failed')
- else
= project.human_import_status_name
@@ -55,7 +57,9 @@
= text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
- Import
+ = has_ci_cd_only_params? ? _('Connect') : _('Import')
= icon("spinner spin", class: "loading-icon")
-.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", import_path: "#{url_for([:import, provider])}" } }
+.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}",
+ import_path: "#{url_for([:import, provider])}",
+ ci_cd_only: "#{has_ci_cd_only_params?}" } }
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index 9c2da3a3eec..ca47ab5f274 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -1,43 +1,31 @@
-- page_title "GitHub Import"
+- title = has_ci_cd_only_params? ? _('Connect repositories from GitHub') : _('GitHub import')
+- page_title title
+- breadcrumb_title title
- header_title "Projects", root_path
%h3.page-title
- = icon 'github', text: 'Import Projects from GitHub'
+ = icon 'github', text: import_github_title
- if github_import_configured?
%p
- To import a GitHub project, you first need to authorize GitLab to access
- the list of your GitHub repositories:
+ = import_github_authorize_message
- = link_to 'List your GitHub repositories', status_import_github_path, class: 'btn btn-success'
+ = link_to _('List your GitHub repositories'), status_import_github_path, class: 'btn btn-success'
%hr
%p
- - if github_import_configured?
- Alternatively,
- - else
- To import a GitHub project,
- you can use a
- = succeed '.' do
- = link_to 'Personal Access Token', 'https://github.com/settings/tokens'
- When you create your Personal Access Token,
- you will need to select the <code>repo</code> scope, so we can display a
- list of your public and private repositories which are available for import.
+ = import_github_personal_access_token_message
= form_tag personal_access_token_import_github_path, method: :post, class: 'form-inline' do
.form-group
- = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: "Personal Access Token", size: 40
- = submit_tag 'List your GitHub repositories', class: 'btn btn-success'
+ = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('Personal Access Token'), size: 40
+ = submit_tag _('List your GitHub repositories'), class: 'btn btn-success'
+
+ -# EE-specific start
+ -# EE-specific end
- unless github_import_configured?
%hr
%p
- Note:
- - if current_user.admin?
- As an administrator you may like to configure
- - else
- Consider asking your GitLab administrator to configure
- = link_to 'GitHub integration', help_page_path("integration/github")
- which will allow login via GitHub and allow importing projects without
- generating a Personal Access Token.
+ = import_configure_github_admin_message
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 0fe578a0036..b00b972d9c9 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,6 +1,8 @@
-- page_title "GitHub Import"
+- title = has_ci_cd_only_params? ? _('Connect repositories from GitHub') : _('GitHub import')
+- page_title title
+- breadcrumb_title title
- header_title "Projects", root_path
%h3.page-title
- = icon 'github', text: 'Import Projects from GitHub'
+ = icon 'github', text: import_github_title
= render 'import/githubish_status', provider: 'github'
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index ad6213b4efd..c2bb1216c5f 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -12,7 +12,7 @@
- project = @member.source
project
%strong
- = link_to project.name_with_namespace, project_url(project)
+ = link_to project.full_name, project_url(project)
- when Group
- group = @member.source
group
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 0c979109b3f..b981b5fdafa 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -42,7 +42,6 @@
= webpack_bundle_tag "common"
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled
- = webpack_bundle_tag "test" if Rails.env.test?
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml
index 59becb043d3..5809d6f7fea 100644
--- a/app/views/layouts/nav/projects_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml
@@ -1,4 +1,4 @@
-- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
+- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
.projects-dropdown-container
.project-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 47ae79b7a69..5ea19c9882d 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -1,7 +1,6 @@
-- issues_count = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute.count
-- merge_requests_count = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute.count
-
-- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
+- issues_count = group_issues_count(state: 'opened')
+- merge_requests_count = group_merge_requests_count(state: 'opened')
+- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index', 'boards#index', 'boards#show']
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
@@ -52,12 +51,19 @@
%strong.fly-out-top-item-name
#{ _('Issues') }
%span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count)
+
%li.divider.fly-out-top-item
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
= link_to issues_group_path(@group), title: 'List' do
%span
List
+ - if group_sidebar_link?(:boards)
+ = nav_link(path: ['boards#index', 'boards#show']) do
+ = link_to group_boards_path(@group), title: boards_link_text do
+ %span
+ = boards_link_text
+
- if group_sidebar_link?(:labels)
= nav_link(path: 'labels#index') do
= link_to group_labels_path(@group), title: 'Labels' do
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 6b847fb4b7c..6b51483810e 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,4 +1,4 @@
-- page_title @project.name_with_namespace
+- page_title @project.full_name
- page_description @project.description unless page_description
- header_title project_title(@project) unless header_title
- nav "project"
diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml
index f0ba7827cef..71c62f6be4e 100644
--- a/app/views/notify/project_was_exported_email.html.haml
+++ b/app/views/notify/project_was_exported_email.html.haml
@@ -3,6 +3,6 @@
%p
The project export can be downloaded from:
= link_to download_export_project_url(@project), rel: 'nofollow', download: '' do
- = @project.name_with_namespace + " export"
+ = @project.full_name + " export"
%p
The download link will expire in 24 hours.
diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml
index c476a39b661..1b6b1a81665 100644
--- a/app/views/notify/project_was_moved_email.html.haml
+++ b/app/views/notify/project_was_moved_email.html.haml
@@ -3,7 +3,7 @@
%p
The project is now located under
= link_to project_url(@project) do
- = @project.name_with_namespace
+ = @project.full_name
%p
To update the remote url in your local repository run (for ssh):
%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" }
diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml
deleted file mode 100644
index a8eb66ca13c..00000000000
--- a/app/views/profiles/_head.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('profile')
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 0f849f6f8b7..02263095599 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -1,6 +1,5 @@
- page_title "Account"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
- if current_user.ldap_user?
.alert.alert-info
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index cbea5ca605a..a924369050b 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -1,6 +1,5 @@
- page_title "Authentication log"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
index fe1cf802971..c7094800fb2 100644
--- a/app/views/profiles/chat_names/_chat_name.html.haml
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -4,7 +4,7 @@
%td
%strong
- if can?(current_user, :read_project, project)
- = link_to project.name_with_namespace, project_path(project)
+ = link_to project.full_name, project_path(project)
- else
.light N/A
%td
diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml
index 8f7121afe02..4b6e419af50 100644
--- a/app/views/profiles/chat_names/index.html.haml
+++ b/app/views/profiles/chat_names/index.html.haml
@@ -1,6 +1,5 @@
- page_title 'Chat'
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index df1df4f5d72..e3c2bd1150e 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,6 +1,5 @@
- page_title "Emails"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
index e44506ec9c9..1d2e41cb437 100644
--- a/app/views/profiles/gpg_keys/index.html.haml
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -1,6 +1,5 @@
- page_title "GPG Keys"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 5f7b41cf30e..1e206def7ee 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,6 +1,5 @@
- page_title "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
@@ -13,7 +12,9 @@
Add an SSH key
%p.profile-settings-content
Before you can add an SSH key you need to
- = link_to "generate it.", help_page_path("ssh/README")
+ = link_to "generate one", help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
+ or use an
+ = link_to "existing key.", help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
= render 'form'
%hr
%h5
diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml
index 7b7960708c4..28be6172219 100644
--- a/app/views/profiles/keys/show.html.haml
+++ b/app/views/profiles/keys/show.html.haml
@@ -2,5 +2,4 @@
- breadcrumb_title @key.title
- page_title @key.title, "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
= render "key_details"
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 202eccb7bb6..8f099aa6dd7 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,6 +1,5 @@
- page_title "Notifications"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
%div
- if @user.errors.any?
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index f445e5a2417..78848542810 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -2,7 +2,6 @@
- page_title "Personal Access Tokens"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 66d1d1e8d44..6aefd97bb96 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,6 +1,5 @@
- page_title 'Preferences'
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-4.application-theme
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 110736dc557..e497eab32e0 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,6 +1,5 @@
- breadcrumb_title "Edit Profile"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f|
= form_errors(@user)
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index e58cd20402c..1bd10018b40 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -2,13 +2,6 @@
- add_to_breadcrumbs("Two-Factor Authentication", profile_account_path)
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
-
-- content_for :page_specific_javascripts do
- - if inject_u2f_api?
- = webpack_bundle_tag('u2f')
- = webpack_bundle_tag('two_factor_auth')
-
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
.row.prepend-top-default
.col-lg-4
diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml
index b55dc3dce5c..b387e38c1a6 100644
--- a/app/views/projects/_commit_button.html.haml
+++ b/app/views/projects/_commit_button.html.haml
@@ -3,6 +3,4 @@
= link_to 'Cancel', cancel_path,
class: 'btn btn-cancel', data: {confirm: leave_edit_message}
- - unless can?(current_user, :push_code, @project)
- .inline.prepend-left-10
- = commit_in_fork_help
+ = render 'shared/projects/edit_information'
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index b565f14747a..a2ecfddb163 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -23,6 +23,12 @@
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_source_name(@project) }
+ .project-badges
+ - @project.badges.each do |badge|
+ - badge_link_url = badge.rendered_link_url(@project)
+ %a{ href: badge_link_url, target: '_blank', rel: 'noopener noreferrer' }
+ %img{ src: badge.rendered_image_url(@project), alt: badge_link_url }
+
.project-repo-buttons
.count-buttons
= render 'projects/buttons/star'
diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml
index 749e273b2e2..c137e38ed50 100644
--- a/app/views/projects/_issuable_by_email.html.haml
+++ b/app/views/projects/_issuable_by_email.html.haml
@@ -18,7 +18,14 @@
.email-modal-input-group.input-group
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-btn
- = clipboard_button(target: '#issuable_email')
+ = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard btn-transparent hidden-xs')
+ = mail_to email, class: 'btn btn-clipboard btn-transparent',
+ subject: _("Enter the #{name} title"),
+ body: _("Enter the #{name} description"),
+ title: _('Send email'),
+ data: { toggle: 'tooltip', placement: 'bottom' } do
+ = sprite_icon('mail')
+
%p
= render 'by_email_description'
%p
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index d367bd6be7b..f4b5ef1555e 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -1,6 +1,8 @@
- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
+- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
.row{ id: project_name_id }
+ = f.hidden_field :ci_cd_only, value: ci_cd_only
.form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-light' do
%span
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 1b150ec3e5c..f93bb02acb9 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -12,7 +12,6 @@
.btn-group{ role: "group" }<
= edit_blob_button
- = ide_edit_button
- if current_user
= replace_blob_link
= delete_blob_link
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index 5d48a35dc4c..48ff66900be 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -17,6 +17,4 @@
= submit_tag _("Create directory"), class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- - unless can?(current_user, :push_code, @project)
- .inline.prepend-left-10
- = commit_in_fork_help
+ = render 'shared/projects/edit_information'
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index f1324c61500..182d02376bf 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -24,6 +24,4 @@
= button_title
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- - unless can?(current_user, :push_code, @project)
- .inline.prepend-left-10
- = commit_in_fork_help
+ = render 'shared/projects/edit_information'
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index cc85e5de40f..3124443b4e4 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -1,9 +1,10 @@
- hidden = local_assigns.fetch(:hidden, false)
- render_error = viewer.render_error
+- rich_type = viewer.type == :rich ? viewer.partial_name : nil
- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
-.blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) }
+.blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) }
- if render_error
= render 'projects/blob/render_error', viewer: viewer
- elsif load_async
diff --git a/app/views/projects/blob/viewers/_balsamiq.html.haml b/app/views/projects/blob/viewers/_balsamiq.html.haml
index 15349387eb2..b20106e8c3a 100644
--- a/app/views/projects/blob/viewers/_balsamiq.html.haml
+++ b/app/views/projects/blob/viewers/_balsamiq.html.haml
@@ -1,4 +1 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('balsamiq_viewer')
-
.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: blob_raw_path } }
diff --git a/app/views/projects/blob/viewers/_notebook.html.haml b/app/views/projects/blob/viewers/_notebook.html.haml
index d1ffaca35b9..eb4ca1b9816 100644
--- a/app/views/projects/blob/viewers/_notebook.html.haml
+++ b/app/views/projects/blob/viewers/_notebook.html.haml
@@ -1,5 +1 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('notebook_viewer')
-
.file-content#js-notebook-viewer{ data: { endpoint: blob_raw_path } }
diff --git a/app/views/projects/blob/viewers/_pdf.html.haml b/app/views/projects/blob/viewers/_pdf.html.haml
index fc3f0d922b1..95d837a57dc 100644
--- a/app/views/projects/blob/viewers/_pdf.html.haml
+++ b/app/views/projects/blob/viewers/_pdf.html.haml
@@ -1,5 +1 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('pdf_viewer')
-
.file-content#js-pdf-viewer{ data: { endpoint: blob_raw_path } }
diff --git a/app/views/projects/blob/viewers/_sketch.html.haml b/app/views/projects/blob/viewers/_sketch.html.haml
index 8fb67c819c1..b4b6492b92f 100644
--- a/app/views/projects/blob/viewers/_sketch.html.haml
+++ b/app/views/projects/blob/viewers/_sketch.html.haml
@@ -1,7 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('sketch_viewer')
-
.file-content#js-sketch-viewer{ data: { endpoint: blob_raw_path } }
.js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' }
= icon('spinner spin 2x', 'aria-hidden' => 'true');
diff --git a/app/views/projects/blob/viewers/_stl.html.haml b/app/views/projects/blob/viewers/_stl.html.haml
index e58809ec008..55dd8cba7fe 100644
--- a/app/views/projects/blob/viewers/_stl.html.haml
+++ b/app/views/projects/blob/viewers/_stl.html.haml
@@ -1,6 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('stl_viewer')
-
.file-content.is-stl-loading
.text-center#js-stl-viewer{ data: { endpoint: blob_raw_path } }
= icon('spinner spin 2x', class: 'prepend-top-default append-bottom-default', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml
new file mode 100644
index 00000000000..12e5a8e8d69
--- /dev/null
+++ b/app/views/projects/branches/_panel.html.haml
@@ -0,0 +1,19 @@
+- branches = local_assigns.fetch(:branches)
+- state = local_assigns.fetch(:state)
+- panel_title = local_assigns.fetch(:panel_title)
+- show_more_text = local_assigns.fetch(:show_more_text)
+- project = local_assigns.fetch(:project)
+- overview_max_branches = local_assigns.fetch(:overview_max_branches)
+
+- return unless branches.any?
+
+.panel.panel-default.prepend-top-10
+ .panel-heading
+ %h4.panel-title
+ = panel_title
+ %ul.content-list.all-branches
+ - branches.first(overview_max_branches).each do |branch|
+ = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch)
+ - if branches.size > overview_max_branches
+ .panel-footer.text-center
+ = link_to show_more_text, project_branches_filtered_path(project, state: state), id: "state-#{state}", data: { state: state }
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index fb770764364..5dcc72d8263 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -3,26 +3,35 @@
%div{ class: container_class }
.top-area.adjust
- - if can?(current_user, :admin_project, @project)
- .nav-text
- - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
- = s_('Branches|Protected branches can be managed in %{project_settings_link}').html_safe % { project_settings_link: project_settings_link }
+ %ul.nav-links.issues-state-filters
+ %li{ class: active_when(@mode == 'overview') }>
+ = link_to s_('Branches|Overview'), project_branches_path(@project), title: s_('Branches|Show overview of the branches')
+
+ %li{ class: active_when(@mode == 'active') }>
+ = link_to s_('Branches|Active'), project_branches_filtered_path(@project, state: 'active'), title: s_('Branches|Show active branches')
+
+ %li{ class: active_when(@mode == 'stale') }>
+ = link_to s_('Branches|Stale'), project_branches_filtered_path(@project, state: 'stale'), title: s_('Branches|Show stale branches')
+
+ %li{ class: active_when(!%w[overview active stale].include?(@mode)) }>
+ = link_to s_('Branches|All'), project_branches_filtered_path(@project, state: 'all'), title: s_('Branches|Show all branches')
.nav-controls
- = form_tag(filter_branches_path, method: :get) do
+ = form_tag(project_branches_filtered_path(@project, state: 'all'), method: :get) do
= search_field_tag :search, params[:search], { placeholder: s_('Branches|Filter by branch name'), id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
- .dropdown.inline>
- %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.light
- = branches_sort_options_hash[@sort]
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
- %li.dropdown-header
- = s_('Branches|Sort by')
- - branches_sort_options_hash.each do |value, title|
- %li
- = link_to title, filter_branches_path(sort: value), class: ("is-active" if @sort == value)
+ - unless @mode == 'overview'
+ .dropdown.inline>
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %span.light
+ = branches_sort_options_hash[@sort]
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ = s_('Branches|Sort by')
+ - branches_sort_options_hash.each do |value, title|
+ %li
+ = link_to title, project_branches_filtered_path(@project, state: 'all', search: params[:search], sort: value), class: ("is-active" if @sort == value)
- if can? current_user, :push_code, @project
= link_to project_merged_branches_path(@project),
@@ -35,7 +44,17 @@
= link_to new_project_branch_path(@project), class: 'btn btn-create' do
= s_('Branches|New branch')
- - if @branches.any?
+ - if can?(current_user, :admin_project, @project)
+ - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
+ .row-content-block
+ %h5
+ = s_('Branches|Protected branches can be managed in %{project_settings_link}.').html_safe % { project_settings_link: project_settings_link }
+
+ - if @mode == 'overview' && (@active_branches.any? || @stale_branches.any?)
+ = render "projects/branches/panel", branches: @active_branches, state: 'active', panel_title: s_('Branches|Active branches'), show_more_text: s_('Branches|Show more active branches'), project: @project, overview_max_branches: @overview_max_branches
+ = render "projects/branches/panel", branches: @stale_branches, state: 'stale', panel_title: s_('Branches|Stale branches'), show_more_text: s_('Branches|Show more stale branches'), project: @project, overview_max_branches: @overview_max_branches
+
+ - elsif @branches.any?
%ul.content-list.all-branches
- @branches.each do |branch|
= render "projects/branches/branch", branch: branch, merged: @merged_branch_names.include?(branch.name)
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index e9d8fc75142..c7fc5a98ca8 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -28,4 +28,5 @@
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 0cd2d45c74b..9126476e79e 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -63,7 +63,7 @@
- if admin
%td
- if job.project
- = link_to job.project.name_with_namespace, admin_project_path(job.project)
+ = link_to job.project.full_name, admin_project_path(job.project)
%td
- if job.try(:runner)
= runner_link(job.runner)
diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml
index d4c0cd82ce3..db97203a2aa 100644
--- a/app/views/projects/clusters/_integration_form.html.haml
+++ b/app/views/projects/clusters/_integration_form.html.haml
@@ -21,6 +21,12 @@
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
.form-group
+ %h5= s_('ClusterIntegration|Security')
+ %p
+ = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.")
+ = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
+
+ .form-group
%h5= s_('ClusterIntegration|Environment scope')
%p
= s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index 2b1b23ba198..2ee0eafcf1a 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -10,11 +10,13 @@
install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm),
install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),
install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus),
+ install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner),
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
+ ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'),
manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } }
.js-cluster-application-notice
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 93407956f56..21e4664d4e4 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -35,6 +35,4 @@
= submit_tag label, class: 'btn btn-create'
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- - unless can?(current_user, :push_code, @project)
- .inline.prepend-left-10
- = commit_in_fork_help
+ = render 'shared/projects/edit_information'
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 3f699882c5f..68b35072f26 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -6,7 +6,3 @@
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
} }
-
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('commit_pipelines')
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 4058e61eb9a..abb292f8f27 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -6,9 +6,6 @@
- @content_class = limited_container_width
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('diff_notes')
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 6ff7bcae54f..078bd0eee63 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -20,7 +20,7 @@
.avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
- .commit-detail
+ .commit-detail.flex-list
.commit-content
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
%span.commit-row-message.visible-xs-inline
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index d98e0564da4..5041f322612 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,8 +1,5 @@
- @no_container = true
- page_title "Cycle Analytics"
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('cycle_analytics')
#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
- if @cycle_analytics_no_data
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index b947b91322d..a96485ab155 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -70,6 +70,7 @@
Enable or disable certain project features and choose access levels.
.settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
+ -# haml-lint:disable InlineJavaScript
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index eca10d99908..1ac7dab6775 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -1,10 +1,6 @@
- @no_container = true
- page_title "Environments"
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag("environments_folder")
-
#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
"folder-name" => @folder,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 31cf173fa9c..7ebe617766f 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -2,10 +2,6 @@
- page_title "Environments"
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag("common_vue")
- = webpack_bundle_tag("environments")
-
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index 91b3743e9e7..c151b5acdf7 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -1,7 +1,5 @@
- @no_container = true
- page_title "Metrics for environment", @environment.name
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
.prometheus-container{ class: container_class }
.top-area
@@ -17,7 +15,8 @@
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
- "additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json),
+ "metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json),
+ "deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json),
"project-path": project_path(@project),
"tags-path": project_tags_path(@project),
- "has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } }
+ "has-metrics": "#{@environment.has_metrics?}" } }
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index 7be4ef39117..6ec4ff56552 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -3,7 +3,6 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag "xterm/xterm"
- = webpack_bundle_tag("terminal")
%div{ class: container_class }
.top-area
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 2599ce5c4b8..620fd1906ba 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -53,7 +53,7 @@
- if admin
%td
- if generic_commit_status.project
- = link_to generic_commit_status.project.name_with_namespace, admin_project_path(generic_commit_status.project)
+ = link_to generic_commit_status.project.full_name, admin_project_path(generic_commit_status.project)
%td
- if generic_commit_status.try(:runner)
= runner_link(generic_commit_status.runner)
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index d4b4a6203f3..14c47a5d91c 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -74,6 +74,7 @@
= _("Commits per day hour (UTC)")
%canvas#hour-chart
+-# haml-lint:disable InlineJavaScript
%script#projectChartData{ type: "application/json" }
- projectChartData = {};
- projectChartData['hour'] = @commits_per_time
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 8c490773a56..3b0c828ccd1 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,12 +1,11 @@
-- page_title @project.forked? ? "Forking in progress" : "Import in progress"
+- page_title import_in_progress_title
+
.save-project-loader
.center
%h2
%i.fa.fa-spinner.fa-spin
- - if @project.forked?
- Forking in progress.
- - else
- Import in progress.
- - if @project.external_import?
+ = import_in_progress_title
+ - if !has_ci_cd_only_params? && @project.external_import?
%p.monospace git clone --bare #{@project.safe_import_url}
- %p Please wait while we import the repository for you. Refresh at will.
+ %p
+ = import_wait_and_refresh_message
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 11b5e02f1e0..cdfc3e232c5 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -6,14 +6,6 @@
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%section.js-vue-notes-event
- #js-vue-notes{ data: { discussions_path: discussions_project_issue_path(@project, @issue, format: :json),
- register_path: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
- new_session_path: new_session_path(:user, redirect_to_referer: 'yes'),
- markdown_docs_path: help_page_path('user/markdown'),
- quick_actions_docs_path: help_page_path('user/project/quick_actions'),
- notes_path: notes_url,
- close_issue_path: issue_path(@issue, issue: { state_event: :close }, format: 'json'),
- reopen_issue_path: issue_path(@issue, issue: { state_event: :reopen }, format: 'json'),
- last_fetched_at: Time.now.to_i,
- noteable_data: serialize_issuable(@issue),
- current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
+ #js-vue-notes{ data: { notes_data: notes_data(@issue),
+ noteable_data: serialize_issuable(@issue),
+ current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 5f97d31f610..5c36d2202a6 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -18,7 +18,7 @@
- unless @issue.project.id == merge_request.target_project.id
in
- project = merge_request.target_project
- = link_to project.name_with_namespace, project_path(project)
+ = link_to project.full_name, project_path(project)
- if merge_request.merged?
%span.merge-request-status.prepend-left-10.merged
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index fb06ba58c27..c427a9eedc2 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -4,9 +4,6 @@
- page_title "Issues"
- new_issue_email = @project.new_issuable_address(current_user, 'issue')
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
-
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index d63443c9da5..ec7e87219f5 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -55,6 +55,7 @@
.issue-details.issuable-details
.detail-page-description.content-block
+ -# haml-lint:disable InlineJavaScript
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json
#js-issuable-app
%h2.title= markdown_field(@issue, :title)
@@ -73,7 +74,7 @@
.content-block.emoji-block
.row
- .col-sm-8.js-issue-note-awards
+ .col-sm-8.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @issue, inline: true
.col-sm-4.new-branch-col
= render 'new_branch' unless @issue.confidential?
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 80e4dce1a80..9c78bade254 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -4,6 +4,7 @@
- can_admin_label = can?(current_user, :admin_label, @project)
- if @labels.exists? || @prioritized_labels.exists?
+ #promote-label-modal
%div{ class: container_class }
.top-area.adjust
.nav-text
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index 2a2e57027be..a6e2565a485 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,7 +1,5 @@
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('merge_conflicts')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/mr_title"
diff --git a/app/views/projects/merge_requests/conflicts/show.html.haml b/app/views/projects/merge_requests/conflicts/show.html.haml
index 2a2e57027be..a6e2565a485 100644
--- a/app/views/projects/merge_requests/conflicts/show.html.haml
+++ b/app/views/projects/merge_requests/conflicts/show.html.haml
@@ -1,7 +1,5 @@
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('merge_conflicts')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/mr_title"
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 720ba236434..b2c0d9e1cfa 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -6,9 +6,6 @@
- page_title "Merge Requests"
- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
-
%div{ class: container_class }
= render 'projects/last_push'
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index e29f21b3bec..9866cc716ee 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -1,11 +1,10 @@
+- @gfm_form = true
- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs "Merge Requests", project_merge_requests_path(@project)
- breadcrumb_title @merge_request.to_reference
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
.merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } }
= render "projects/merge_requests/mr_title"
@@ -23,10 +22,7 @@
#js-vue-mr-widget.mr-widget
- - content_for :page_specific_javascripts do
- = webpack_bundle_tag 'vue_merge_request_widget'
-
- .content-block.content-block-small.emoji-list-container
+ .content-block.content-block-small.emoji-list-container.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
@@ -54,28 +50,37 @@
= tab_link_for @merge_request, :diffs do
Changes
%span.badge= @merge_request.diff_size
- #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
- %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- %div
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
- = render 'shared/icons/icon_status_success_solid.svg'
- %template{ 'v-else' => '' }
- = render 'shared/icons/icon_resolve_discussion.svg'
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
- = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
- = render "discussions/jump_to_next"
+
+ - if has_vue_discussions_cookie?
+ #js-vue-discussion-counter
+ - else
+ #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
+ %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
+ %div
+ .line-resolve-all{ "v-show" => "discussionCount > 0",
+ ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
+ %span.line-resolve-btn.is-disabled{ type: "button",
+ ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
+ %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
+ = render 'shared/icons/icon_status_success_solid.svg'
+ %template{ 'v-else' => '' }
+ = render 'shared/icons/icon_resolve_discussion.svg'
+ %span.line-resolve-text
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
+ = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
+ = render "discussions/jump_to_next"
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
.row
%section.col-md-12
- .issuable-discussion
+ %script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
+ .issuable-discussion.js-vue-notes-event
= render "projects/merge_requests/discussion"
+ - if has_vue_discussions_cookie?
+ #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
+ noteable_data: serialize_issuable(@merge_request),
+ current_user_data: UserSerializer.new.represent(current_user).to_json} }
#commits.commits.tab-pane
-# This tab is always loaded via AJAX
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 6a7bc4b1888..5b0197ed58c 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -13,6 +13,7 @@
.milestones
#delete-milestone-modal
+ #promote-milestone-modal
%ul.content-list
= render @milestones
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index de381d489c6..b423888c875 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -27,8 +27,15 @@
Edit
- if @project.group
- = link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting #{@milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do
- Promote
+ %button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
+ target: '#promote-milestone-modal',
+ milestone_title: @milestone.title,
+ url: promote_project_milestone_path(@milestone.project, @milestone),
+ container: 'body' },
+ disabled: true,
+ type: 'button' }
+ = _('Promote')
+ #promote-milestone-modal
- if @milestone.active?
= link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 97be8950db0..4b7be9a223f 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,7 +1,5 @@
- breadcrumb_title "Graph"
- page_title "Graph", @ref
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('network')
= render "head"
%div{ class: container_class }
.project-network
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 679ba23a4db..8cdb0a6aff4 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -12,11 +12,14 @@
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
- New project
+ = _('New project')
%p
- A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}.
+ - among_other_things_link = link_to _('among other things'), help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'
+ = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
%p
- All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
+ = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
+ -# EE-specific start
+ -# EE-specific end
.md
= brand_new_project_guidelines
%p
@@ -28,36 +31,38 @@
.col-lg-9.js-toggle-container
%ul.nav-links.gitlab-tabs{ role: 'tablist' }
- %li{ class: ('active' if active_tab == 'blank'), role: 'presentation' }
+ %li{ class: active_when(active_tab == 'blank'), role: 'presentation' }
%a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Blank project
%span.visible-xs Blank
- %li{ class: ('active' if active_tab == 'template'), role: 'presentation' }
+ %li{ class: active_when(active_tab == 'template'), role: 'presentation' }
%a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Create from template
%span.visible-xs Template
- %li{ class: ('active' if active_tab == 'import'), role: 'presentation' }
+ %li{ class: active_when(active_tab == 'import'), role: 'presentation' }
%a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Import project
%span.visible-xs Import
+ -# EE-specific start
+ -# EE-specific end
.tab-content.gitlab-tab-content
- .tab-pane{ id: 'blank-project-pane', class: ('active' if active_tab == 'blank'), role: 'tabpanel' }
+ .tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
- .tab-pane.no-padding{ id: 'create-from-template-pane', class: ('active' if active_tab == 'template'), role: 'tabpanel' }
+ .tab-pane.no-padding{ id: 'create-from-template-pane', class: active_when(active_tab == 'template'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
.project-template
.form-group
%div
= render 'project_templates', f: f
- .tab-pane.import-project-pane{ id: 'import-project-pane', class: ('active' if active_tab == 'import'), role: 'tabpanel' }
+ .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
- if import_sources_enabled?
.project-import.row
- .col-sm-12
+ .col-lg-12
.form-group.import-btn-container.clearfix
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
Import project from
@@ -68,7 +73,7 @@
= icon('gitlab', text: 'GitLab export')
%div
- if github_import_enabled?
- = link_to new_import_github_path, class: 'btn import_github' do
+ = link_to new_import_github_path, class: 'btn js-import-github' do
= icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
@@ -97,7 +102,7 @@
Gitea
%div
- if git_import_enabled?
- %button.btn.js-toggle-button.import_git{ type: "button" }
+ %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
= icon('git', text: 'Repo by URL')
.col-lg-12
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
@@ -105,6 +110,10 @@
= render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
+
+ -# EE-specific start
+ -# EE-specific end
+
.save-project-loader.hide
.center
%h2
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index ca1b41b140a..d81b07832bb 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -1,34 +1,30 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
- - if @domain.errors.any?
- #error_explanation
- .alert.alert-danger
- - @domain.errors.full_messages.each do |msg|
- %p= msg
+- if @domain.errors.any?
+ #error_explanation
+ .alert.alert-danger
+ - @domain.errors.full_messages.each do |msg|
+ %p= msg
+.form-group
+ = f.label :domain, class: 'control-label' do
+ Domain
+ .col-sm-10
+ = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted?
+
+- if Gitlab.config.pages.external_https
.form-group
- = f.label :domain, class: 'control-label' do
- Domain
+ = f.label :certificate, class: 'control-label' do
+ Certificate (PEM)
.col-sm-10
- = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control'
-
- - if Gitlab.config.pages.external_https
- .form-group
- = f.label :certificate, class: 'control-label' do
- Certificate (PEM)
- .col-sm-10
- = f.text_area :certificate, rows: 5, class: 'form-control'
- %span.help-inline Upload a certificate for your domain with all intermediates
-
- .form-group
- = f.label :key, class: 'control-label' do
- Key (PEM)
- .col-sm-10
- = f.text_area :key, rows: 5, class: 'form-control'
- %span.help-inline Upload a private key for your certificate
- - else
- .nothing-here-block
- Support for custom certificates is disabled.
- Ask your system's administrator to enable it.
+ = f.text_area :certificate, rows: 5, class: 'form-control'
+ %span.help-inline Upload a certificate for your domain with all intermediates
- .form-actions
- = f.submit 'Create New Domain', class: "btn btn-save"
+ .form-group
+ = f.label :key, class: 'control-label' do
+ Key (PEM)
+ .col-sm-10
+ = f.text_area :key, rows: 5, class: 'form-control'
+ %span.help-inline Upload a private key for your certificate
+- else
+ .nothing-here-block
+ Support for custom certificates is disabled.
+ Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
new file mode 100644
index 00000000000..5645a4604bf
--- /dev/null
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -0,0 +1,11 @@
+- add_to_breadcrumbs "Pages", project_pages_path(@project)
+- breadcrumb_title @domain.domain
+- page_title @domain.domain
+%h3.page_title
+ = @domain.domain
+%hr.clearfix
+%div
+ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ = render 'form', { f: f }
+ .form-actions
+ = f.submit 'Save Changes', class: "btn btn-save"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index e1477c71d06..5a397c9d3c7 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -1,6 +1,10 @@
+- add_to_breadcrumbs "Pages", project_pages_path(@project)
- page_title 'New Pages Domain'
%h3.page_title
New Pages Domain
%hr.clearfix
%div
- = render 'form'
+ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ = render 'form', { f: f }
+ .form-actions
+ = f.submit 'Create New Domain', class: "btn btn-save"
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index 72e9203bdb0..ba0713daee9 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -1,4 +1,7 @@
+- add_to_breadcrumbs "Pages", project_pages_path(@project)
+- breadcrumb_title @domain.domain
- page_title "#{@domain.domain}", 'Pages Domains'
+
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
- if verification_enabled && @domain.unverified?
%p.alert.alert-warning
@@ -8,6 +11,7 @@
%h3.page-title
Pages Domain
+ = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success pull-right'
.table-holder
%table.table
diff --git a/app/views/projects/pipelines/charts/_pipeline_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml
index 510697c2ae9..c23fe6ff170 100644
--- a/app/views/projects/pipelines/charts/_pipeline_times.haml
+++ b/app/views/projects/pipelines/charts/_pipeline_times.haml
@@ -4,4 +4,5 @@
%canvas#build_timesChart{ height: 200 }
+-# haml-lint:disable InlineJavaScript
%script#pipelinesTimesChartsData{ type: "application/json" }= { :labels => @charts[:pipeline_times].labels, :values => @charts[:pipeline_times].pipeline_times }.to_json.html_safe
diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml
index 2f4b6def155..14b3d47a9c2 100644
--- a/app/views/projects/pipelines/charts/_pipelines.haml
+++ b/app/views/projects/pipelines/charts/_pipelines.haml
@@ -26,6 +26,7 @@
= _("Pipelines for last year")
%canvas#yearChart.padded{ height: 250 }
+-# haml-lint:disable InlineJavaScript
%script#pipelinesChartsData{ type: "application/json" }
- chartData = []
- [:week, :month, :year].each do |scope|
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index fdcc60f48a5..3e6b3346787 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -7,11 +7,9 @@
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
- "new-pipeline-path" => new_project_pipeline_path(@project),
+ "no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
- "has-ci" => @repository.gitlab_ci_yml,
- "ci-lint-path" => ci_lint_path,
- "reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } }
-
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('pipelines')
+ "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project),
+ "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path,
+ "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) ,
+ "has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } }
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 4ad37d0e882..877101b05ca 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -20,4 +20,5 @@
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 2174154b207..a7d7c923957 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -10,7 +10,3 @@
= render "projects/pipelines/with_tabs", pipeline: @pipeline
.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } }
-
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('pipelines_details')
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
deleted file mode 100644
index d15f4310ff5..00000000000
--- a/app/views/projects/project_members/update.js.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-:plain
- var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
- $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
- gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
diff --git a/app/views/projects/protected_branches/_index.html.haml b/app/views/projects/protected_branches/_index.html.haml
index 127a338e413..2b0a502fe4d 100644
--- a/app/views/projects/protected_branches/_index.html.haml
+++ b/app/views/projects/protected_branches/_index.html.haml
@@ -1,6 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('protected_branches')
-
- content_for :create_protected_branch do
= render 'projects/protected_branches/create_protected_branch'
diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml
index 74f7f63c941..6b284fda35c 100644
--- a/app/views/projects/protected_tags/_index.html.haml
+++ b/app/views/projects/protected_tags/_index.html.haml
@@ -1,6 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('protected_tags')
-
- content_for :create_protected_tag do
= render 'projects/protected_tags/create_protected_tag'
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 744b88760bc..12d56e244ce 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -14,9 +14,6 @@
.col-lg-12
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } }
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('registry_list')
-
.row.prepend-top-10
.col-lg-12
.panel.panel-default
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index e660fce652f..49c90869146 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -30,6 +30,11 @@
.col-sm-10
= f.text_field :token, class: 'form-control', readonly: true
.form-group
+ = label_tag :ip_address, class: 'control-label' do
+ IP Address
+ .col-sm-10
+ = f.text_field :ip_address, class: 'form-control', readonly: true
+ .form-group
= label_tag :description, class: 'control-label' do
Description
.col-sm-10
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index dfab04aa1fb..4e57f5f844d 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -41,6 +41,9 @@
%td Version
%td= @runner.version
%tr
+ %td IP Address
+ %td= @runner.ip_address
+ %tr
%td Revision
%td= @runner.revision
%tr
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 17e804d682b..053ea24b848 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -5,6 +5,9 @@
= boolean_to_icon @service.activated?
%p= @service.description
+
+ - if @service.respond_to?(:detailed_description)
+ %p= @service.detailed_description
.col-lg-9
= form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, subject: @service
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 5dbcbf7eba6..2ab0227126a 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -1,4 +1,4 @@
-- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}"
+- run_actions_text = "Perform common operations on GitLab project: #{@project.full_name}"
%p To setup this service:
%ul.list-unstyled.indent-list
@@ -20,7 +20,7 @@
.form-group
= label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
- = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
+ = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(target: '#display_name')
diff --git a/app/views/projects/services/prometheus/_configuration_banner.html.haml b/app/views/projects/services/prometheus/_configuration_banner.html.haml
new file mode 100644
index 00000000000..2cc2a6b2b5b
--- /dev/null
+++ b/app/views/projects/services/prometheus/_configuration_banner.html.haml
@@ -0,0 +1,26 @@
+%h4
+ = s_('PrometheusService|Auto configuration')
+
+- if service.manual_configuration?
+ .well
+ = s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
+- else
+ .container-fluid
+ .row
+ - if service.prometheus_installed?
+ .col-sm-2
+ .svg-container
+ = image_tag 'illustrations/monitoring/getting_started.svg'
+ .col-sm-10
+ %p.text-success.prepend-top-default
+ = s_('PrometheusService|Prometheus is being automatically managed on your clusters')
+ = link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'btn'
+ - else
+ .col-sm-2
+ = image_tag 'illustrations/monitoring/loading.svg'
+ .col-sm-10
+ %p.prepend-top-default
+ = s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
+ = link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(project), class: 'btn btn-success'
+
+%hr
diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/projects/services/prometheus/_help.html.haml
index 5e320a252d8..88acb824ba7 100644
--- a/app/views/projects/services/prometheus/_help.html.haml
+++ b/app/views/projects/services/prometheus/_help.html.haml
@@ -1,29 +1,5 @@
-%h4
- = s_('PrometheusService|Auto configuration')
-
-- if @service.manual_configuration?
- .well
- = s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
-- else
- .container-fluid
- .row
- - if @service.prometheus_installed?
- .col-sm-2
- .svg-container
- = image_tag 'illustrations/monitoring/getting_started.svg'
- .col-sm-10
- %p.text-success.prepend-top-default
- = s_('PrometheusService|Prometheus is being automatically managed on your clusters')
- = link_to s_('PrometheusService|Manage clusters'), project_clusters_path(@project), class: 'btn'
- - else
- .col-sm-2
- = image_tag 'illustrations/monitoring/loading.svg'
- .col-sm-10
- %p.prepend-top-default
- = s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
- = link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(@project), class: 'btn btn-success'
-
-%hr
+- if @project
+ = render 'projects/services/prometheus/configuration_banner', project: @project, service: @service
%h4.append-bottom-default
= s_('PrometheusService|Manual configuration')
diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml
index 6dc2b85fd32..43e6a173108 100644
--- a/app/views/projects/services/prometheus/_show.html.haml
+++ b/app/views/projects/services/prometheus/_show.html.haml
@@ -7,21 +7,19 @@
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus')
.col-lg-9
- .panel.panel-default.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json) } }
+ .panel.panel-default.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } }
.panel-heading
%h3.panel-title
- = s_('PrometheusService|Monitored')
+ = s_('PrometheusService|Common metrics')
%span.badge.js-monitored-count 0
.panel-body
- .loading-metrics.text-center.js-loading-metrics
- = icon('spinner spin 3x', class: 'metrics-load-spinner')
- %p
+ .loading-metrics.js-loading-metrics
+ %p.prepend-top-10.prepend-left-10
+ = icon('spinner spin', class: 'metrics-load-spinner')
= s_('PrometheusService|Finding and configuring metrics...')
- .empty-metrics.text-center.hidden.js-empty-metrics
- = custom_icon('icon_empty_metrics')
- %p
- = s_('PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment.')
- = link_to s_('PrometheusService|View environments'), project_environments_path(@project), class: 'btn btn-success'
+ .empty-metrics.hidden.js-empty-metrics
+ %p.text-tertiary.prepend-top-10.prepend-left-10
+ = s_('PrometheusService|Waiting for your first deployment to an environment to find common metrics')
%ul.list-unstyled.metrics-list.hidden.js-metrics-list
.panel.panel-default.hidden.js-panel-missing-env-vars
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index c31c95608c6..d592a5e4663 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,4 +1,4 @@
-- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
+- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 235d532bf98..6bef4d19434 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -2,9 +2,6 @@
- page_title "Repository"
- @content_class = "limit-container-width" unless fluid_layout
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
-
-# Protected branches & tags use a lot of nested partials.
-# The shared parts of the views can be found in the `shared` directory.
-# Those are used throughout the actual views. These `shared` views are then
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index da364b58e36..10415d011d6 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- @sort ||= sort_value_recently_updated
- page_title s_('TagsPage|Tags')
-- add_to_breadcrumbs("Repository", project_tree_path(@project))
.flex-list{ class: container_class }
.top-area.adjust
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 6e105a5521a..1827a3d323c 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -43,4 +43,5 @@
.form-actions
= button_tag s_('TagsPage|Create tag'), class: 'btn btn-create'
= link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 39511435508..06bce52e709 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -72,11 +72,6 @@
#{ _('New tag') }
.tree-controls
- - if show_new_ide?
- = succeed " " do
- = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do
- = _('Web IDE')
-
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
= render 'projects/find_file_link'
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 915e648a5d3..7d43fd61081 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -14,25 +14,25 @@
= link_to search_filter_path(scope: 'issues') do
Issues
%span.badge
- = @search_results.issues_count
+ = limited_count(@search_results.limited_issues_count)
- if project_search_tabs?(:merge_requests)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
%span.badge
- = @search_results.merge_requests_count
+ = limited_count(@search_results.limited_merge_requests_count)
- if project_search_tabs?(:milestones)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
%span.badge
- = @search_results.milestones_count
+ = limited_count(@search_results.limited_milestones_count)
- if project_search_tabs?(:notes)
%li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do
Comments
%span.badge
- = @search_results.notes_count
+ = limited_count(@search_results.limited_notes_count)
- if project_search_tabs?(:wiki)
%li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index e43796e9654..e4902d368e7 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -22,7 +22,7 @@
%span.dropdown-toggle-text
Project:
- if @project.present?
- = @project.name_with_namespace
+ = @project.full_name
- else
Any
= icon("chevron-down")
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 60ef44482f0..ab56f48ba4d 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -6,7 +6,7 @@
= search_entries_info(@search_objects, @scope, @search_term)
- unless @show_snippets
- if @project
- in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]}
+ in project #{link_to @project.full_name, [@project.namespace.becomes(Namespace), @project]}
- elsif @group
in group #{link_to @group.name, @group}
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index b4bc8982c05..b7a27ef6be2 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -10,4 +10,4 @@
.description.term
= search_md_sanitize(issue, :description)
%span.light
- #{issue.project.name_with_namespace}
+ #{issue.project.full_name}
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 1a5499e4d58..8b0fd74f680 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -11,4 +11,4 @@
.description.term
= search_md_sanitize(merge_request, :description)
%span.light
- #{merge_request.project.name_with_namespace}
+ #{merge_request.project.full_name}
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index a7e178dfa71..e4ab7b0541f 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -7,7 +7,7 @@
%i.fa.fa-comment
= link_to_member(project, note.author, avatar: false)
commented on
- = link_to project.name_with_namespace, project
+ = link_to project.full_name, project
&middot;
- if note.for_commit?
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 65710c09a89..d46c4d11e51 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -11,7 +11,7 @@
%small.pull-right.cgray
- if snippet_title.project_id?
- = link_to snippet_title.project.name_with_namespace, project_path(snippet_title.project)
+ = link_to snippet_title.project.full_name, project_path(snippet_title.project)
.snippet-info
= snippet_title.to_reference
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
index de52fd00157..7d3e243495f 100644
--- a/app/views/sent_notifications/unsubscribe.html.haml
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -1,7 +1,7 @@
- noteable = @sent_notification.noteable
- noteable_type = @sent_notification.noteable_type.titleize.downcase
- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
-- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace
+- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.full_name
%h3.page-title
Unsubscribe from #{noteable_type}
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 736afa085e8..5eaaa1448d5 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -1,17 +1,22 @@
+- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
+
.form-group.import-url-data
= f.label :import_url, class: 'label-light' do
- %span Git repository URL
+ %span
+ = _('Git repository URL')
- = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+ = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true
.well.prepend-top-20
%ul
%li
- The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
+ = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
%li
- If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
+ = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe
%li
- The import will time out after #{time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)}.
- For repositories that take longer, use a clone/push combination.
+ = import_will_timeout_message(ci_cd_only)
%li
- To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}.
+ = import_svn_message(ci_cd_only)
+
+-# EE-specific start
+-# EE-specific end
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 8847d11f623..5afbc78df53 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -48,8 +48,16 @@
.pull-right.hidden-xs.hidden-sm
- if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
- = link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting #{label.title} will make it available for all projects inside #{label.project.group.name}. Existing project labels with the same name will be merged. This action cannot be reversed.", toggle: "tooltip"}, method: :post do
- %span.sr-only Promote to Group
+ %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
+ disabled: true,
+ type: 'button',
+ data: { url: promote_project_label_path(label.project, label),
+ label_title: label.title,
+ label_color: label.color,
+ label_text_color: label.text_color,
+ target: '#promote-label-modal',
+ container: 'body',
+ toggle: 'modal' } }
= sprite_icon('level-up')
- if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 0a4a24ae807..9221fd1e025 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -1,3 +1,6 @@
+- project = @project.present(current_user: current_user)
+- branch_name = selected_branch
+
= render 'shared/commit_message_container', placeholder: placeholder
- if @project.empty_repo?
@@ -7,12 +10,14 @@
.form-group.branch
= label_tag 'branch_name', _('Target Branch'), class: 'control-label'
.col-sm-10
- = text_field_tag 'branch_name', @branch_name || tree_edit_branch, required: true, class: "form-control js-branch-name ref-name"
+ = text_field_tag 'branch_name', branch_name, required: true, class: "form-control js-branch-name ref-name"
.js-create-merge-request-container
= render 'shared/new_merge_request_checkbox'
+ - elsif project.can_current_user_push_to_branch?(branch_name)
+ = hidden_field_tag 'branch_name', branch_name
- else
- = hidden_field_tag 'branch_name', @branch_name || tree_edit_branch
+ = hidden_field_tag 'branch_name', branch_name
= hidden_field_tag 'create_merge_request', 1
= hidden_field_tag 'original_branch', @ref, class: 'js-original-branch'
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 479bd2cdb38..4c8c92d722a 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,6 +1,5 @@
- show_create = local_assigns.fetch(:show_create, false)
-- show_new_branch_form = show_new_ide? && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination
@@ -16,14 +15,3 @@
= dropdown_filter _("Search branches and tags")
= dropdown_content
= dropdown_loading
- - if show_new_branch_form
- = dropdown_footer do
- %ul.dropdown-footer-list
- %li
- %a.dropdown-toggle-page{ href: "#" }
- Create new branch
- - if show_new_branch_form
- .dropdown-page-two
- = dropdown_title("Create new branch", options: { back: true })
- = dropdown_content do
- .js-new-branch-dropdown
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 61b39afb5d4..355b3ac75ae 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -13,12 +13,12 @@
.col-sm-10
= form.check_box :active, disabled: disable_fields_service?(@service)
- - if @service.supported_events.present?
+ - if @service.configurable_events.present?
.form-group
= form.label :url, "Trigger", class: 'control-label'
.col-sm-10
- - @service.supported_events.each do |event|
+ - @service.configurable_events.each do |event|
%div
= form.check_box service_event_field_name(event), class: 'pull-left'
.prepend-left-20
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 3312254f5fb..44b09545a61 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -1,3 +1,5 @@
+- board = local_assigns.fetch(:board, nil)
+- group = local_assigns.fetch(:group, false)
- @no_breadcrumb_container = true
- @no_container = true
- @content_class = "issue-boards-content"
@@ -5,8 +7,8 @@
- page_title "Boards"
- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
+ -# haml-lint:disable InlineJavaScript
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
@@ -26,7 +28,7 @@
":root-path" => "rootPath",
":board-id" => "boardId",
":key" => "_uid" }
- = render "shared/boards/components/sidebar"
+ = render "shared/boards/components/sidebar", group: group
- if @project
%board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project),
"milestone-path" => milestones_filter_dropdown_path,
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index c687e66fd43..2e9ad380012 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -42,6 +42,7 @@
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
+ ":groupId" => ((current_board_parent.id if @group) || 'null'),
"ref" => "board-list" }
- if can?(current_user, :admin_list, current_board_parent)
%board-blank-state{ "v-if" => 'list.id == "blank"' }
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 6dfabd7ba4c..4c8f03f1498 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -33,6 +33,8 @@
= render 'shared/issuable/form/merge_params', issuable: issuable
+= render 'shared/issuable/form/contribution', issuable: issuable, form: form
+
- if @merge_request_to_resolve_discussions_of
.form-group
.col-sm-10.col-sm-offset-2
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index fabb17c7340..fc6f71ef60f 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -112,6 +112,7 @@
- if can?(current_user, :admin_label, board.parent)
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
- #js-add-issues-btn.prepend-left-10
+ - if @project
+ #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
- elsif type != :boards_modal
= render 'shared/sort_dropdown'
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index dc583d3eb3b..adaddda13eb 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,7 +1,4 @@
- todo = issuable_todo(issuable)
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('sidebar')
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
@@ -120,10 +117,12 @@
= render partial: "shared/issuable/label_page_create"
- if issuable.has_attribute?(:confidential)
+ -# haml-lint:disable InlineJavaScript
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point
- if issuable.has_attribute?(:discussion_locked)
+ -# haml-lint:disable InlineJavaScript
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
@@ -160,4 +159,5 @@
= _('Move')
= icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
+ -# haml-lint:disable InlineJavaScript
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml
new file mode 100644
index 00000000000..0f2d313a5cc
--- /dev/null
+++ b/app/views/shared/issuable/form/_contribution.html.haml
@@ -0,0 +1,20 @@
+- issuable = local_assigns.fetch(:issuable)
+- form = local_assigns.fetch(:form)
+
+- return unless issuable.is_a?(MergeRequest)
+- return unless issuable.for_fork?
+- return unless can?(current_user, :push_code, issuable.source_project)
+
+%hr
+
+.form-group
+ .control-label
+ = _('Contribution')
+ .col-sm-10
+ .checkbox
+ = form.label :allow_maintainer_to_push do
+ = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user)
+ = _('Allow edits from maintainers')
+ = link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access')
+ .help-block
+ = allow_maintainer_push_unavailable_reason(issuable)
diff --git a/app/views/shared/members/update.js.haml b/app/views/shared/members/update.js.haml
new file mode 100644
index 00000000000..55050bd8a15
--- /dev/null
+++ b/app/views/shared/members/update.js.haml
@@ -0,0 +1,6 @@
+- member = local_assigns.fetch(:member)
+
+:plain
+ var $listItem = $('#{escape_javascript(render('shared/members/member', member: member))}');
+ $("##{dom_id(member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(member)}"));
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 129f6ab604e..eba64daaadc 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -12,7 +12,7 @@
- if show_project_name
%strong #{project.name} &middot;
- elsif show_full_project_name
- %strong #{project.name_with_namespace} &middot;
+ %strong #{project.full_name} &middot;
- if issuable.is_a?(Issue)
= confidential_icon(issuable)
= link_to issuable.title, issuable_url_args, title: issuable.title
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index e3b2b53833e..5926867e2d7 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -27,7 +27,7 @@
- milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
%span.label.label-gray
- = dashboard ? milestone.project.name_with_namespace : milestone.project.name
+ = dashboard ? milestone.project.full_name : milestone.project.name
- if @group
.col-sm-6.milestone-actions
- if can?(current_user, :admin_milestones, @group)
@@ -51,18 +51,25 @@
\
- if @project.group
- = link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting #{milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do
- Promote
+ %button.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
+ disabled: true,
+ type: 'button',
+ data: { url: promote_project_milestone_path(milestone.project, milestone),
+ milestone_title: milestone.title,
+ target: '#promote-milestone-modal',
+ container: 'body',
+ toggle: 'modal' } }
+ = _('Promote')
= link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
- %button.js-delete-milestone-button.btn.btn-xs.btn-grouped.btn-danger{ data: { toggle: 'modal',
- target: '#delete-milestone-modal',
- milestone_id: milestone.id,
- milestone_title: markdown_field(milestone, :title),
- milestone_url: project_milestone_path(milestone.project, milestone),
- milestone_issue_count: milestone.issues.count,
- milestone_merge_request_count: milestone.merge_requests.count },
- disabled: true }
- = _('Delete')
- = icon('spin spinner', class: 'js-loading-icon hidden' )
+ %button.js-delete-milestone-button.btn.btn-xs.btn-grouped.btn-danger{ data: { toggle: 'modal',
+ target: '#delete-milestone-modal',
+ milestone_id: milestone.id,
+ milestone_title: markdown_field(milestone, :title),
+ milestone_url: project_milestone_path(milestone.project, milestone),
+ milestone_issue_count: milestone.issues.count,
+ milestone_merge_request_count: milestone.merge_requests.count },
+ disabled: true }
+ = _('Delete')
+ = icon('spin spinner', class: 'js-loading-icon hidden' )
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index cd4188daf5b..a942ebc328b 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -1,7 +1,5 @@
- affix_offset = local_assigns.fetch(:affix_offset, "50")
- project = local_assigns[:project]
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_vue')
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index fd0760d83a5..6006ab8b43f 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -56,7 +56,7 @@
- milestone.milestones.each do |ms|
%tr
%td
- - project_name = group ? ms.project.name : ms.project.name_with_namespace
+ - project_name = group ? ms.project.name : ms.project.full_name
= link_to project_name, project_milestone_path(ms.project, ms)
%td
= ms.issues_visible_to_user(current_user).opened.count
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index b3f865c5b47..1db7c4e67cf 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -1,13 +1,14 @@
- issuable = @issue || @merge_request
- discussion_locked = issuable&.discussion_locked?
-%ul#notes-list.notes.main-notes-list.timeline
- = render "shared/notes/notes"
+- unless has_vue_discussions_cookie?
+ %ul#notes-list.notes.main-notes-list.timeline
+ = render "shared/notes/notes"
= render 'shared/notes/edit_form', project: @project
- if can_create_note?
- %ul.notes.notes-form.timeline
+ %ul.notes.notes-form.timeline{ :class => ('hidden' if has_vue_discussions_cookie?) }
%li.timeline-entry
.timeline-entry-inner
.flash-container.timeline-content
@@ -34,4 +35,5 @@
is locked. Only
%b project members
can comment.
+-# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/shared/plugins/_index.html.haml b/app/views/shared/plugins/_index.html.haml
new file mode 100644
index 00000000000..fc643c3ecc2
--- /dev/null
+++ b/app/views/shared/plugins/_index.html.haml
@@ -0,0 +1,23 @@
+- plugins = Gitlab::Plugin.files
+
+.row.prepend-top-default
+ .col-lg-4
+ %h4.prepend-top-0
+ Plugins
+ %p
+ #{link_to 'Plugins', help_page_path('administration/plugins')} are similar to
+ system hooks but are executed as files instead of sending data to a URL.
+
+ .col-lg-8.append-bottom-default
+ - if plugins.any?
+ .panel.panel-default
+ .panel-heading
+ Plugins (#{plugins.count})
+ %ul.content-list
+ - plugins.each do |file|
+ %li
+ .monospace
+ = File.basename(file)
+ - else
+ %p.light-well.text-center
+ No plugins found.
diff --git a/app/views/shared/projects/_edit_information.html.haml b/app/views/shared/projects/_edit_information.html.haml
new file mode 100644
index 00000000000..ec9dc8f62c2
--- /dev/null
+++ b/app/views/shared/projects/_edit_information.html.haml
@@ -0,0 +1,6 @@
+- unless can?(current_user, :push_code, @project)
+ .inline.prepend-left-10
+ - if @project.branch_allows_maintainer_push?(current_user, selected_branch)
+ = commit_in_single_accessible_branch
+ - else
+ = commit_in_fork_help
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 2726a4934fb..c75c882a693 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,6 +1,5 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = webpack_bundle_tag('snippet')
.snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 491a8a41090..3acec88c2e3 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -31,7 +31,7 @@
%span.hidden-xs
in
= link_to project_path(snippet.project) do
- = snippet.project.name_with_namespace
+ = snippet.project.full_name
.pull-right.snippet-updated-at
%span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index f878bece2fa..7eb221620ad 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -1,6 +1,7 @@
#js-authenticate-u2f
%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
+-# haml-lint:disable InlineJavaScript
%script#js-authenticate-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index 79e8f8d0e89..cc0e93c0755 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -1,5 +1,6 @@
#js-register-u2f
+-# haml-lint:disable InlineJavaScript
%script#js-register-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 28a5e5da037..f65e8385ac8 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -24,6 +24,7 @@
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation
- gcp_cluster:check_gcp_project_billing
+- gcp_cluster:cluster_wait_for_ingress_ip_address
- github_import_advance_stage
- github_importer:github_import_import_diff_note
@@ -42,12 +43,11 @@
- pipeline_cache:expire_pipeline_cache
- pipeline_creation:create_pipeline
- pipeline_creation:run_pipeline_schedule
+- pipeline_background:archive_trace
- pipeline_default:build_coverage
- pipeline_default:build_trace_sections
-- pipeline_default:create_trace_artifact
- pipeline_default:pipeline_metrics
- pipeline_default:pipeline_notification
-- pipeline_default:update_head_pipeline_for_merge_request
- pipeline_hooks:build_hooks
- pipeline_hooks:pipeline_hooks
- pipeline_processing:build_finished
@@ -57,6 +57,7 @@
- pipeline_processing:pipeline_success
- pipeline_processing:pipeline_update
- pipeline_processing:stage_update
+- pipeline_processing:update_head_pipeline_for_merge_request
- repository_check:repository_check_clear
- repository_check:repository_check_single_repository
@@ -84,6 +85,7 @@
- new_note
- pages
- pages_domain_verification
+- plugin
- post_receive
- process_commit
- project_cache
diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb
new file mode 100644
index 00000000000..dea7425ad88
--- /dev/null
+++ b/app/workers/archive_trace_worker.rb
@@ -0,0 +1,10 @@
+class ArchiveTraceWorker
+ include ApplicationWorker
+ include PipelineBackgroundQueue
+
+ def perform(job_id)
+ Ci::Build.find_by(id: job_id).try do |job|
+ job.trace.archive!
+ end
+ end
+end
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index b5ed8d607b3..46f1ac09915 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -12,7 +12,7 @@ class BuildFinishedWorker
# We execute that async as this are two indepentent operations that can be executed after TraceSections and Coverage
BuildHooksWorker.perform_async(build.id)
- CreateTraceArtifactWorker.perform_async(build.id)
+ ArchiveTraceWorker.perform_async(build.id)
end
end
end
diff --git a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
new file mode 100644
index 00000000000..8ba5951750c
--- /dev/null
+++ b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
@@ -0,0 +1,11 @@
+class ClusterWaitForIngressIpAddressWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::CheckIngressIpAddressService.new(app).execute
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index 9a9fbaad653..100d86e38c8 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -22,7 +22,7 @@ module Gitlab
importer_class.new(object, project, client).execute
- counter.increment(project: project.path_with_namespace)
+ counter.increment(project: project.full_path)
end
def counter
diff --git a/app/workers/concerns/pipeline_background_queue.rb b/app/workers/concerns/pipeline_background_queue.rb
new file mode 100644
index 00000000000..8bf43de6b26
--- /dev/null
+++ b/app/workers/concerns/pipeline_background_queue.rb
@@ -0,0 +1,10 @@
+##
+# Concern for setting Sidekiq settings for the low priority CI pipeline workers.
+#
+module PipelineBackgroundQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :pipeline_background
+ end
+end
diff --git a/app/workers/create_trace_artifact_worker.rb b/app/workers/create_trace_artifact_worker.rb
deleted file mode 100644
index 11cda58021e..00000000000
--- a/app/workers/create_trace_artifact_worker.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class CreateTraceArtifactWorker
- include ApplicationWorker
- include PipelineQueue
-
- def perform(job_id)
- Ci::Build.preload(:project, :user).find_by(id: job_id).try do |job|
- Ci::CreateTraceArtifactService.new(job.project, job.user).execute(job)
- end
- end
-end
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 21da27973fe..2a4d65b5cb3 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -66,7 +66,7 @@ class EmailsOnPushWorker
# These are input errors and won't be corrected even if Sidekiq retries
rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
- logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}")
+ logger.info("Failed to send e-mail for project '#{project.full_name}' to #{recipient}: #{e}")
end
end
ensure
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 7ba224d74c8..55fb817ca6e 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -44,6 +44,10 @@ class GitGarbageCollectWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
+
+ # In case pack files are deleted, release libgit2 cache and open file
+ # descriptors ASAP instead of waiting for Ruby garbage collection
+ project.cleanup
ensure
cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present?
end
diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
index 073d6608082..a779e631516 100644
--- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -16,7 +16,7 @@ module Gitlab
def report_import_time(project)
duration = Time.zone.now - project.created_at
- path = project.path_with_namespace
+ path = project.full_path
histogram.observe({ project: path }, duration)
counter.increment
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index d3b95009364..66a0ff83bef 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -1,7 +1,7 @@
class PagesWorker
include ApplicationWorker
- sidekiq_options retry: false
+ sidekiq_options retry: 3
def perform(action, *arg)
send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/workers/plugin_worker.rb b/app/workers/plugin_worker.rb
new file mode 100644
index 00000000000..bfcc683d99a
--- /dev/null
+++ b/app/workers/plugin_worker.rb
@@ -0,0 +1,15 @@
+class PluginWorker
+ include ApplicationWorker
+
+ sidekiq_options retry: false
+
+ def perform(file_name, data)
+ success, message = Gitlab::Plugin.execute(file_name, data)
+
+ unless success
+ Gitlab::PluginLogger.error("Plugin Error => #{file_name}: #{message}")
+ end
+
+ true
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index f2b2c4428d3..3909dbf7d7f 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -55,7 +55,7 @@ class PostReceive
end
def process_wiki_changes(post_received)
- # Nothing defined here yet.
+ post_received.project.touch(:last_activity_at, :last_repository_updated_at)
end
def log(message)
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 5b25d980bdb..201e7f332b4 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -30,10 +30,9 @@ class ProcessCommitWorker
end
def process_commit_message(project, commit, user, author, default = false)
- # this is a GitLab generated commit message, ignore it.
- return if commit.merged_merge_request?(user)
-
- closed_issues = default ? commit.closes_issues(user) : []
+ # Ignore closing references from GitLab-generated commit messages.
+ find_closing_issues = default && !commit.merged_merge_request?(user)
+ closed_issues = find_closing_issues ? commit.closes_issues(user) : []
close_issues(project, user, author, commit, closed_issues) if closed_issues.any?
commit.create_cross_references!(author, closed_issues)
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index d80b3b15840..68960f72bf6 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -5,7 +5,7 @@ class RemoveExpiredMembersWorker
def perform
Member.expired.find_each do |member|
begin
- Members::AuthorizedDestroyService.new(member).execute
+ Members::DestroyService.new.execute(member, skip_authorization: true)
rescue => ex
logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
end
diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb
index f09d89aa170..76f84ff920f 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -2,6 +2,8 @@ class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker
include PipelineQueue
+ queue_namespace :pipeline_processing
+
def perform(merge_request_id)
merge_request = MergeRequest.find(merge_request_id)
pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last